orchestrix-yuri 3.4.0 → 3.5.0
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/bin/doctor.js +355 -0
- package/bin/install.js +3 -0
- package/package.json +1 -1
package/bin/doctor.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const HOME = os.homedir();
|
|
10
|
+
const YURI_GLOBAL = path.join(HOME, '.yuri');
|
|
11
|
+
const SKILL_DIR = path.join(HOME, '.claude', 'skills', 'yuri');
|
|
12
|
+
|
|
13
|
+
const c = {
|
|
14
|
+
reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m',
|
|
15
|
+
yellow: '\x1b[33m', cyan: '\x1b[36m', dim: '\x1b[90m', bold: '\x1b[1m',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let passCount = 0;
|
|
19
|
+
let failCount = 0;
|
|
20
|
+
let warnCount = 0;
|
|
21
|
+
|
|
22
|
+
function pass(msg) { passCount++; console.log(` ${c.green}✅${c.reset} ${msg}`); }
|
|
23
|
+
function fail(msg, fix) {
|
|
24
|
+
failCount++;
|
|
25
|
+
console.log(` ${c.red}❌${c.reset} ${msg}`);
|
|
26
|
+
if (fix) console.log(` ${c.dim}Fix: ${fix}${c.reset}`);
|
|
27
|
+
}
|
|
28
|
+
function warn(msg) { warnCount++; console.log(` ${c.yellow}⚠️${c.reset} ${msg}`); }
|
|
29
|
+
function info(msg) { console.log(` ${c.dim}ℹ ${msg}${c.reset}`); }
|
|
30
|
+
function section(title) { console.log(`\n ${c.bold}${c.cyan}${title}${c.reset}`); }
|
|
31
|
+
|
|
32
|
+
function cmd(command) {
|
|
33
|
+
try { return execSync(command, { encoding: 'utf8', timeout: 10000 }).trim(); } catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fileExists(p) { return fs.existsSync(p); }
|
|
37
|
+
function isExecutable(p) {
|
|
38
|
+
try { fs.accessSync(p, fs.constants.X_OK); return true; } catch { return false; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Checks ─────────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function checkRuntime() {
|
|
44
|
+
section('1. Runtime Environment');
|
|
45
|
+
|
|
46
|
+
// Node.js
|
|
47
|
+
const nodeV = cmd('node --version');
|
|
48
|
+
if (nodeV) {
|
|
49
|
+
const major = parseInt(nodeV.replace('v', ''), 10);
|
|
50
|
+
if (major >= 18) pass(`Node.js ${nodeV}`);
|
|
51
|
+
else fail(`Node.js ${nodeV} (requires >= 18)`, 'Upgrade Node.js to v18+');
|
|
52
|
+
} else {
|
|
53
|
+
fail('Node.js not found', 'Install Node.js >= 18');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// tmux
|
|
57
|
+
const tmuxV = cmd('tmux -V');
|
|
58
|
+
if (tmuxV) pass(`tmux ${tmuxV}`);
|
|
59
|
+
else fail('tmux not found', 'brew install tmux (macOS) or apt install tmux (Linux)');
|
|
60
|
+
|
|
61
|
+
// zsh (needed for claude binary resolution)
|
|
62
|
+
const zshV = cmd('zsh --version');
|
|
63
|
+
if (zshV) pass(`zsh available`);
|
|
64
|
+
else warn('zsh not found — Claude binary resolution may fail on some systems');
|
|
65
|
+
|
|
66
|
+
// git
|
|
67
|
+
const gitV = cmd('git --version');
|
|
68
|
+
if (gitV) pass(`git available`);
|
|
69
|
+
else warn('git not found — project creation requires git');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function checkClaude() {
|
|
73
|
+
section('2. Claude Code CLI');
|
|
74
|
+
|
|
75
|
+
const candidates = [
|
|
76
|
+
cmd('zsh -lc "which claude" 2>/dev/null'),
|
|
77
|
+
'/usr/local/bin/claude',
|
|
78
|
+
'/opt/homebrew/bin/claude',
|
|
79
|
+
path.join(HOME, '.npm-global', 'bin', 'claude'),
|
|
80
|
+
path.join(HOME, '.local', 'bin', 'claude'),
|
|
81
|
+
path.join(HOME, '.claude', 'bin', 'claude'),
|
|
82
|
+
].filter(Boolean);
|
|
83
|
+
|
|
84
|
+
let found = null;
|
|
85
|
+
for (const p of candidates) {
|
|
86
|
+
if (fileExists(p)) { found = p; break; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (found) {
|
|
90
|
+
pass(`Claude binary: ${found}`);
|
|
91
|
+
const version = cmd(`"${found}" --version 2>/dev/null`);
|
|
92
|
+
if (version) pass(`Claude Code ${version}`);
|
|
93
|
+
else warn('Could not get Claude Code version');
|
|
94
|
+
} else {
|
|
95
|
+
fail('Claude Code CLI not found', 'npm install -g @anthropic-ai/claude-code');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function checkInstallation() {
|
|
100
|
+
section('3. Yuri Installation');
|
|
101
|
+
|
|
102
|
+
// Skill files
|
|
103
|
+
const skillMd = path.join(SKILL_DIR, 'SKILL.md');
|
|
104
|
+
if (fileExists(skillMd)) {
|
|
105
|
+
const size = fs.statSync(skillMd).size;
|
|
106
|
+
if (size > 100) pass(`SKILL.md (${(size / 1024).toFixed(1)}KB)`);
|
|
107
|
+
else warn('SKILL.md exists but seems empty');
|
|
108
|
+
} else {
|
|
109
|
+
fail('SKILL.md not found', 'orchestrix-yuri install');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Scripts
|
|
113
|
+
const scripts = ['ensure-session.sh', 'monitor-agent.sh', 'scan-stories.sh', 'start-planning.sh'];
|
|
114
|
+
const scriptDir = path.join(SKILL_DIR, 'scripts');
|
|
115
|
+
for (const s of scripts) {
|
|
116
|
+
const p = path.join(scriptDir, s);
|
|
117
|
+
if (fileExists(p)) {
|
|
118
|
+
if (isExecutable(p)) pass(`scripts/${s}`);
|
|
119
|
+
else warn(`scripts/${s} exists but not executable`);
|
|
120
|
+
} else {
|
|
121
|
+
warn(`scripts/${s} missing`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Resources
|
|
126
|
+
const resources = ['start-orchestrix.sh', 'handoff-detector.sh', 'settings.local.json'];
|
|
127
|
+
const resDir = path.join(SKILL_DIR, 'resources');
|
|
128
|
+
for (const r of resources) {
|
|
129
|
+
const p = path.join(resDir, r);
|
|
130
|
+
if (fileExists(p)) pass(`resources/${r}`);
|
|
131
|
+
else warn(`resources/${r} missing`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function checkMemory() {
|
|
136
|
+
section('4. Global Memory (~/.yuri/)');
|
|
137
|
+
|
|
138
|
+
if (!fileExists(YURI_GLOBAL)) {
|
|
139
|
+
fail('~/.yuri/ directory not found', 'orchestrix-yuri install');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
pass('~/.yuri/ directory exists');
|
|
143
|
+
|
|
144
|
+
const required = [
|
|
145
|
+
['self.yaml', 'Yuri identity'],
|
|
146
|
+
['boss/profile.yaml', 'Boss profile'],
|
|
147
|
+
['boss/preferences.yaml', 'Boss preferences'],
|
|
148
|
+
['portfolio/registry.yaml', 'Project registry'],
|
|
149
|
+
['focus.yaml', 'Global focus'],
|
|
150
|
+
['config/channels.yaml', 'Channel config'],
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
for (const [rel, label] of required) {
|
|
154
|
+
const p = path.join(YURI_GLOBAL, rel);
|
|
155
|
+
if (fileExists(p)) pass(`${rel}`);
|
|
156
|
+
else fail(`${rel} missing (${label})`, 'orchestrix-yuri install');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check inbox
|
|
160
|
+
const inboxPath = path.join(YURI_GLOBAL, 'inbox.jsonl');
|
|
161
|
+
if (fileExists(inboxPath)) {
|
|
162
|
+
const content = fs.readFileSync(inboxPath, 'utf8').trim();
|
|
163
|
+
const lines = content ? content.split('\n').length : 0;
|
|
164
|
+
const unprocessed = content ? content.split('\n').filter((l) => l.includes('"processed":false')).length : 0;
|
|
165
|
+
pass(`inbox.jsonl (${lines} entries, ${unprocessed} unprocessed)`);
|
|
166
|
+
} else {
|
|
167
|
+
warn('inbox.jsonl not found');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check wisdom
|
|
171
|
+
const wisdomDir = path.join(YURI_GLOBAL, 'wisdom');
|
|
172
|
+
if (fileExists(wisdomDir)) pass('wisdom/ directory');
|
|
173
|
+
else warn('wisdom/ directory missing');
|
|
174
|
+
|
|
175
|
+
// Chat history
|
|
176
|
+
const historyDir = path.join(YURI_GLOBAL, 'chat-history');
|
|
177
|
+
if (fileExists(historyDir)) pass('chat-history/ directory');
|
|
178
|
+
else warn('chat-history/ directory missing');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function checkConfig() {
|
|
182
|
+
section('5. Gateway Configuration');
|
|
183
|
+
|
|
184
|
+
const configPath = path.join(YURI_GLOBAL, 'config', 'channels.yaml');
|
|
185
|
+
if (!fileExists(configPath)) {
|
|
186
|
+
fail('channels.yaml not found', 'orchestrix-yuri install');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const yaml = require('js-yaml');
|
|
192
|
+
const config = yaml.load(fs.readFileSync(configPath, 'utf8')) || {};
|
|
193
|
+
|
|
194
|
+
// Telegram
|
|
195
|
+
const tg = config.channels && config.channels.telegram;
|
|
196
|
+
if (tg && tg.enabled && tg.token) {
|
|
197
|
+
if (tg.token.includes(':')) pass(`Telegram token configured (${tg.token.slice(0, 8)}...)`);
|
|
198
|
+
else warn('Telegram token format looks invalid (expected BOT_ID:TOKEN)');
|
|
199
|
+
} else if (tg && tg.enabled && !tg.token) {
|
|
200
|
+
fail('Telegram enabled but token is empty', 'orchestrix-yuri start --token YOUR_TOKEN');
|
|
201
|
+
} else {
|
|
202
|
+
info('Telegram not enabled');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Owner binding
|
|
206
|
+
if (tg && tg.owner_chat_id) {
|
|
207
|
+
pass(`Owner bound: chat ${tg.owner_chat_id}`);
|
|
208
|
+
} else {
|
|
209
|
+
info('No owner bound yet (will auto-bind on first /start)');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Engine
|
|
213
|
+
const engine = config.engine || {};
|
|
214
|
+
info(`Timeout: ${(engine.timeout || 300000) / 1000}s, Compact every: ${engine.compact_every || 50} msgs`);
|
|
215
|
+
|
|
216
|
+
} catch (err) {
|
|
217
|
+
fail(`channels.yaml parse error: ${err.message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function checkGatewayState() {
|
|
222
|
+
section('6. Gateway State');
|
|
223
|
+
|
|
224
|
+
// PID file
|
|
225
|
+
const pidPath = path.join(YURI_GLOBAL, 'gateway.pid');
|
|
226
|
+
if (fileExists(pidPath)) {
|
|
227
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
228
|
+
try {
|
|
229
|
+
process.kill(pid, 0);
|
|
230
|
+
pass(`Gateway running (PID ${pid})`);
|
|
231
|
+
} catch {
|
|
232
|
+
warn(`Stale PID file (process ${pid} not running)`);
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
info('Gateway not running');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Session state
|
|
239
|
+
const sessionPath = path.join(YURI_GLOBAL, 'gateway-session.json');
|
|
240
|
+
if (fileExists(sessionPath)) {
|
|
241
|
+
try {
|
|
242
|
+
const s = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
|
|
243
|
+
const age = Date.now() - new Date(s.savedAt).getTime();
|
|
244
|
+
const ageH = (age / 3600000).toFixed(1);
|
|
245
|
+
if (age < 24 * 3600000) {
|
|
246
|
+
pass(`Session: ${s.sessionId.slice(0, 8)}... (${s.messageCount || 0} msgs, ${ageH}h old, $${(s.totalCost || 0).toFixed(4)})`);
|
|
247
|
+
} else {
|
|
248
|
+
warn(`Session expired (${ageH}h old)`);
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
warn('Session file corrupted');
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
info('No session state (will create on first message)');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// tmux sessions
|
|
258
|
+
const tmuxList = cmd('tmux list-sessions -F "#{session_name}" 2>/dev/null');
|
|
259
|
+
if (tmuxList) {
|
|
260
|
+
const sessions = tmuxList.split('\n').filter((s) => s.startsWith('op-') || s.startsWith('orchestrix-'));
|
|
261
|
+
if (sessions.length > 0) {
|
|
262
|
+
for (const s of sessions) {
|
|
263
|
+
info(`Active tmux session: ${s}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function checkProjects() {
|
|
270
|
+
section('7. Projects');
|
|
271
|
+
|
|
272
|
+
const registryPath = path.join(YURI_GLOBAL, 'portfolio', 'registry.yaml');
|
|
273
|
+
if (!fileExists(registryPath)) {
|
|
274
|
+
info('No project registry');
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const yaml = require('js-yaml');
|
|
280
|
+
const registry = yaml.load(fs.readFileSync(registryPath, 'utf8')) || {};
|
|
281
|
+
const projects = registry.projects || [];
|
|
282
|
+
|
|
283
|
+
if (projects.length === 0) {
|
|
284
|
+
info('No projects registered. Use *create to create one.');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (const p of projects) {
|
|
289
|
+
const rootExists = p.root && fileExists(p.root);
|
|
290
|
+
const status = rootExists ? `${p.status || '?'} (Phase ${p.phase || '?'})` : 'root directory missing!';
|
|
291
|
+
if (rootExists) pass(`${p.name || p.id}: ${status}`);
|
|
292
|
+
else fail(`${p.name || p.id}: ${status}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Active project
|
|
296
|
+
const focusPath = path.join(YURI_GLOBAL, 'focus.yaml');
|
|
297
|
+
if (fileExists(focusPath)) {
|
|
298
|
+
const focus = yaml.load(fs.readFileSync(focusPath, 'utf8')) || {};
|
|
299
|
+
if (focus.active_project) {
|
|
300
|
+
info(`Active project: ${focus.active_project}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch { /* ok */ }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function checkNetwork() {
|
|
307
|
+
section('8. Network');
|
|
308
|
+
|
|
309
|
+
// Telegram API
|
|
310
|
+
const configPath = path.join(YURI_GLOBAL, 'config', 'channels.yaml');
|
|
311
|
+
if (fileExists(configPath)) {
|
|
312
|
+
try {
|
|
313
|
+
const yaml = require('js-yaml');
|
|
314
|
+
const config = yaml.load(fs.readFileSync(configPath, 'utf8')) || {};
|
|
315
|
+
const tg = config.channels && config.channels.telegram;
|
|
316
|
+
|
|
317
|
+
if (tg && tg.enabled && tg.token) {
|
|
318
|
+
const result = cmd(`curl -s -o /dev/null -w "%{http_code}" "https://api.telegram.org/bot${tg.token}/getMe" 2>/dev/null`);
|
|
319
|
+
if (result === '200') pass('Telegram API reachable + token valid');
|
|
320
|
+
else if (result === '401') fail('Telegram token is invalid (401)', 'Check your bot token with @BotFather');
|
|
321
|
+
else if (result) warn(`Telegram API returned HTTP ${result}`);
|
|
322
|
+
else warn('Cannot reach api.telegram.org (network issue?)');
|
|
323
|
+
}
|
|
324
|
+
} catch { /* ok */ }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Main ───────────────────────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
function doctor() {
|
|
331
|
+
console.log(`\n ${c.bold}Orchestrix Yuri — Health Check${c.reset}\n`);
|
|
332
|
+
|
|
333
|
+
checkRuntime();
|
|
334
|
+
checkClaude();
|
|
335
|
+
checkInstallation();
|
|
336
|
+
checkMemory();
|
|
337
|
+
checkConfig();
|
|
338
|
+
checkGatewayState();
|
|
339
|
+
checkProjects();
|
|
340
|
+
checkNetwork();
|
|
341
|
+
|
|
342
|
+
// Summary
|
|
343
|
+
console.log(`\n ${c.bold}Summary${c.reset}`);
|
|
344
|
+
console.log(` ${c.green}✅ ${passCount} passed${c.reset} ${failCount > 0 ? c.red : c.dim}❌ ${failCount} failed${c.reset} ${warnCount > 0 ? c.yellow : c.dim}⚠️ ${warnCount} warnings${c.reset}`);
|
|
345
|
+
|
|
346
|
+
if (failCount === 0) {
|
|
347
|
+
console.log(`\n ${c.green}${c.bold}All critical checks passed!${c.reset}\n`);
|
|
348
|
+
} else {
|
|
349
|
+
console.log(`\n ${c.red}${failCount} issue(s) need attention. Fix them and run doctor again.${c.reset}\n`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
process.exit(failCount > 0 ? 1 : 0);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
doctor();
|
package/bin/install.js
CHANGED
|
@@ -18,6 +18,8 @@ if (command === 'install') {
|
|
|
18
18
|
require('./stop');
|
|
19
19
|
} else if (command === 'status') {
|
|
20
20
|
require('./status');
|
|
21
|
+
} else if (command === 'doctor') {
|
|
22
|
+
require('./doctor');
|
|
21
23
|
} else if (command === '--version' || command === '-v' || command === '-V') {
|
|
22
24
|
const { version } = require('../package.json');
|
|
23
25
|
console.log(version);
|
|
@@ -31,6 +33,7 @@ if (command === 'install') {
|
|
|
31
33
|
orchestrix-yuri start --token TOKEN Start & save Telegram Bot token (first time only)
|
|
32
34
|
orchestrix-yuri stop Stop the running gateway
|
|
33
35
|
orchestrix-yuri status Show gateway status
|
|
36
|
+
orchestrix-yuri doctor Health check all dependencies & config
|
|
34
37
|
orchestrix-yuri migrate [path] Migrate legacy memory.yaml
|
|
35
38
|
orchestrix-yuri --version Show version
|
|
36
39
|
orchestrix-yuri --help Show this help message
|