@robbiesrobotics/alice-agents 1.3.3 → 1.4.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/lib/skills.mjs ADDED
@@ -0,0 +1,310 @@
1
+ // lib/skills.mjs
2
+ // A.L.I.C.E. Skills Manager — install, list, remove skills via clawhub
3
+
4
+ import { execSync } from 'node:child_process';
5
+ import { existsSync, writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { homedir } from 'node:os';
8
+ import { readManifest, writeManifest } from './manifest.mjs';
9
+ import { icons, greenBold, green, red, yellow, cyan, dim, bold,
10
+ printSection, printSeparator, printBox, printStepDone,
11
+ printStepFail, printStepSkip, separator } from './colors.mjs';
12
+ import { confirm, choose, closePrompt } from './prompter.mjs';
13
+
14
+ const OPENCLAW_DIR = join(homedir(), '.openclaw');
15
+ const SKILLS_DIR = join(OPENCLAW_DIR, 'skills');
16
+
17
+ export const SKILL_CATALOG = [
18
+ {
19
+ category: 'Productivity',
20
+ icon: '📅',
21
+ skills: [
22
+ { id: 'apple-reminders', label: 'Apple Reminders', desc: 'Manage reminders from the terminal' },
23
+ { id: 'apple-notes', label: 'Apple Notes', desc: 'Create and search notes' },
24
+ { id: 'things-mac', label: 'Things 3', desc: 'Task management via Things 3 on macOS' },
25
+ { id: 'gog', label: 'Google Workspace', desc: 'Gmail, Calendar, Drive, Docs, Sheets' },
26
+ { id: 'obsidian', label: 'Obsidian', desc: 'Work with Obsidian vaults' },
27
+ ],
28
+ },
29
+ {
30
+ category: 'Web & Research',
31
+ icon: '🌐',
32
+ skills: [
33
+ { id: 'blogwatcher', label: 'Blog Watcher', desc: 'Monitor RSS/Atom feeds for updates' },
34
+ { id: 'weather', label: 'Weather', desc: 'Current weather and forecasts (no API key)' },
35
+ { id: 'summarize', label: 'Summarize', desc: 'Summarize URLs, podcasts, YouTube videos' },
36
+ { id: 'gifgrep', label: 'GIF Search', desc: 'Search and download GIFs' },
37
+ ],
38
+ },
39
+ {
40
+ category: 'Communication',
41
+ icon: '💬',
42
+ skills: [
43
+ { id: 'imsg', label: 'iMessage', desc: 'Send and read iMessages/SMS' },
44
+ { id: 'wacli', label: 'WhatsApp', desc: 'WhatsApp messaging via CLI' },
45
+ { id: 'himalaya', label: 'Email', desc: 'IMAP/SMTP email via himalaya' },
46
+ ],
47
+ },
48
+ {
49
+ category: 'Developer Tools',
50
+ icon: '⚙️',
51
+ skills: [
52
+ { id: 'github', label: 'GitHub', desc: 'Issues, PRs, CI via gh CLI' },
53
+ { id: 'gh-issues', label: 'GitHub Issues Bot', desc: 'Auto-fix issues and open PRs' },
54
+ { id: 'coding-agent', label: 'Coding Agent', desc: 'Delegate tasks to Codex / Claude Code' },
55
+ { id: '1password', label: '1Password', desc: 'Secrets and credentials via op CLI' },
56
+ ],
57
+ },
58
+ {
59
+ category: 'Smart Home & IoT',
60
+ icon: '🏠',
61
+ skills: [
62
+ { id: 'openhue', label: 'Philips Hue', desc: 'Control Hue lights and scenes' },
63
+ { id: 'sonoscli', label: 'Sonos', desc: 'Control Sonos speakers' },
64
+ { id: 'blucli', label: 'BluOS', desc: 'BluOS audio system control' },
65
+ ],
66
+ },
67
+ ];
68
+
69
+ // Egress endpoints needed per skill beyond NemoClaw baseline
70
+ const SKILL_POLICY_ENDPOINTS = {
71
+ 'weather': ['wttr.in:443', 'api.open-meteo.com:443'],
72
+ 'gog': ['oauth2.googleapis.com:443', 'www.googleapis.com:443', 'accounts.google.com:443'],
73
+ 'gifgrep': ['api.giphy.com:443', 'api.tenor.com:443'],
74
+ 'blogwatcher': ['*:443'],
75
+ 'summarize': ['*:443'],
76
+ // github, gh-issues, apple-*, things-mac, obsidian, imsg, wacli, himalaya, coding-agent, 1password,
77
+ // openhue, sonoscli, blucli — no extra endpoints needed or use dynamic approval
78
+ };
79
+
80
+ function commandExists(cmd) {
81
+ const probe = process.platform === 'win32' ? 'where' : 'which';
82
+ try { execSync(`${probe} ${cmd}`, { stdio: 'pipe' }); return true; }
83
+ catch { return false; }
84
+ }
85
+
86
+ function isClawhubAvailable() {
87
+ return commandExists('clawhub');
88
+ }
89
+
90
+ function isSkillInstalled(skillId) {
91
+ return existsSync(join(SKILLS_DIR, skillId));
92
+ }
93
+
94
+ export function getInstalledSkills() {
95
+ const manifest = readManifest();
96
+ return manifest?.skills || [];
97
+ }
98
+
99
+ async function detectNemoclawSandboxName() {
100
+ const manifest = readManifest();
101
+ if (manifest?.nemoclawSandbox) return manifest.nemoclawSandbox;
102
+ try {
103
+ const out = execSync('nemoclaw list', { stdio: 'pipe', encoding: 'utf8' });
104
+ const match = out.match(/^(\S+)\s/m);
105
+ if (match) return match[1];
106
+ } catch {}
107
+ return 'my-assistant';
108
+ }
109
+
110
+ async function applyNemoclawPolicyForSkill(skillId, sandboxName) {
111
+ const endpoints = SKILL_POLICY_ENDPOINTS[skillId];
112
+ if (!endpoints || endpoints.length === 0) return;
113
+
114
+ if (endpoints.includes('*:443')) {
115
+ console.log(` ${icons.info} ${cyan(skillId)} ${dim('requires dynamic egress — approve via')} ${cyan('openshell term')}`);
116
+ return;
117
+ }
118
+
119
+ const policyContent = [
120
+ 'network:',
121
+ ` - name: alice_${skillId}`,
122
+ ' endpoints:',
123
+ ...endpoints.map(e => ` - ${e}`),
124
+ ' binaries:',
125
+ ' - /usr/local/bin/openclaw',
126
+ ' rules:',
127
+ ' - methods: [GET, POST]',
128
+ '',
129
+ ].join('\n');
130
+
131
+ const tmpPath = join(homedir(), `.alice-policy-${skillId}.yaml`);
132
+ writeFileSync(tmpPath, policyContent, 'utf8');
133
+
134
+ try {
135
+ execSync(`openshell policy set ${tmpPath}`, { stdio: 'pipe' });
136
+ console.log(` ${icons.ok} ${dim('Policy updated for')} ${green(skillId)} ${dim('─')} ${endpoints.join(', ')}`);
137
+ } catch {
138
+ console.log(` ${icons.warn} ${yellow('Could not auto-apply policy for')} ${skillId}`);
139
+ console.log(` ${dim('Run manually:')} openshell policy set ${tmpPath}`);
140
+ }
141
+ }
142
+
143
+ export async function installSkill(skillId, { nemoclaw = false, sandboxName = 'my-assistant' } = {}) {
144
+ if (isSkillInstalled(skillId)) {
145
+ printStepSkip(skillId, 'already installed');
146
+ return { status: 'skipped' };
147
+ }
148
+
149
+ if (!isClawhubAvailable()) {
150
+ printStepFail(skillId, 'clawhub not found — install: npm install -g clawhub');
151
+ return { status: 'error', reason: 'clawhub not available' };
152
+ }
153
+
154
+ try {
155
+ execSync(`clawhub install ${skillId}`, { stdio: 'pipe' });
156
+ printStepDone(skillId);
157
+ if (nemoclaw) {
158
+ await applyNemoclawPolicyForSkill(skillId, sandboxName);
159
+ }
160
+ return { status: 'ok' };
161
+ } catch (err) {
162
+ printStepFail(skillId, err.message?.slice(0, 60));
163
+ return { status: 'error', reason: err.message };
164
+ }
165
+ }
166
+
167
+ export async function runSkillsWizardStep({ auto = false, nemoclaw = false, sandboxName = 'my-assistant' } = {}) {
168
+ printSection('Skills & Tools');
169
+ console.log('');
170
+ console.log(` ${dim('Skills extend your agents with real-world capabilities.')}`);
171
+ console.log(` ${dim('Installed via')} ${green('clawhub')} ${dim('— the OpenClaw skill marketplace.')}`);
172
+ console.log('');
173
+
174
+ if (!isClawhubAvailable()) {
175
+ console.log(` ${icons.warn} ${yellow('clawhub not found.')} ${dim('Skills cannot be installed right now.')}`);
176
+ console.log(` ${dim('Install it with:')} ${cyan('npm install -g clawhub')}`);
177
+ console.log(` ${dim('Then run:')} ${cyan('npx @robbiesrobotics/alice-agents --skills')}`);
178
+ console.log('');
179
+ return [];
180
+ }
181
+
182
+ if (auto) {
183
+ console.log(` ${icons.info} ${dim('Skipping skills in non-interactive mode.')}`);
184
+ console.log(` ${dim('Run')} ${cyan('npx @robbiesrobotics/alice-agents --skills')} ${dim('to install later.')}`);
185
+ console.log('');
186
+ return [];
187
+ }
188
+
189
+ const shouldInstall = await confirm(' Install recommended skills now?');
190
+ if (!shouldInstall) {
191
+ printStepSkip('Skills', 'skipped — run --skills to install later');
192
+ return [];
193
+ }
194
+
195
+ console.log('');
196
+ const selected = [];
197
+
198
+ for (const group of SKILL_CATALOG) {
199
+ printSection(`${group.icon} ${group.category}`);
200
+ console.log('');
201
+
202
+ for (const skill of group.skills) {
203
+ const already = isSkillInstalled(skill.id);
204
+ if (already) {
205
+ console.log(` ${icons.check} ${green(skill.label)} ${dim('─ already installed')}`);
206
+ continue;
207
+ }
208
+ const yes = await confirm(` Install ${green(skill.label)}? ${dim('─')} ${dim(skill.desc)}`);
209
+ if (yes) selected.push(skill.id);
210
+ }
211
+ console.log('');
212
+ }
213
+
214
+ if (selected.length === 0) {
215
+ console.log(` ${icons.info} ${dim('No skills selected.')}`);
216
+ console.log('');
217
+ return [];
218
+ }
219
+
220
+ printSection('Installing Skills');
221
+ console.log('');
222
+
223
+ if (nemoclaw) {
224
+ console.log(` ${icons.info} ${cyan('NemoClaw detected')} ${dim('─ policy endpoints will be applied automatically.')}`);
225
+ console.log('');
226
+ }
227
+
228
+ const results = [];
229
+ for (const skillId of selected) {
230
+ const result = await installSkill(skillId, { nemoclaw, sandboxName });
231
+ results.push({ skillId, ...result });
232
+ }
233
+
234
+ const ok = results.filter(r => r.status === 'ok').length;
235
+ const err = results.filter(r => r.status === 'error').length;
236
+
237
+ console.log('');
238
+ console.log(` ${separator()}`);
239
+ console.log(` ${icons.ok} ${green(String(ok))} ${dim('skills installed')}${err ? ` ${icons.warn} ${yellow(String(err) + ' failed')}` : ''}`);
240
+ console.log('');
241
+
242
+ const manifest = readManifest();
243
+ if (manifest) {
244
+ const already = manifest.skills || [];
245
+ const newOnes = results.filter(r => r.status === 'ok').map(r => r.skillId);
246
+ writeManifest({ ...manifest, skills: [...new Set([...already, ...newOnes])] });
247
+ }
248
+
249
+ return selected;
250
+ }
251
+
252
+ export async function runSkillsManager() {
253
+ console.log('');
254
+ printSection('A.L.I.C.E. Skills Manager');
255
+ console.log('');
256
+
257
+ const installed = getInstalledSkills();
258
+
259
+ if (installed.length > 0) {
260
+ console.log(` ${dim('Installed skills:')}`);
261
+ installed.forEach(id => {
262
+ const onDisk = isSkillInstalled(id);
263
+ console.log(` ${onDisk ? icons.ok : icons.warn} ${onDisk ? green(id) : yellow(id + ' (missing on disk)')}`);
264
+ });
265
+ console.log('');
266
+ } else {
267
+ console.log(` ${icons.info} ${dim('No skills installed yet.')}`);
268
+ console.log('');
269
+ }
270
+
271
+ const action = await choose(' What would you like to do?', [
272
+ { label: 'Browse & install skills', value: 'install' },
273
+ { label: 'Remove a skill', value: 'remove' },
274
+ { label: 'Exit', value: 'exit' },
275
+ ]);
276
+
277
+ if (action === 'exit') { closePrompt(); return; }
278
+
279
+ if (action === 'install') {
280
+ const nemoclaw = commandExists('nemoclaw');
281
+ const sandboxName = nemoclaw ? await detectNemoclawSandboxName() : null;
282
+ await runSkillsWizardStep({ auto: false, nemoclaw, sandboxName });
283
+ }
284
+
285
+ if (action === 'remove') {
286
+ if (installed.length === 0) {
287
+ console.log(` ${icons.info} ${dim('Nothing to remove.')}`);
288
+ closePrompt();
289
+ return;
290
+ }
291
+ const toRemove = await choose(' Which skill to remove?', [
292
+ ...installed.map(id => ({ label: id, value: id })),
293
+ { label: 'Cancel', value: null },
294
+ ]);
295
+ if (toRemove) {
296
+ try {
297
+ execSync(`clawhub uninstall ${toRemove}`, { stdio: 'inherit' });
298
+ printStepDone(toRemove, 'removed');
299
+ const manifest = readManifest();
300
+ if (manifest) {
301
+ writeManifest({ ...manifest, skills: (manifest.skills || []).filter(s => s !== toRemove) });
302
+ }
303
+ } catch {
304
+ printStepFail(toRemove, 'remove failed');
305
+ }
306
+ }
307
+ }
308
+
309
+ closePrompt();
310
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@robbiesrobotics/alice-agents",
3
- "version": "1.3.3",
4
- "description": "A.L.I.C.E. \u2014 28 AI agents for OpenClaw. One conversation, one team.",
3
+ "version": "1.4.1",
4
+ "description": "A.L.I.C.E. 28 AI agents for OpenClaw. One conversation, one team.",
5
5
  "bin": {
6
6
  "alice-agents": "bin/alice-install.mjs"
7
7
  },
@@ -35,4 +35,4 @@
35
35
  "publishConfig": {
36
36
  "access": "public"
37
37
  }
38
- }
38
+ }
@@ -89,6 +89,33 @@ function diffConfigSchema(snapshot, liveConfig) {
89
89
  });
90
90
  }
91
91
 
92
+ const configuredProviders = new Set([
93
+ ...Object.keys(liveConfig?.models?.providers || {}),
94
+ ...Object.values(liveConfig?.auth?.profiles || {}).map((profile) => profile?.provider),
95
+ ].filter(Boolean).map((provider) => provider === 'openai-codex' ? 'openai' : provider));
96
+
97
+ const referencedModels = [
98
+ defaults?.model?.primary,
99
+ defaults?.model?.orchestrator,
100
+ ...(defaults?.model?.fallbacks || []),
101
+ ...agents.map((agent) => agent?.model).filter(Boolean),
102
+ ].filter(Boolean);
103
+
104
+ for (const model of referencedModels) {
105
+ const provider = typeof model === 'string' && model.includes('/') ? model.split('/')[0] : null;
106
+ const normalized = provider === 'openai-codex' ? 'openai' : provider;
107
+ if (normalized && configuredProviders.size > 0 && !configuredProviders.has(normalized)) {
108
+ changes.push({
109
+ category: 'config',
110
+ severity: 'high',
111
+ field: 'agents.defaults.model',
112
+ change: `Model '${model}' references provider '${normalized}', but that provider is not configured in OpenClaw auth/models`,
113
+ autoFixable: false
114
+ });
115
+ break;
116
+ }
117
+ }
118
+
92
119
  return changes;
93
120
  }
94
121