@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/README.md CHANGED
@@ -74,7 +74,7 @@ When you install, the installer will **auto-detect your configured model** and u
74
74
  # Interactive install
75
75
  npx @robbiesrobotics/alice-agents
76
76
 
77
- # Non-interactive with defaults (Sonnet, Starter tier)
77
+ # Non-interactive with defaults (detected model if available, otherwise Sonnet; Starter tier)
78
78
  npx @robbiesrobotics/alice-agents --yes
79
79
 
80
80
  # Show help
@@ -4,6 +4,7 @@ import { readFileSync } from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
5
  import { runInstall, runUninstall } from '../lib/installer.mjs';
6
6
  import { runDoctor } from '../lib/doctor.mjs';
7
+ import { runSkillsManager } from '../lib/skills.mjs';
7
8
 
8
9
  const args = process.argv.slice(2);
9
10
  const flags = new Set(args);
@@ -24,6 +25,7 @@ if (flags.has('--help') || flags.has('-h')) {
24
25
  npx @robbiesrobotics/alice-agents --update Non-interactive upgrade to latest agents
25
26
  npx @robbiesrobotics/alice-agents --uninstall Remove A.L.I.C.E. agents from config
26
27
  npx @robbiesrobotics/alice-agents --doctor Run diagnostics on your A.L.I.C.E. install
28
+ npx @robbiesrobotics/alice-agents --skills Manage skills (install, remove, browse)
27
29
  npx @robbiesrobotics/alice-agents --version Show version
28
30
  npx @robbiesrobotics/alice-agents --help Show this help
29
31
 
@@ -47,6 +49,11 @@ if (flags.has('--doctor')) {
47
49
  console.error(' ❌ Update failed:', err.message);
48
50
  process.exit(1);
49
51
  });
52
+ } else if (flags.has('--skills')) {
53
+ runSkillsManager().catch((err) => {
54
+ console.error(` ✗ Skills manager failed: ${err.message}`);
55
+ process.exit(1);
56
+ });
50
57
  } else if (flags.has('--uninstall')) {
51
58
  runUninstall({ yes: flags.has('--yes') }).catch((err) => {
52
59
  console.error(' ❌ Uninstall failed:', err.message);
package/lib/colors.mjs ADDED
@@ -0,0 +1,102 @@
1
+ // lib/colors.mjs
2
+ // ANSI color helper — no external deps, NO_COLOR + non-TTY safe
3
+ // Brand: A.L.I.C.E. / NemoClaw green (#76b900 → \x1b[92m bright green)
4
+
5
+ const enabled =
6
+ process.stdout.isTTY &&
7
+ !process.env.NO_COLOR &&
8
+ process.env.TERM !== 'dumb';
9
+
10
+ const esc = (code) => (str) => enabled ? `\x1b[${code}m${str}\x1b[0m` : str;
11
+
12
+ export const c = {
13
+ green: esc('92'),
14
+ greenDim: esc('32'),
15
+ success: esc('92'),
16
+ error: esc('91'),
17
+ warning: esc('93'),
18
+ info: esc('96'),
19
+ accent: esc('95'),
20
+ bold: esc('1'),
21
+ dim: esc('2'),
22
+ italic: esc('3'),
23
+ underline:esc('4'),
24
+ white: esc('97'),
25
+ gray: esc('90'),
26
+ };
27
+
28
+ export const bold = (s) => c.bold(s);
29
+ export const dim = (s) => c.dim(s);
30
+ export const green = (s) => c.green(s);
31
+ export const greenBold = (s) => enabled ? `\x1b[1m\x1b[92m${s}\x1b[0m` : s;
32
+ export const red = (s) => c.error(s);
33
+ export const yellow = (s) => c.warning(s);
34
+ export const cyan = (s) => c.info(s);
35
+ export const gray = (s) => c.gray(s);
36
+ export const white = (s) => c.white(s);
37
+
38
+ export const icons = {
39
+ ok: green('✔'),
40
+ fail: red('✗'),
41
+ warn: yellow('⚠'),
42
+ info: cyan('ℹ'),
43
+ pkg: '📦',
44
+ arrow: dim('›'),
45
+ bullet:dim('·'),
46
+ check: green('✓'),
47
+ cross: red('✗'),
48
+ };
49
+
50
+ const SEP_WIDTH = 50;
51
+ const SEP_CHAR = '─';
52
+
53
+ export function separator() {
54
+ return dim(SEP_CHAR.repeat(SEP_WIDTH));
55
+ }
56
+
57
+ export function printSection(title) {
58
+ const totalDashes = SEP_WIDTH - title.length - 4;
59
+ const left = SEP_CHAR.repeat(2);
60
+ const right = SEP_CHAR.repeat(Math.max(0, totalDashes));
61
+ console.log(`\n ${dim(left)} ${greenBold(title)} ${dim(right)}`);
62
+ }
63
+
64
+ export function printSeparator() {
65
+ console.log(` ${separator()}`);
66
+ }
67
+
68
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
69
+
70
+ export function printBox(lines, { title = null, padding = 1 } = {}) {
71
+ const innerWidth = Math.max(...lines.map(l => stripAnsi(l).length)) + padding * 2;
72
+ const w = innerWidth;
73
+ const TL = '╭', TR = '╮', BL = '╰', BR = '╯', H = '─', V = '│';
74
+
75
+ const topBar = title
76
+ ? `${TL}${H} ${greenBold(title)} ${H.repeat(Math.max(0, w - stripAnsi(title).length - 4))}${TR}`
77
+ : `${TL}${H.repeat(w + 2)}${TR}`;
78
+
79
+ console.log(` ${dim(topBar)}`);
80
+ lines.forEach(line => {
81
+ const pad = ' '.repeat(padding);
82
+ const raw = stripAnsi(line);
83
+ const trail = ' '.repeat(Math.max(0, w - raw.length - padding));
84
+ console.log(` ${dim(V)}${pad}${line}${trail}${dim(V)}`);
85
+ });
86
+ console.log(` ${dim(`${BL}${H.repeat(w + 2)}${BR}`)}`);
87
+ }
88
+
89
+ export function printStepDone(label, detail = '') {
90
+ const suffix = detail ? ` ${dim(detail)}` : '';
91
+ console.log(` ${icons.ok} ${label}${suffix}`);
92
+ }
93
+
94
+ export function printStepFail(label, reason = '') {
95
+ const suffix = reason ? ` ${red(reason)}` : '';
96
+ console.log(` ${icons.fail} ${label}${suffix}`);
97
+ }
98
+
99
+ export function printStepSkip(label, reason = '') {
100
+ const suffix = reason ? ` ${dim(reason)}` : '';
101
+ console.log(` ${dim('–')} ${dim(label)}${suffix}`);
102
+ }
@@ -6,6 +6,33 @@ import { randomBytes } from 'node:crypto';
6
6
  const OPENCLAW_DIR = join(homedir(), '.openclaw');
7
7
  const CONFIG_PATH = join(OPENCLAW_DIR, 'openclaw.json');
8
8
 
9
+ function normalizeProviderId(provider) {
10
+ if (!provider) return null;
11
+ if (provider === 'openai-codex') return 'openai';
12
+ return provider;
13
+ }
14
+
15
+ function getModelProvider(model) {
16
+ if (!model || typeof model !== 'string' || !model.includes('/')) return null;
17
+ return normalizeProviderId(model.split('/')[0]);
18
+ }
19
+
20
+ function collectConfiguredProviders(config) {
21
+ const providers = new Set();
22
+
23
+ for (const provider of Object.keys(config?.models?.providers || {})) {
24
+ const normalized = normalizeProviderId(provider);
25
+ if (normalized) providers.add(normalized);
26
+ }
27
+
28
+ for (const profile of Object.values(config?.auth?.profiles || {})) {
29
+ const normalized = normalizeProviderId(profile?.provider);
30
+ if (normalized) providers.add(normalized);
31
+ }
32
+
33
+ return [...providers];
34
+ }
35
+
9
36
  export function configExists() {
10
37
  return existsSync(CONFIG_PATH);
11
38
  }
@@ -34,7 +61,7 @@ export function detectAvailableModels() {
34
61
  const globalModel = config?.model;
35
62
 
36
63
  // Configured provider keys (e.g. ['anthropic', 'openai', 'ollama'])
37
- const providers = Object.keys(config?.models?.providers || {});
64
+ const providers = collectConfiguredProviders(config);
38
65
 
39
66
  const primary = agentDefaults.primary || globalModel || null;
40
67
  const orchestrator = agentDefaults.orchestrator || primary;
@@ -127,6 +154,22 @@ function getModelConfig(preset, customModels, detectedModels) {
127
154
  }
128
155
  }
129
156
 
157
+ function isCompatibleWithConfiguredProviders(modelCfg, detectedModels) {
158
+ const providers = new Set(detectedModels?.providers || []);
159
+ if (providers.size === 0) return true;
160
+
161
+ const models = [
162
+ modelCfg?.primary,
163
+ modelCfg?.orchestrator,
164
+ ...(modelCfg?.fallbacks || []),
165
+ ].filter(Boolean);
166
+
167
+ return models.every((model) => {
168
+ const provider = getModelProvider(model);
169
+ return !provider || providers.has(provider);
170
+ });
171
+ }
172
+
130
173
  function buildAgentEntry(agent, modelCfg) {
131
174
  const entry = {
132
175
  id: agent.id,
@@ -157,7 +200,17 @@ export function mergeConfig({ agents, mode, preset, customModels }) {
157
200
  const backupPath = backupConfig();
158
201
  const config = readConfig();
159
202
  const detectedModels = detectAvailableModels();
160
- const modelCfg = getModelConfig(preset, customModels, detectedModels);
203
+ let effectivePreset = preset;
204
+ let warning = null;
205
+ let modelCfg = getModelConfig(preset, customModels, detectedModels);
206
+
207
+ if (!modelCfg.preserve && !isCompatibleWithConfiguredProviders(modelCfg, detectedModels)) {
208
+ effectivePreset = detectedModels?.hasModel ? 'detected' : preset;
209
+ if (effectivePreset === 'detected') {
210
+ warning = `Preset "${preset}" does not match configured OpenClaw providers (${(detectedModels.providers || []).join(', ')}). Preserving existing model settings instead.`;
211
+ modelCfg = getModelConfig('detected', customModels, detectedModels);
212
+ }
213
+ }
161
214
 
162
215
  // Build agent entries
163
216
  const aliceEntries = agents.map((a) => buildAgentEntry(a, modelCfg));
@@ -221,7 +274,7 @@ export function mergeConfig({ agents, mode, preset, customModels }) {
221
274
  config.tools.agentToAgent.allow = [...aliceIds];
222
275
 
223
276
  writeConfigAtomic(config);
224
- return { backupPath, agentCount: aliceEntries.length };
277
+ return { backupPath, agentCount: aliceEntries.length, effectivePreset, warning };
225
278
  }
226
279
 
227
280
  export function removeAliceAgents(agentIds) {
package/lib/doctor.mjs CHANGED
@@ -3,9 +3,21 @@ import { join, dirname } from 'node:path';
3
3
  import { homedir, platform } from 'node:os';
4
4
  import { execSync } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import { icons, greenBold, green, red, yellow, cyan, dim, bold,
7
+ printSection, printSeparator, separator } from './colors.mjs';
6
8
 
7
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
10
  const HOME = homedir();
11
+
12
+ function commandExists(cmd) {
13
+ const probe = process.platform === 'win32' ? 'where' : 'which';
14
+ try {
15
+ execSync(`${probe} ${cmd}`, { stdio: 'pipe' });
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
9
21
  const OPENCLAW_DIR = join(HOME, '.openclaw');
10
22
 
11
23
  const STARTER_AGENTS = [
@@ -14,9 +26,9 @@ const STARTER_AGENTS = [
14
26
  ];
15
27
 
16
28
  function check(label, ok, hint) {
17
- const icon = ok ? '✓' : '✗';
18
- console.log(` ${icon} ${label}`);
19
- if (!ok && hint) console.log(` → ${hint}`);
29
+ const icon = ok ? icons.ok : icons.fail;
30
+ console.log(` ${icon} ${ok ? green(label) : red(label)}`);
31
+ if (!ok && hint) console.log(` ${dim('')} ${dim(hint)}`);
20
32
  return ok;
21
33
  }
22
34
 
@@ -51,12 +63,7 @@ function checkDockerEnvironment() {
51
63
  const isLinux = platform() === 'linux';
52
64
 
53
65
  // Check if docker binary exists at all
54
- let dockerInstalled = false;
55
- try {
56
- execSync('which docker', { stdio: 'pipe' });
57
- dockerInstalled = true;
58
- } catch {
59
- // Docker not installed — not a blocker unless openclaw needs it
66
+ if (!commandExists('docker')) {
60
67
  return { present: false, accessible: false, needsSudo: false, hint: null };
61
68
  }
62
69
 
@@ -109,7 +116,10 @@ function checkDockerEnvironment() {
109
116
  }
110
117
 
111
118
  export async function runDoctor() {
112
- console.log('\n 🩺 A.L.I.C.E. Doctor — Diagnostic Report\n');
119
+ console.log('');
120
+ printSection('A.L.I.C.E. Doctor');
121
+ console.log(` ${dim('Diagnostic Report')}`);
122
+ console.log('');
113
123
  let allOk = true;
114
124
 
115
125
  // 1. Runtime check
@@ -130,7 +140,7 @@ export async function runDoctor() {
130
140
 
131
141
  if (!configExists) {
132
142
  allOk = false;
133
- console.log('\n ⚠️ Cannot continue checks — openclaw.json missing.\n');
143
+ console.log(`\n ${icons.warn} ${yellow('Cannot continue checks — openclaw.json missing.')}\n`);
134
144
  return false;
135
145
  }
136
146
 
@@ -143,7 +153,7 @@ export async function runDoctor() {
143
153
  );
144
154
  if (!configValid) {
145
155
  allOk = false;
146
- console.log('\n ⚠️ Cannot continue checks — config is invalid JSON.\n');
156
+ console.log(`\n ${icons.warn} ${yellow('Cannot continue checks — config is invalid JSON.')}\n`);
147
157
  return false;
148
158
  }
149
159
 
@@ -236,11 +246,16 @@ export async function runDoctor() {
236
246
  );
237
247
  // Docker permission issue is a warning, not a hard failure for A.L.I.C.E. itself
238
248
  // but it will break OpenClaw's own Docker features
239
- console.log(' ℹ️ Note: This will affect OpenClaw features that use Docker.\n');
249
+ console.log(` ${icons.info} ${dim('This will affect OpenClaw features that use Docker.')}\n`);
240
250
  } else {
241
251
  check('Docker daemon not running or not accessible', false, docker.hint);
242
252
  }
243
253
  }
254
+
255
+ const isWSL = !!process.env.WSL_DISTRO_NAME || !!process.env.WSLENV;
256
+ if (isWSL && docker.present) {
257
+ console.log(" ℹ️ WSL2 detected. If Docker has cgroup issues, run: nemoclaw setup-spark\n");
258
+ }
244
259
  // If docker not present at all, skip silently — not required for all setups
245
260
 
246
261
  // 7. License check
@@ -276,13 +291,43 @@ export async function runDoctor() {
276
291
  check('License: Starter tier (no license required)', true);
277
292
  }
278
293
 
294
+ // 8. Skills disk check
295
+ const skillsManifestPath = join(OPENCLAW_DIR, '.alice-manifest.json');
296
+ const skillsManifestData = (() => {
297
+ try {
298
+ if (!existsSync(skillsManifestPath)) return null;
299
+ return JSON.parse(readFileSync(skillsManifestPath, 'utf8'));
300
+ } catch { return null; }
301
+ })();
302
+
303
+ const installedSkills = skillsManifestData?.skills || [];
304
+ if (installedSkills.length > 0) {
305
+ const missingSkills = installedSkills.filter(
306
+ id => !existsSync(join(HOME, '.openclaw', 'skills', id))
307
+ );
308
+ if (missingSkills.length > 0) {
309
+ check(
310
+ `Skills missing on disk: ${missingSkills.join(', ')}`,
311
+ false,
312
+ 'Run: npx @robbiesrobotics/alice-agents --skills to reinstall'
313
+ );
314
+ allOk = false;
315
+ } else {
316
+ check(`Skills installed (${installedSkills.length}): ${installedSkills.join(', ')}`, true);
317
+ }
318
+ } else {
319
+ console.log(` ${dim('–')} ${dim('No skills installed (optional)')}`);
320
+ }
321
+
279
322
  // Summary
280
- console.log();
323
+ console.log('');
281
324
  if (allOk) {
282
- console.log(' A.L.I.C.E. is healthy!\n');
325
+ console.log(` ${icons.ok} ${greenBold('A.L.I.C.E. is healthy!')}`);
283
326
  } else {
284
- console.log(' ⚠️ Issues found follow the hints above to fix them.\n');
327
+ console.log(` ${icons.warn} ${yellow('Issues found')} ${dim('─ follow the hints above to fix them.')}`);
285
328
  }
329
+ printSeparator();
330
+ console.log('');
286
331
 
287
332
  return allOk;
288
333
  }