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 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrix-yuri",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Yuri — Meta-Orchestrator for Orchestrix. Drive your entire project lifecycle with natural language.",
5
5
  "main": "lib/installer.js",
6
6
  "bin": {