lazyclaw 5.4.4 → 6.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 (91) hide show
  1. package/channels/handoff.mjs +36 -0
  2. package/channels-discord/index.mjs +76 -0
  3. package/channels-discord/package.json +14 -0
  4. package/channels-email/index.mjs +109 -0
  5. package/channels-email/package.json +14 -0
  6. package/channels-signal/index.mjs +69 -0
  7. package/channels-signal/package.json +9 -0
  8. package/channels-voice/index.mjs +81 -0
  9. package/channels-voice/package.json +9 -0
  10. package/channels-whatsapp/index.mjs +70 -0
  11. package/channels-whatsapp/package.json +13 -0
  12. package/cli.mjs +73 -7399
  13. package/commands/agents.mjs +669 -0
  14. package/commands/auth_nodes.mjs +323 -0
  15. package/commands/automation.mjs +582 -0
  16. package/commands/channels.mjs +255 -0
  17. package/commands/chat.mjs +1217 -0
  18. package/commands/config.mjs +315 -0
  19. package/commands/daemon.mjs +260 -0
  20. package/commands/misc.mjs +128 -0
  21. package/commands/providers.mjs +511 -0
  22. package/commands/sessions.mjs +343 -0
  23. package/commands/setup.mjs +741 -0
  24. package/commands/skills.mjs +218 -0
  25. package/commands/workflow.mjs +661 -0
  26. package/daemon/lib/auth.mjs +58 -0
  27. package/daemon/lib/cost.mjs +30 -0
  28. package/daemon/lib/provider.mjs +69 -0
  29. package/daemon/lib/respond.mjs +83 -0
  30. package/daemon/route_table.mjs +96 -0
  31. package/daemon/routes/_deps.mjs +36 -0
  32. package/daemon/routes/config.mjs +99 -0
  33. package/daemon/routes/conversation.mjs +371 -0
  34. package/daemon/routes/meta.mjs +239 -0
  35. package/daemon/routes/ops.mjs +185 -0
  36. package/daemon/routes/providers.mjs +211 -0
  37. package/daemon/routes/rates.mjs +90 -0
  38. package/daemon/routes/registry.mjs +223 -0
  39. package/daemon/routes/sessions.mjs +213 -0
  40. package/daemon/routes/skills.mjs +260 -0
  41. package/daemon/routes/workflows.mjs +224 -0
  42. package/daemon.mjs +23 -2085
  43. package/dotenv_min.mjs +23 -0
  44. package/first_run.mjs +15 -0
  45. package/goals_cron.mjs +37 -0
  46. package/lib/args.mjs +216 -0
  47. package/lib/config.mjs +113 -0
  48. package/lib/registry_boot.mjs +55 -0
  49. package/mas/agent_turn.mjs +2 -1
  50. package/mas/index_db.mjs +82 -0
  51. package/mas/learning.mjs +17 -1
  52. package/mas/mention_router.mjs +38 -10
  53. package/mas/provider_adapters.mjs +28 -4
  54. package/mas/scrub_env.mjs +34 -0
  55. package/mas/tool_runner.mjs +23 -7
  56. package/mas/tools/bash.mjs +10 -5
  57. package/mas/tools/browser.mjs +18 -0
  58. package/mas/tools/learning.mjs +24 -14
  59. package/mas/tools/recall.mjs +5 -1
  60. package/mas/tools/web.mjs +47 -11
  61. package/mas/trajectory_store.mjs +7 -4
  62. package/package.json +16 -2
  63. package/providers/auth_store.mjs +22 -0
  64. package/providers/claude_cli.mjs +28 -2
  65. package/providers/claude_cli_detect.mjs +46 -0
  66. package/providers/custom_provider.mjs +70 -0
  67. package/providers/model_catalogue.mjs +86 -0
  68. package/providers/orchestrator.mjs +30 -9
  69. package/providers/rates.mjs +12 -2
  70. package/providers/registry.mjs +10 -7
  71. package/sandbox/confiners/landlock.mjs +14 -8
  72. package/sandbox/confiners/seatbelt.mjs +18 -2
  73. package/scripts/loop-worker.mjs +18 -7
  74. package/scripts/migrate-v5.mjs +5 -61
  75. package/secure_write.mjs +46 -0
  76. package/sessions.mjs +0 -0
  77. package/tui/modal_filter.mjs +59 -0
  78. package/tui/modal_picker.mjs +12 -37
  79. package/tui/pickers.mjs +917 -0
  80. package/tui/provider_families.mjs +41 -0
  81. package/tui/repl.mjs +67 -36
  82. package/tui/slash_commands.mjs +2 -1
  83. package/tui/slash_dispatcher.mjs +717 -58
  84. package/tui/splash.mjs +5 -12
  85. package/tui/subcommands.mjs +17 -0
  86. package/tui/terminal_approve.mjs +37 -0
  87. package/web/dashboard.css +275 -0
  88. package/web/dashboard.html +2 -1685
  89. package/web/dashboard.js +1406 -0
  90. package/workflow/persistent.mjs +13 -6
  91. package/mas/tools/skill_view.mjs +0 -43
@@ -0,0 +1,218 @@
1
+ // Skill management commands (list/show/install/remove/search/curate/classify),
2
+ // extracted from cli.mjs in Phase D3. Self-contained over skills*.mjs modules.
3
+ import path from 'node:path';
4
+ import fs from 'node:fs';
5
+ import { configPath } from '../lib/config.mjs';
6
+
7
+ export async function cmdSkills(sub, positional, flags = {}) {
8
+ const skillsMod = await import('../skills.mjs');
9
+ const cfgDir = path.dirname(configPath());
10
+ switch (sub) {
11
+ case undefined:
12
+ case 'list': {
13
+ // Same --filter / --limit semantic as v3.33's sessions list:
14
+ // case-insensitive name substring, then post-filter cap.
15
+ let items = skillsMod.listSkills(cfgDir);
16
+ if (flags.filter) {
17
+ const f = String(flags.filter).toLowerCase();
18
+ items = items.filter(s => s.name.toLowerCase().includes(f));
19
+ }
20
+ if (flags.limit !== undefined) {
21
+ const n = parseInt(flags.limit, 10);
22
+ if (Number.isFinite(n) && n > 0) items = items.slice(0, n);
23
+ }
24
+ console.log(JSON.stringify(items.map(s => ({ name: s.name, bytes: s.bytes, summary: s.summary })), null, 2));
25
+ return;
26
+ }
27
+ case 'show': {
28
+ const name = positional[0];
29
+ if (!name) { console.error('Usage: lazyclaw skills show <name>'); process.exit(2); }
30
+ try { process.stdout.write(skillsMod.loadSkill(name, cfgDir)); }
31
+ catch (e) { console.error(e.message); process.exit(1); }
32
+ return;
33
+ }
34
+ case 'install': {
35
+ // Four forms:
36
+ // 1. install user/repo[@ref][:subpath] — GitHub bundle
37
+ // 2. install <name> --from <path>
38
+ // 3. install <name> --from-url <https://...>
39
+ // 4. install <name> — body via stdin
40
+ // Detect form 1 via a slash in the first positional and the
41
+ // absence of any --from* flag (so a literal local skill name
42
+ // with `/` still routes to the explicit-flag branch — though
43
+ // skillPath() rejects slashes anyway).
44
+ const name = positional[0];
45
+ if (!name) { console.error('Usage: lazyclaw skills install <user/repo[@ref][:path]> | <name> [--from <path> | --from-url <https://...>]'); process.exit(2); }
46
+ if (name.includes('/') && !flags.from && !flags['from-url']) {
47
+ const inst = await import('../skills_install.mjs');
48
+ try {
49
+ const r = await inst.installFromGithub(name, cfgDir, {
50
+ prefix: flags.prefix || '',
51
+ force: !!flags.force,
52
+ maxBytes: flags['max-bytes'] !== undefined ? parseInt(flags['max-bytes'], 10) : undefined,
53
+ timeoutMs: flags['timeout-ms'] !== undefined ? parseInt(flags['timeout-ms'], 10) : undefined,
54
+ });
55
+ console.log(JSON.stringify({
56
+ ok: true,
57
+ spec: `${r.spec.owner}/${r.spec.repo}@${r.spec.ref}${r.spec.subpath ? ':' + r.spec.subpath : ''}`,
58
+ installed: r.installed,
59
+ skipped: r.skipped,
60
+ }, null, 2));
61
+ return;
62
+ } catch (e) {
63
+ console.error(`error: ${e?.message || e}`);
64
+ process.exit(1);
65
+ }
66
+ }
67
+ let content;
68
+ if (flags['from-url']) {
69
+ const url = String(flags['from-url']);
70
+ // Refuse http/file/data — only https. The skill content goes
71
+ // straight into the system prompt, so source authenticity matters.
72
+ if (!url.startsWith('https://')) {
73
+ console.error('skills install --from-url requires an https:// URL');
74
+ process.exit(2);
75
+ }
76
+ const fetchFn = globalThis.fetch;
77
+ if (!fetchFn) { console.error('fetch is not available in this Node runtime'); process.exit(1); }
78
+ // Configurable max size — protect against pathological responses
79
+ // that would balloon the prompt and the disk file. 1 MiB cap.
80
+ const MAX_BYTES = 1_048_576;
81
+ try {
82
+ const res = await fetchFn(url, { redirect: 'follow' });
83
+ if (!res.ok) { console.error(`fetch ${url} → ${res.status}`); process.exit(1); }
84
+ // Stream the body so we can stop at the cap rather than loading
85
+ // an arbitrarily large response into memory.
86
+ const reader = res.body?.getReader?.();
87
+ if (!reader) { content = await res.text(); }
88
+ else {
89
+ const chunks = [];
90
+ let total = 0;
91
+ while (true) {
92
+ const { value, done } = await reader.read();
93
+ if (done) break;
94
+ total += value.length;
95
+ if (total > MAX_BYTES) {
96
+ console.error(`skills install: response exceeds ${MAX_BYTES} bytes; refusing`);
97
+ process.exit(1);
98
+ }
99
+ chunks.push(value);
100
+ }
101
+ content = new TextDecoder('utf-8', { fatal: false }).decode(Buffer.concat(chunks.map(c => Buffer.from(c))));
102
+ }
103
+ } catch (e) {
104
+ console.error(`skills install fetch failed: ${e?.message || e}`);
105
+ process.exit(1);
106
+ }
107
+ } else if (flags.from) {
108
+ content = fs.readFileSync(flags.from, 'utf8');
109
+ } else {
110
+ content = await new Promise(resolve => {
111
+ let buf = '';
112
+ process.stdin.setEncoding('utf8');
113
+ process.stdin.on('data', d => { buf += d; });
114
+ process.stdin.on('end', () => resolve(buf));
115
+ });
116
+ }
117
+ const written = skillsMod.installSkill(name, content, cfgDir);
118
+ console.log(JSON.stringify({ ok: true, name, path: written, bytes: content.length }));
119
+ return;
120
+ }
121
+ case 'remove': {
122
+ const name = positional[0];
123
+ if (!name) { console.error('Usage: lazyclaw skills remove <name>'); process.exit(2); }
124
+ skillsMod.removeSkill(name, cfgDir);
125
+ console.log(JSON.stringify({ ok: true, removed: name }));
126
+ return;
127
+ }
128
+ case 'search': {
129
+ // Mirror of `lazyclaw sessions search` — case-insensitive substring
130
+ // by default, --regex for pattern mode. Returns per-skill match
131
+ // count + first-excerpt window (40 chars before/after match).
132
+ // The skill body IS markdown so users typically search for terms
133
+ // mentioned in instructions or examples.
134
+ const query = positional[0];
135
+ if (!query) { console.error('Usage: lazyclaw skills search <query> [--regex]'); process.exit(2); }
136
+ const useRegex = !!flags.regex;
137
+ let matcher;
138
+ if (useRegex) {
139
+ try { matcher = new RegExp(query, 'i'); }
140
+ catch (e) { console.error(`invalid regex: ${e.message}`); process.exit(2); }
141
+ } else {
142
+ const q = query.toLowerCase();
143
+ matcher = { test: (s) => String(s).toLowerCase().includes(q) };
144
+ }
145
+ const items = skillsMod.listSkills(cfgDir);
146
+ const matches = [];
147
+ for (const s of items) {
148
+ let body;
149
+ try { body = skillsMod.loadSkill(s.name, cfgDir); }
150
+ catch { continue; } // file may have been removed mid-listing
151
+ // Count matches across the whole body, not per-line. For a
152
+ // skill body that's a few KB this is plenty fast and the count
153
+ // matches the user's intuition of "how many times does it
154
+ // mention X."
155
+ let matchCount = 0;
156
+ let firstExcerpt = null;
157
+ if (useRegex) {
158
+ // Re-anchor the regex with /gi so we can iterate; the original
159
+ // matcher was /i for boolean test() above. Rebuild here.
160
+ const gFlag = new RegExp(query, 'gi');
161
+ for (const m of body.matchAll(gFlag)) {
162
+ matchCount++;
163
+ if (firstExcerpt === null) {
164
+ const pos = m.index ?? 0;
165
+ const start = Math.max(0, pos - 40);
166
+ const end = Math.min(body.length, pos + m[0].length + 40);
167
+ firstExcerpt = (start > 0 ? '…' : '') + body.slice(start, end) + (end < body.length ? '…' : '');
168
+ }
169
+ }
170
+ } else {
171
+ const lower = body.toLowerCase();
172
+ const q = query.toLowerCase();
173
+ let pos = 0;
174
+ while (true) {
175
+ const i = lower.indexOf(q, pos);
176
+ if (i < 0) break;
177
+ matchCount++;
178
+ if (firstExcerpt === null) {
179
+ const start = Math.max(0, i - 40);
180
+ const end = Math.min(body.length, i + q.length + 40);
181
+ firstExcerpt = (start > 0 ? '…' : '') + body.slice(start, end) + (end < body.length ? '…' : '');
182
+ }
183
+ pos = i + q.length;
184
+ }
185
+ }
186
+ if (matchCount > 0) {
187
+ matches.push({
188
+ name: s.name,
189
+ bytes: s.bytes,
190
+ matchCount,
191
+ excerpt: firstExcerpt,
192
+ });
193
+ }
194
+ }
195
+ console.log(JSON.stringify({ query, regex: useRegex, matches }, null, 2));
196
+ return;
197
+ }
198
+ case 'curate': {
199
+ // Lifecycle sweep: agent-authored skills unused >90d move into
200
+ // skills/.archive/ (recoverable). Human-authored skills are never
201
+ // moved. The real clock is injected here; the module stays pure.
202
+ const curator = await import('../skills_curator.mjs');
203
+ const r = curator.curate(cfgDir, Date.now());
204
+ console.log(JSON.stringify(r, null, 2));
205
+ return;
206
+ }
207
+ case 'classify': {
208
+ const name = positional[0];
209
+ if (!name) { console.error('Usage: lazyclaw skills classify <name>'); process.exit(2); }
210
+ const curator = await import('../skills_curator.mjs');
211
+ console.log(JSON.stringify({ name, state: curator.classify(name, cfgDir, Date.now()), usage: curator.usageOf(name, cfgDir) }, null, 2));
212
+ return;
213
+ }
214
+ default:
215
+ console.error('Usage: lazyclaw skills <list|show <name>|install <name> [--from path]|remove <name>|search <query> [--regex]|curate|classify <name>>');
216
+ process.exit(2);
217
+ }
218
+ }