@robbiesrobotics/alice-agents 1.5.9 → 1.5.10
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 +152 -132
- package/bin/alice-install.mjs +27 -35
- package/lib/hermes-agent.mjs +449 -0
- package/lib/hermes-installer.mjs +338 -0
- package/lib/installer.mjs +254 -19
- package/lib/skills.mjs +128 -4
- package/package.json +3 -3
- package/templates/workspaces/_shared/AGENTS-hermes.md +54 -0
- package/templates/workspaces/_shared/AGENTS.md +25 -0
- package/templates/workspaces/_shared/SOUL-hermes.md +35 -0
- package/templates/workspaces/_shared/hermes-agent-skill.md +40 -0
- package/templates/workspaces/_shared/hermes-orchestrator-skill.md +150 -0
- package/templates/workspaces/_shared/hermes-specialist-skill.md +109 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// lib/hermes-installer.mjs
|
|
2
|
+
// Installs and configures A.L.I.C.E. for Hermes-only runtime
|
|
3
|
+
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, rmSync } from 'node:fs';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const HERMES_DIR = join(homedir(), '.hermes');
|
|
12
|
+
const ALICE_SKILLS_DIR = join(HERMES_DIR, 'skills', 'alice');
|
|
13
|
+
const ALICE_MEMORIES_DIR = join(HERMES_DIR, 'memories', 'alice');
|
|
14
|
+
const ALICE_CONFIG_JSON = join(HERMES_DIR, '.alice-config.json');
|
|
15
|
+
const TEMPLATES_DIR = join(__dirname, '..', 'templates', 'workspaces', '_shared');
|
|
16
|
+
|
|
17
|
+
// ── Runtime detection ─────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export function isHermesInstalled() {
|
|
20
|
+
if (!existsSync(join(homedir(), '.hermes'))) return false;
|
|
21
|
+
if (!existsSync(join(homedir(), '.hermes', 'config.yaml'))) return false;
|
|
22
|
+
try {
|
|
23
|
+
execSync('hermes version', { stdio: 'pipe' });
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getHermesVersion() {
|
|
31
|
+
try {
|
|
32
|
+
// Suppress stderr to avoid "Failed to load config" warnings
|
|
33
|
+
return execSync('hermes version 2>/dev/null', { encoding: 'utf8', stdio: 'pipe' })
|
|
34
|
+
.trim()
|
|
35
|
+
.split('\n')[0]; // only first line
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read the default model from ~/.hermes/config.yaml
|
|
43
|
+
* Returns null if no model is configured.
|
|
44
|
+
*/
|
|
45
|
+
export function getHermesDefaultModel() {
|
|
46
|
+
try {
|
|
47
|
+
const configPath = join(homedir(), '.hermes', 'config.yaml');
|
|
48
|
+
const content = readFileSync(configPath, 'utf8');
|
|
49
|
+
const lines = content.split('\n');
|
|
50
|
+
let inModel = false;
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const trimmed = line.trim();
|
|
53
|
+
if (trimmed === 'model:') { inModel = true; continue; }
|
|
54
|
+
if (inModel) {
|
|
55
|
+
// 'default: <modelname>' at the same or deeper indent
|
|
56
|
+
const match = trimmed.match(/^default:\s*(.+)$/);
|
|
57
|
+
if (match) return match[1].trim();
|
|
58
|
+
// If we hit a new top-level key, stop
|
|
59
|
+
if (trimmed.match(/^[a-z]/) && !trimmed.startsWith('default:')) break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Hermes config helpers ─────────────────────────────────────────────────────
|
|
69
|
+
// Uses `hermes config set` for safe updates — Hermes rewrites the file correctly.
|
|
70
|
+
|
|
71
|
+
const ALICE_PERSONALITY = `You are A.L.I.C.E. — the Chief Orchestration Officer of the A.L.I.C.E. multi-agent AI team. Your role is to receive user requests, route them to the right specialist agents (dylan for development, selena for security, devon for DevOps, etc.), wait for their results, and synthesize one clear response. You delegate ALL specialist work — you do not do it yourself. You are sharp, confident, and efficient. Users may call you Alice, A.L.I.C.E., or Olivia.`;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Configure Hermes with alice personality and display settings.
|
|
75
|
+
* Uses `hermes config set` which safely rewrites the YAML.
|
|
76
|
+
*/
|
|
77
|
+
function configureHermesAlice() {
|
|
78
|
+
// Set display personality to alice
|
|
79
|
+
execSync(`hermes config set display.personality alice`, {
|
|
80
|
+
stdio: 'pipe',
|
|
81
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Set alice personality
|
|
85
|
+
execSync(`hermes config set agent.personalities.alice "${ALICE_PERSONALITY.replace(/"/g, '\\"')}"`, {
|
|
86
|
+
stdio: 'pipe',
|
|
87
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Alice skill file generation ───────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
function generateOrchestratorSkill() {
|
|
94
|
+
const templatePath = join(TEMPLATES_DIR, 'hermes-orchestrator-skill.md');
|
|
95
|
+
if (!existsSync(templatePath)) {
|
|
96
|
+
// Fallback inline if template not found
|
|
97
|
+
return readFileSync(templatePath, 'utf8');
|
|
98
|
+
}
|
|
99
|
+
return readFileSync(templatePath, 'utf8');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function generateSpecialistSkill(agentId, agentName, domain, domainDescription) {
|
|
103
|
+
let templatePath = join(TEMPLATES_DIR, 'hermes-specialist-skill.md');
|
|
104
|
+
if (!existsSync(templatePath)) {
|
|
105
|
+
// Inline fallback template
|
|
106
|
+
return `---
|
|
107
|
+
name: ${agentId}
|
|
108
|
+
description: ${domain} specialist on the A.L.I.C.E. team.
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
# ${agentName} — ${domain} Specialist
|
|
112
|
+
|
|
113
|
+
**${agentName}** is a ${domain} specialist on the A.L.I.C.E. team.
|
|
114
|
+
A.L.I.C.E. (orchestrator) assigns you tasks. Complete them and report back.
|
|
115
|
+
|
|
116
|
+
## Domain
|
|
117
|
+
${domainDescription}
|
|
118
|
+
|
|
119
|
+
## How You Work
|
|
120
|
+
1. Receive task from A.L.I.C.E.
|
|
121
|
+
2. Use your tools to complete the work
|
|
122
|
+
3. Return structured results to A.L.I.C.E.
|
|
123
|
+
|
|
124
|
+
## Response Format
|
|
125
|
+
**Summary:** one-line answer
|
|
126
|
+
**Details:** what you did/found
|
|
127
|
+
**Next Steps:** recommended actions
|
|
128
|
+
**Blockers:** anything blocking progress (or "None")
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let content = readFileSync(templatePath, 'utf8');
|
|
133
|
+
content = content
|
|
134
|
+
.replace(/\{\{agentId\}\}/g, agentId)
|
|
135
|
+
.replace(/\{\{agentName\}\}/g, agentName)
|
|
136
|
+
.replace(/\{\{domain\}\}/g, domain)
|
|
137
|
+
.replace(/\{\{domainDescription\}\}/g, domainDescription || `${domain} specialist work.`);
|
|
138
|
+
return content;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Alice agent registry ─────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
const ALICE_AGENTS = [
|
|
144
|
+
// Starter agents
|
|
145
|
+
{ id: 'alice', name: 'A.L.I.C.E.', domain: 'Orchestration', isOrchestrator: true, tier: 'core' },
|
|
146
|
+
{ id: 'dylan', name: 'Dylan', domain: 'Development', description: 'Full-stack code, APIs, debugging, architecture, database design', tier: 'starter' },
|
|
147
|
+
{ id: 'selena', name: 'Selena', domain: 'Security', description: 'Security audits, hardening, access controls, incident response, vulnerability assessment', tier: 'starter' },
|
|
148
|
+
{ id: 'devon', name: 'Devon', domain: 'DevOps', description: 'CI/CD pipelines, infrastructure, deployment, Docker, Kubernetes, monitoring', tier: 'starter' },
|
|
149
|
+
{ id: 'quinn', name: 'Quinn', domain: 'QA', description: 'Test design, automation, bug verification, quality assurance, test coverage', tier: 'starter' },
|
|
150
|
+
{ id: 'felix', name: 'Felix', domain: 'Frontend', description: 'UI implementation, React, responsive design, component libraries, CSS', tier: 'starter' },
|
|
151
|
+
{ id: 'daphne', name: 'Daphne', domain: 'Documentation', description: 'API docs, guides, runbooks, READMEs, technical writing', tier: 'starter' },
|
|
152
|
+
{ id: 'rowan', name: 'Rowan', domain: 'Research', description: 'Web research, competitive analysis, fact-finding, technology landscape', tier: 'starter' },
|
|
153
|
+
{ id: 'darius', name: 'Darius', domain: 'Data', description: 'Data pipelines, SQL, analytics, warehousing, ETL processes', tier: 'starter' },
|
|
154
|
+
{ id: 'sophie', name: 'Sophie', domain: 'Support', description: 'Customer support, triage, drafting responses, FAQ creation', tier: 'starter' },
|
|
155
|
+
// Pro agents
|
|
156
|
+
{ id: 'hannah', name: 'Hannah', domain: 'HR', description: 'Onboarding, HR policy, organizational structure, people operations', tier: 'pro' },
|
|
157
|
+
{ id: 'aiden', name: 'Aiden', domain: 'Analytics', description: 'Dashboards, KPI tracking, business intelligence, analytics models', tier: 'pro' },
|
|
158
|
+
{ id: 'clara', name: 'Clara', domain: 'Communications', description: 'Writing, email sequences, press releases, messaging frameworks', tier: 'pro' },
|
|
159
|
+
{ id: 'avery', name: 'Avery', domain: 'Automation', description: 'Workflow automation, no-code automation, process engineering', tier: 'pro' },
|
|
160
|
+
{ id: 'owen', name: 'Owen', domain: 'Operations', description: 'Vendor management, process efficiency, day-to-day operations', tier: 'pro' },
|
|
161
|
+
{ id: 'isaac', name: 'Isaac', domain: 'Integrations', description: 'API integrations, webhook configurations, third-party connections', tier: 'pro' },
|
|
162
|
+
{ id: 'tommy', name: 'Tommy', domain: 'Travel', description: 'Travel booking, itineraries, logistics, trip planning', tier: 'pro' },
|
|
163
|
+
{ id: 'sloane', name: 'Sloane', domain: 'Sales', description: 'Sales pipeline, outreach, deal management, revenue strategy', tier: 'pro' },
|
|
164
|
+
{ id: 'nadia', name: 'Nadia', domain: 'UI/UX Design', description: 'UX wireframes, visual systems, prototypes, design reviews', tier: 'pro' },
|
|
165
|
+
{ id: 'morgan', name: 'Morgan', domain: 'Marketing', description: 'Content marketing, campaigns, SEO, social media, positioning', tier: 'pro' },
|
|
166
|
+
{ id: 'alex', name: 'Alex', domain: 'API Crawling', description: 'Web scraping, API data extraction, large-scale crawling', tier: 'pro' },
|
|
167
|
+
{ id: 'uma', name: 'Uma', domain: 'UX Research', description: 'User interviews, usability studies, qualitative research, personas', tier: 'pro' },
|
|
168
|
+
{ id: 'caleb', name: 'Caleb', domain: 'CRM', description: 'CRM administration, pipeline management, contact lifecycle', tier: 'pro' },
|
|
169
|
+
{ id: 'elena', name: 'Elena', domain: 'Estimation', description: 'Project estimation, effort scoping, technical planning', tier: 'pro' },
|
|
170
|
+
{ id: 'audrey', name: 'Audrey', domain: 'Accounting', description: 'Financial tracking, budgets, expense management, reconciliation', tier: 'pro' },
|
|
171
|
+
{ id: 'logan', name: 'Logan', domain: 'Legal', description: 'Contract review, terms of service, NDAs, regulatory compliance', tier: 'pro' },
|
|
172
|
+
{ id: 'eva', name: 'Eva', domain: 'Executive Assistant', description: 'Scheduling, meeting preparation, executive briefs, follow-ups', tier: 'pro' },
|
|
173
|
+
{ id: 'parker', name: 'Parker', domain: 'Project Management', description: 'Project milestones, tracking, cross-functional coordination', tier: 'pro' },
|
|
174
|
+
{ id: 'nate', name: 'Nate', domain: 'n8n Automation', description: 'n8n workflow building, node-based automation, workflow design', tier: 'pro' },
|
|
175
|
+
{ id: 'aria', name: 'Aria', domain: 'Autonomous Research', description: 'Deep investigative research, longitudinal studies, synthesis', tier: 'pro' },
|
|
176
|
+
{ id: 'maxxipro', name: 'MaxxiPro', domain: 'Roof Maxx Expert', description: 'Roofing estimation, contracting, roof inspection workflows', tier: 'pro' },
|
|
177
|
+
{ id: 'accuscope', name: 'AccuScope', domain: 'AccuLynx CRM Auditor', description: 'AccuLynx CRM data quality, auditing, pipeline health', tier: 'pro' },
|
|
178
|
+
{ id: 'smoketestagent', name: 'SmokeTestAgent', domain: 'QA Specialist', description: 'Smoke testing, regression testing, automated test suites', tier: 'pro' },
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
export function getAliceAgents(tier = 'both') {
|
|
182
|
+
if (tier === 'starter') return ALICE_AGENTS.filter(a => a.tier === 'starter' || a.isOrchestrator);
|
|
183
|
+
if (tier === 'pro') return ALICE_AGENTS;
|
|
184
|
+
return ALICE_AGENTS;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Install A.L.I.C.E. on Hermes ─────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
export async function installForHermes(options = {}) {
|
|
190
|
+
const { tier = 'starter', auto = false, userInfo = null } = options;
|
|
191
|
+
const agents = getAliceAgents(tier);
|
|
192
|
+
const result = { skills: [], memories: [], config: null, errors: [] };
|
|
193
|
+
|
|
194
|
+
console.log('');
|
|
195
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
196
|
+
console.log(` ${'🧠'.padStart(15)} ${'A.L.I.C.E. on Hermes'}`);
|
|
197
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
198
|
+
console.log('');
|
|
199
|
+
|
|
200
|
+
// 1. Verify Hermes is installed
|
|
201
|
+
if (!isHermesInstalled()) {
|
|
202
|
+
throw new Error('Hermes is not installed. Run: curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const version = getHermesVersion();
|
|
206
|
+
console.log(` ${'✓'.padStart(15)} ${version}`);
|
|
207
|
+
console.log('');
|
|
208
|
+
|
|
209
|
+
// 2. Ensure alice skill directory
|
|
210
|
+
if (!existsSync(ALICE_SKILLS_DIR)) {
|
|
211
|
+
mkdirSync(ALICE_SKILLS_DIR, { recursive: true });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 3. Install orchestrator skill
|
|
215
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
216
|
+
console.log(` ${'🤖'.padStart(15)} ${'A.L.I.C.E. orchestrator skill'}`);
|
|
217
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
218
|
+
const orchestratorDir = join(ALICE_SKILLS_DIR, 'alice');
|
|
219
|
+
mkdirSync(orchestratorDir, { recursive: true });
|
|
220
|
+
const orchestratorContent = generateOrchestratorSkill();
|
|
221
|
+
writeFileSync(join(orchestratorDir, 'SKILL.md'), orchestratorContent, 'utf8');
|
|
222
|
+
console.log(` ${'✓'.padStart(15)} alice/SKILL.md — orchestrator`);
|
|
223
|
+
result.skills.push('alice');
|
|
224
|
+
|
|
225
|
+
// 4. Install specialist skills
|
|
226
|
+
const specialistAgents = agents.filter(a => !a.isOrchestrator);
|
|
227
|
+
console.log('');
|
|
228
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
229
|
+
console.log(` ${'👥'.padStart(15)} ${specialistAgents.length} specialist skills`);
|
|
230
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
231
|
+
|
|
232
|
+
for (const agent of specialistAgents) {
|
|
233
|
+
const skillDir = join(ALICE_SKILLS_DIR, agent.id);
|
|
234
|
+
mkdirSync(skillDir, { recursive: true });
|
|
235
|
+
const skillContent = generateSpecialistSkill(agent.id, agent.name, agent.domain, agent.description || '');
|
|
236
|
+
writeFileSync(join(skillDir, 'SKILL.md'), skillContent, 'utf8');
|
|
237
|
+
console.log(` ${'✓'.padStart(15)} ${agent.id.padEnd(15)} — ${agent.domain}`);
|
|
238
|
+
result.skills.push(agent.id);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 5. Create memory directories
|
|
242
|
+
console.log('');
|
|
243
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
244
|
+
console.log(` ${'🧠'.padStart(15)} ${'Memory directories'}`);
|
|
245
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
246
|
+
|
|
247
|
+
for (const agent of agents) {
|
|
248
|
+
const memDir = join(HERMES_DIR, 'memories', agent.id);
|
|
249
|
+
if (!existsSync(memDir)) {
|
|
250
|
+
mkdirSync(memDir, { recursive: true });
|
|
251
|
+
}
|
|
252
|
+
result.memories.push(agent.id);
|
|
253
|
+
}
|
|
254
|
+
console.log(` ${'✓'.padStart(15)} ${agents.length} memory dirs created`);
|
|
255
|
+
|
|
256
|
+
// 6. Patch Hermes config with alice personality
|
|
257
|
+
console.log('');
|
|
258
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
259
|
+
console.log(` ${'⚙️'.padStart(15)} ${'Hermes configuration'}`);
|
|
260
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
261
|
+
|
|
262
|
+
configureHermesAlice();
|
|
263
|
+
result.config = 'config.yaml updated';
|
|
264
|
+
console.log(` ${'✓'.padStart(15)} personality 'alice' added to config.yaml`);
|
|
265
|
+
console.log(` ${'✓'.padStart(15)} display.personality set to alice`);
|
|
266
|
+
|
|
267
|
+
// 7. Write alice-config.json for installer tracking
|
|
268
|
+
const aliceConfig = {
|
|
269
|
+
runtime: 'hermes',
|
|
270
|
+
version,
|
|
271
|
+
installedAt: new Date().toISOString(),
|
|
272
|
+
agents: agents.map(a => a.id),
|
|
273
|
+
tier,
|
|
274
|
+
userName: userInfo?.name || 'unknown',
|
|
275
|
+
};
|
|
276
|
+
writeFileSync(ALICE_CONFIG_JSON, JSON.stringify(aliceConfig, null, 2), 'utf8');
|
|
277
|
+
|
|
278
|
+
console.log('');
|
|
279
|
+
console.log(` ${'✓'.padStart(15)} alice-config.json written`);
|
|
280
|
+
console.log('');
|
|
281
|
+
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Uninstall A.L.I.C.E. from Hermes ────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
export function uninstallFromHermes(options = {}) {
|
|
288
|
+
const { auto = false } = options;
|
|
289
|
+
const result = { removed: [], errors: [] };
|
|
290
|
+
|
|
291
|
+
// Remove alice skill tree
|
|
292
|
+
if (existsSync(ALICE_SKILLS_DIR)) {
|
|
293
|
+
rmSync(ALICE_SKILLS_DIR, { recursive: true, force: true });
|
|
294
|
+
result.removed.push('alice skills');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Remove alice memories
|
|
298
|
+
const memoriesBase = join(HERMES_DIR, 'memories', 'alice');
|
|
299
|
+
if (existsSync(memoriesBase)) {
|
|
300
|
+
rmSync(memoriesBase, { recursive: true, force: true });
|
|
301
|
+
result.removed.push('alice memories');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Remove config tracking
|
|
305
|
+
if (existsSync(ALICE_CONFIG_JSON)) {
|
|
306
|
+
rmSync(ALICE_CONFIG_JSON, { force: true });
|
|
307
|
+
result.removed.push('alice-config.json');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Remove alice personality using hermes config set (deletes by unsetting)
|
|
311
|
+
try {
|
|
312
|
+
// Setting to empty/null effectively removes it
|
|
313
|
+
execSync(`hermes config set display.personality helpful`, {
|
|
314
|
+
stdio: 'pipe',
|
|
315
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
316
|
+
});
|
|
317
|
+
result.removed.push('display.personality reset to helpful');
|
|
318
|
+
} catch (err) {
|
|
319
|
+
result.errors.push(`display.personality reset: ${err.message}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── Status ─────────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
export function getHermesAliceStatus() {
|
|
328
|
+
const configPath = join(ALICE_CONFIG_JSON);
|
|
329
|
+
if (!existsSync(configPath)) {
|
|
330
|
+
return { installed: false };
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const data = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
334
|
+
return { installed: true, ...data };
|
|
335
|
+
} catch {
|
|
336
|
+
return { installed: false };
|
|
337
|
+
}
|
|
338
|
+
}
|
package/lib/installer.mjs
CHANGED
|
@@ -39,6 +39,7 @@ import { c, bold, dim, green, greenBold, red, yellow, cyan, gray,
|
|
|
39
39
|
import { runSkillsWizardStep } from './skills.mjs';
|
|
40
40
|
import { resolveCodingAgentPreference } from './coding-agent.mjs';
|
|
41
41
|
import { loadAgentRegistry } from './agent-registry.mjs';
|
|
42
|
+
import { HermesAgentManager } from './hermes-agent.mjs';
|
|
42
43
|
|
|
43
44
|
function commandExists(cmd) {
|
|
44
45
|
const probe = process.platform === 'win32' ? 'where' : 'which';
|
|
@@ -54,6 +55,105 @@ function isOpenClawInstalled() {
|
|
|
54
55
|
return commandExists('openclaw');
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
// ── Hermes detection ─────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function isHermesInstalled() {
|
|
61
|
+
if (!existsSync(join(homedir(), '.hermes', 'config.yaml'))) return false;
|
|
62
|
+
try {
|
|
63
|
+
execSync('hermes version', { stdio: 'pipe' });
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getHermesVersion() {
|
|
71
|
+
try {
|
|
72
|
+
const output = execSync('hermes version 2>&1', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
73
|
+
return output;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function installHermes(auto) {
|
|
80
|
+
console.log('');
|
|
81
|
+
printSection('Installing Hermes Agent');
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(` ${dim('Hermes Agent is required for the Hermes agent bridge.')}`);
|
|
84
|
+
console.log(` ${dim('Install:')} ${cyan('curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash')}`);
|
|
85
|
+
console.log('');
|
|
86
|
+
|
|
87
|
+
if (auto) {
|
|
88
|
+
console.log(` ${icons.info} ${dim('Non-interactive: running Hermes install script now...')}`);
|
|
89
|
+
console.log('');
|
|
90
|
+
try {
|
|
91
|
+
execSync('curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash', {
|
|
92
|
+
stdio: 'inherit',
|
|
93
|
+
timeout: 120000,
|
|
94
|
+
});
|
|
95
|
+
const version = getHermesVersion();
|
|
96
|
+
if (version) {
|
|
97
|
+
printStepDone('Hermes installed', version);
|
|
98
|
+
return { status: 'ok', version };
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.log(` ${icons.warn} ${yellow('Hermes install failed.')}`);
|
|
102
|
+
console.log(` ${dim('Install manually:')} ${cyan('curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash')}`);
|
|
103
|
+
return { status: 'error', reason: err.message };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const shouldInstall = await confirm(' Install Hermes Agent now?');
|
|
108
|
+
if (!shouldInstall) {
|
|
109
|
+
printStepSkip('Hermes Agent', 'not installed — Hermes agent bridge will be skipped');
|
|
110
|
+
return { status: 'skipped' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
execSync('curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash', {
|
|
115
|
+
stdio: 'inherit',
|
|
116
|
+
timeout: 120000,
|
|
117
|
+
});
|
|
118
|
+
const version = getHermesVersion();
|
|
119
|
+
if (version) {
|
|
120
|
+
printStepDone('Hermes installed', version);
|
|
121
|
+
return { status: 'ok', version };
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.log(` ${icons.warn} ${yellow('Hermes install failed: ' + (err.message || 'unknown error'))}`);
|
|
125
|
+
console.log(` ${dim('Install manually:')} ${cyan('curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash')}`);
|
|
126
|
+
return { status: 'error', reason: err.message };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { status: 'ok', version: getHermesVersion() };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function checkHermes(auto, hermesBridgeRequested) {
|
|
133
|
+
const hermesFound = isHermesInstalled();
|
|
134
|
+
const version = hermesFound ? getHermesVersion() : null;
|
|
135
|
+
|
|
136
|
+
if (hermesFound && version) {
|
|
137
|
+
console.log(` ${icons.ok} ${green('Hermes detected:')} ${version}`);
|
|
138
|
+
return { status: 'found', version };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Hermes not found
|
|
142
|
+
if (hermesBridgeRequested) {
|
|
143
|
+
console.log(` ${icons.warn} ${yellow('Hermes not found.')}`);
|
|
144
|
+
console.log(` ${dim('The --hermes-bridge flag requires Hermes to be installed.')}`);
|
|
145
|
+
const result = await installHermes(auto);
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Hermes not requested — just informational
|
|
150
|
+
console.log(` ${icons.info} ${dim('Hermes not found.')}`);
|
|
151
|
+
console.log(` ${dim('Hermes enables persistent-memory specialist agents for the A.L.I.C.E. team.')}`);
|
|
152
|
+
console.log(` ${dim('Install:')} ${cyan('curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash')}`);
|
|
153
|
+
console.log(` ${dim('Or re-run with')} ${cyan('--hermes-bridge')} ${dim('to install automatically.')}`);
|
|
154
|
+
return { status: 'not_found' };
|
|
155
|
+
}
|
|
156
|
+
|
|
57
157
|
/**
|
|
58
158
|
* On Linux, Docker requires the user to be in the docker group.
|
|
59
159
|
* Detect this early and warn before OpenClaw's own preflight fails cryptically.
|
|
@@ -91,6 +191,16 @@ function checkLinuxDockerPermissions() {
|
|
|
91
191
|
}
|
|
92
192
|
|
|
93
193
|
async function detectRuntime() {
|
|
194
|
+
// Hermes is always standalone — check first
|
|
195
|
+
const hermesDir = join(homedir(), '.hermes');
|
|
196
|
+
const hermesConfig = join(hermesDir, 'config.yaml');
|
|
197
|
+
if (existsSync(hermesConfig)) {
|
|
198
|
+
try {
|
|
199
|
+
execSync('hermes version', { stdio: 'pipe' });
|
|
200
|
+
return 'hermes';
|
|
201
|
+
} catch {}
|
|
202
|
+
}
|
|
203
|
+
|
|
94
204
|
// Check for NemoClaw binary
|
|
95
205
|
try {
|
|
96
206
|
execSync('nemoclaw --version', { stdio: 'pipe' });
|
|
@@ -630,6 +740,120 @@ export async function runInstall(options = {}) {
|
|
|
630
740
|
// 0. Linux Docker permission check — warn early before OpenClaw preflight fails
|
|
631
741
|
checkLinuxDockerPermissions();
|
|
632
742
|
|
|
743
|
+
// 1. Detect runtime — Hermes, NemoClaw, OpenClaw, or none
|
|
744
|
+
const runtime = options.runtimeOverride || await detectRuntime();
|
|
745
|
+
|
|
746
|
+
// ── HERMES-ONLY PATH ──────────────────────────────────────────────────────
|
|
747
|
+
if (runtime === 'hermes') {
|
|
748
|
+
// Lazy-import to avoid circular deps
|
|
749
|
+
const { installForHermes, getHermesAliceStatus } = await import('./hermes-installer.mjs');
|
|
750
|
+
const { detectUserName, detectTimezone } = await import('./prompter.mjs');
|
|
751
|
+
|
|
752
|
+
console.log(` ${icons.ok} ${greenBold('Hermes detected')} ${dim('─')} A.L.I.C.E. will run as a Hermes skill tree\n`);
|
|
753
|
+
|
|
754
|
+
// Check if already installed
|
|
755
|
+
const existingStatus = getHermesAliceStatus();
|
|
756
|
+
if (existingStatus.installed && manifest && !options.force) {
|
|
757
|
+
console.log(` ${icons.info} ${dim('A.L.I.C.E. already installed on Hermes')}`);
|
|
758
|
+
console.log(` ${dim('Run with --force to reinstall.')}\n`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Detect Hermes version and model
|
|
762
|
+
const { getHermesVersion, getHermesDefaultModel } = await import('./hermes-installer.mjs');
|
|
763
|
+
const hermesVersion = getHermesVersion();
|
|
764
|
+
const hermesModel = getHermesDefaultModel();
|
|
765
|
+
if (hermesVersion) {
|
|
766
|
+
console.log(` ${icons.ok} ${green('Hermes version:')} ${hermesVersion}`);
|
|
767
|
+
}
|
|
768
|
+
if (hermesModel) {
|
|
769
|
+
console.log(` ${icons.ok} ${green('Model:')} ${hermesModel}`);
|
|
770
|
+
} else {
|
|
771
|
+
console.log('');
|
|
772
|
+
console.log(` ${icons.fail} ${red('No model configured for Hermes')}`);
|
|
773
|
+
console.log('');
|
|
774
|
+
console.log(` ${dim('A model is required before installing A.L.I.C.E.')}`);
|
|
775
|
+
console.log(` Run: ${cyan('hermes model')}`);
|
|
776
|
+
console.log(` Then re-run: ${cyan('npx @robbiesrobotics/alice-agents --runtime hermes')}`);
|
|
777
|
+
console.log('');
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// 2. Tier selection
|
|
782
|
+
let tier;
|
|
783
|
+
if (options.tierOverride) {
|
|
784
|
+
tier = normalizeTierOption(options.tierOverride);
|
|
785
|
+
} else if (auto) {
|
|
786
|
+
tier = 'starter';
|
|
787
|
+
} else {
|
|
788
|
+
tier = await promptTier();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// 3. User info
|
|
792
|
+
let userInfo;
|
|
793
|
+
if (auto) {
|
|
794
|
+
userInfo = { name: detectUserName(), timezone: detectTimezone(), notes: '' };
|
|
795
|
+
} else {
|
|
796
|
+
printSection('About You');
|
|
797
|
+
console.log('');
|
|
798
|
+
userInfo = await promptUserInfo();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// 4. Confirmation
|
|
802
|
+
const agents = (await import('./agent-registry.mjs')).loadAgentRegistry(tier);
|
|
803
|
+
if (!auto) {
|
|
804
|
+
printSection('Install Summary');
|
|
805
|
+
console.log('');
|
|
806
|
+
console.log(` ${dim('Runtime:')} ${green('Hermes Agent')}`);
|
|
807
|
+
console.log(` ${dim('Model:')} ${green(hermesModel || 'none')}`);
|
|
808
|
+
console.log(` ${dim('Tier:')} ${green(tier)} ${dim('(' + agents.length + ' agents)')}`);
|
|
809
|
+
console.log(` ${dim('User:')} ${green(userInfo.name)}`);
|
|
810
|
+
console.log('');
|
|
811
|
+
const ok = await confirm(' Proceed with Hermes install?');
|
|
812
|
+
if (!ok) {
|
|
813
|
+
console.log(' Aborted.');
|
|
814
|
+
closePrompt();
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
closePrompt();
|
|
819
|
+
|
|
820
|
+
// Execute Hermes install
|
|
821
|
+
printSection('Installing A.L.I.C.E. on Hermes');
|
|
822
|
+
console.log('');
|
|
823
|
+
|
|
824
|
+
const result = await installForHermes({ tier, auto, userInfo });
|
|
825
|
+
|
|
826
|
+
// Write manifest
|
|
827
|
+
writeManifest({
|
|
828
|
+
installedAt: existingStatus?.installedAt || new Date().toISOString(),
|
|
829
|
+
tier,
|
|
830
|
+
agents: result.skills,
|
|
831
|
+
userName: userInfo.name,
|
|
832
|
+
userTimezone: userInfo.timezone,
|
|
833
|
+
modelPreset: 'hermes-detected',
|
|
834
|
+
runtime: 'hermes',
|
|
835
|
+
skills: [],
|
|
836
|
+
codingTool: null,
|
|
837
|
+
codingSkill: null,
|
|
838
|
+
missionControl: null,
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
console.log('');
|
|
842
|
+
printSeparator();
|
|
843
|
+
console.log('');
|
|
844
|
+
console.log(` ${icons.ok} ${greenBold('A.L.I.C.E. installed on Hermes!')} ${dim(result.skills.length + ' skills ready.')}`);
|
|
845
|
+
console.log('');
|
|
846
|
+
console.log(` ${dim('Start A.L.I.C.E.:')} ${cyan('hermes chat')}`);
|
|
847
|
+
console.log(` ${dim('List skills:')} ${cyan('hermes skills list')}`);
|
|
848
|
+
console.log(` ${dim('Skills dir:')} ${cyan('~/.hermes/skills/alice/')}`);
|
|
849
|
+
console.log('');
|
|
850
|
+
printSeparator();
|
|
851
|
+
console.log('');
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
// ── END HERMES PATH ────────────────────────────────────────────────────────
|
|
855
|
+
|
|
856
|
+
// OpenClaw / NemoClaw path (existing behavior)
|
|
633
857
|
// 1. Detect OpenClaw — offer to install if missing
|
|
634
858
|
if (!isOpenClawInstalled() || !configExists()) {
|
|
635
859
|
await installRuntime(auto);
|
|
@@ -645,7 +869,6 @@ export async function runInstall(options = {}) {
|
|
|
645
869
|
// 1b. Check for OpenClaw updates
|
|
646
870
|
await checkForOpenClawUpdate(auto);
|
|
647
871
|
|
|
648
|
-
const runtime = await detectRuntime();
|
|
649
872
|
if (runtime === 'nemoclaw') {
|
|
650
873
|
console.log(` ${icons.ok} ${greenBold('NemoClaw detected')} ${dim('─')} agents run in OpenShell sandbox\n`);
|
|
651
874
|
} else {
|
|
@@ -657,17 +880,26 @@ export async function runInstall(options = {}) {
|
|
|
657
880
|
}
|
|
658
881
|
}
|
|
659
882
|
|
|
883
|
+
// Hermes check (for OpenClaw+Hermes hybrid setups)
|
|
884
|
+
const hermesResult = await checkHermes(auto, !!options.hermesBridge);
|
|
885
|
+
const hermesReady = hermesResult.status === 'found' || hermesResult.status === 'ok';
|
|
886
|
+
|
|
660
887
|
// Detect what models the user already has configured
|
|
661
888
|
const detectedModels = detectAvailableModels();
|
|
662
889
|
if (detectedModels?.hasModel) {
|
|
663
|
-
console.log(` ${icons.ok} ${green('
|
|
890
|
+
console.log(` ${icons.ok} ${green('Model:')} ${detectedModels.primary}`);
|
|
664
891
|
if (detectedModels.providers.length > 0) {
|
|
665
|
-
console.log(` Providers: ${detectedModels.providers.join(', ')}
|
|
666
|
-
} else {
|
|
667
|
-
console.log();
|
|
892
|
+
console.log(` Providers: ${detectedModels.providers.join(', ')}`);
|
|
668
893
|
}
|
|
894
|
+
console.log('');
|
|
669
895
|
} else {
|
|
670
|
-
console.log(` ${icons.
|
|
896
|
+
console.log(` ${icons.fail} ${red('No model configured for OpenClaw')}`);
|
|
897
|
+
console.log('');
|
|
898
|
+
console.log(` ${dim('A model is required before installing A.L.I.C.E.')}`);
|
|
899
|
+
console.log(` Run: ${cyan('openclaw configure')}`);
|
|
900
|
+
console.log(` Then re-run: ${cyan('npx @robbiesrobotics/alice-agents')}`);
|
|
901
|
+
console.log('');
|
|
902
|
+
process.exit(1);
|
|
671
903
|
}
|
|
672
904
|
|
|
673
905
|
// 2. Install mode
|
|
@@ -706,19 +938,9 @@ export async function runInstall(options = {}) {
|
|
|
706
938
|
userInfo = await promptUserInfo();
|
|
707
939
|
}
|
|
708
940
|
|
|
709
|
-
// 4. Model
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
// Non-interactive: use whatever the user already has configured.
|
|
713
|
-
// Only fall back to sonnet if nothing is detected (e.g. fresh OpenClaw install
|
|
714
|
-
// where the user explicitly set up Claude credentials).
|
|
715
|
-
preset = detectedModels?.hasModel ? 'detected' : 'sonnet';
|
|
716
|
-
} else {
|
|
717
|
-
preset = await promptModelPreset(detectedModels);
|
|
718
|
-
if (preset === 'custom') {
|
|
719
|
-
customModels = await promptCustomModel();
|
|
720
|
-
}
|
|
721
|
-
}
|
|
941
|
+
// 4. Model — always use whatever is already configured; we already verified one exists above
|
|
942
|
+
const preset = 'detected';
|
|
943
|
+
const customModels = null; // not used when preset=detected
|
|
722
944
|
|
|
723
945
|
// 5. Tier selection
|
|
724
946
|
let tier;
|
|
@@ -964,6 +1186,19 @@ export async function runInstall(options = {}) {
|
|
|
964
1186
|
sandboxName: 'my-assistant',
|
|
965
1187
|
});
|
|
966
1188
|
|
|
1189
|
+
// Hermes agent bridge setup
|
|
1190
|
+
if (options.hermesBridge && hermesReady) {
|
|
1191
|
+
const hermes = new HermesAgentManager();
|
|
1192
|
+
const bridgeResult = hermes.setupBridge();
|
|
1193
|
+
if (bridgeResult.status === 'ok') {
|
|
1194
|
+
printStepDone('Hermes bridge', `v${bridgeResult.hermesVersion} connected`);
|
|
1195
|
+
} else {
|
|
1196
|
+
printStepSkip('Hermes bridge', bridgeResult.reason);
|
|
1197
|
+
}
|
|
1198
|
+
} else if (options.hermesBridge && !hermesReady) {
|
|
1199
|
+
printStepSkip('Hermes bridge', 'Hermes not installed');
|
|
1200
|
+
}
|
|
1201
|
+
|
|
967
1202
|
// Write manifest
|
|
968
1203
|
const existing = readManifest();
|
|
969
1204
|
writeManifest({
|