orchestrix-yuri 3.3.1 → 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/README.md +113 -142
- package/bin/doctor.js +355 -0
- package/bin/install.js +3 -0
- package/bin/status.js +3 -0
- package/lib/gateway/channels/telegram.js +21 -5
- package/lib/gateway/engine/claude-sdk.js +23 -1
- package/lib/gateway/router.js +95 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ User describes idea → Yuri drives: Create → Plan → Develop → Test → De
|
|
|
8
8
|
|
|
9
9
|
## How It Works
|
|
10
10
|
|
|
11
|
-
Yuri is a [Claude Code skill](https://code.claude.com/docs/en/skills) + Channel Gateway.
|
|
11
|
+
Yuri is a [Claude Code skill](https://code.claude.com/docs/en/skills) + Channel Gateway. Two ways to use:
|
|
12
12
|
|
|
13
13
|
1. **Terminal mode** — activate `/yuri` inside any Claude Code session
|
|
14
|
-
2. **Telegram mode** — chat with Yuri via Telegram bot
|
|
14
|
+
2. **Telegram mode** — chat with Yuri via Telegram bot
|
|
15
15
|
|
|
16
16
|
| Phase | What Yuri Does | Agents Involved |
|
|
17
17
|
|-------|---------------|-----------------|
|
|
@@ -34,35 +34,38 @@ Yuri is a [Claude Code skill](https://code.claude.com/docs/en/skills) + Channel
|
|
|
34
34
|
### Method A: From npm (recommended)
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
# Install globally
|
|
38
37
|
npm install -g orchestrix-yuri
|
|
39
|
-
|
|
40
|
-
# Initialize skill + global memory
|
|
41
38
|
orchestrix-yuri install
|
|
42
|
-
|
|
43
|
-
#
|
|
44
|
-
orchestrix-yuri serve --telegram-token "YOUR_BOT_TOKEN"
|
|
39
|
+
orchestrix-yuri start --token "YOUR_BOT_TOKEN" # first time, saves token
|
|
40
|
+
orchestrix-yuri start # from now on
|
|
45
41
|
```
|
|
46
42
|
|
|
47
43
|
### Method B: From source
|
|
48
44
|
|
|
49
45
|
```bash
|
|
50
|
-
git clone https://github.com/
|
|
46
|
+
git clone https://github.com/dorayo/orchestrix-yuri.git
|
|
51
47
|
cd orchestrix-yuri
|
|
52
48
|
npm install
|
|
53
|
-
|
|
54
|
-
# Initialize skill + global memory
|
|
55
49
|
node bin/install.js install
|
|
56
|
-
|
|
57
|
-
# Start Telegram gateway
|
|
58
|
-
node bin/serve.js --telegram-token "YOUR_BOT_TOKEN"
|
|
50
|
+
node bin/serve.js --token "YOUR_BOT_TOKEN"
|
|
59
51
|
```
|
|
60
52
|
|
|
61
53
|
### What `install` does
|
|
62
54
|
|
|
63
55
|
- Copies the Yuri skill to `~/.claude/skills/yuri/`
|
|
64
|
-
- Initializes global memory at `~/.yuri/` (identity, boss profile, portfolio
|
|
65
|
-
-
|
|
56
|
+
- Initializes global memory at `~/.yuri/` (identity, boss profile, portfolio, focus, wisdom)
|
|
57
|
+
- Writes a bootstrap signal so Yuri greets you on first interaction
|
|
58
|
+
|
|
59
|
+
## CLI Commands
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
orchestrix-yuri install # Install Yuri skill + global memory
|
|
63
|
+
orchestrix-yuri start # Start the Channel Gateway
|
|
64
|
+
orchestrix-yuri start --token TOKEN # Start & save Telegram Bot token (first time)
|
|
65
|
+
orchestrix-yuri stop # Stop the running gateway
|
|
66
|
+
orchestrix-yuri status # Show gateway status + cost tracking
|
|
67
|
+
orchestrix-yuri --version # Show version
|
|
68
|
+
```
|
|
66
69
|
|
|
67
70
|
## Usage
|
|
68
71
|
|
|
@@ -74,168 +77,130 @@ In any Claude Code session:
|
|
|
74
77
|
/yuri
|
|
75
78
|
```
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
### Telegram Mode
|
|
78
81
|
|
|
79
82
|
| Command | Description |
|
|
80
83
|
|---------|-------------|
|
|
81
|
-
| `*create` | Create a new project
|
|
82
|
-
| `*plan` | Start
|
|
83
|
-
| `*develop` | Start
|
|
84
|
-
| `*test` |
|
|
85
|
-
| `*deploy` |
|
|
86
|
-
| `*status` | Show
|
|
87
|
-
| `*
|
|
88
|
-
| `*
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
| `*create` | Create a new project |
|
|
85
|
+
| `*plan` | Start planning (runs in background, agents auto-chain) |
|
|
86
|
+
| `*develop` | Start development (4 agents run autonomously) |
|
|
87
|
+
| `*test` | Run smoke tests |
|
|
88
|
+
| `*deploy` | Deploy the project |
|
|
89
|
+
| `*status` | Show progress + cost tracking |
|
|
90
|
+
| `*projects` | List all registered projects |
|
|
91
|
+
| `*switch <name>` | Switch active project |
|
|
92
|
+
| `*cancel` | Stop running phase |
|
|
93
|
+
| `*resume` | Resume from last checkpoint |
|
|
94
|
+
| `*change "{desc}"` | Handle requirement change |
|
|
95
|
+
|
|
96
|
+
### Agent Interaction via Telegram
|
|
97
|
+
|
|
98
|
+
When a planning agent asks a question (e.g., PM asking to confirm features), Yuri bridges it to Telegram:
|
|
93
99
|
|
|
94
|
-
```bash
|
|
95
|
-
# With token as CLI argument
|
|
96
|
-
orchestrix-yuri serve --telegram-token "YOUR_BOT_TOKEN"
|
|
97
|
-
|
|
98
|
-
# Or configure in ~/.yuri/config/channels.yaml first
|
|
99
|
-
orchestrix-yuri serve
|
|
100
100
|
```
|
|
101
|
+
Yuri: "📋 PM is asking:
|
|
102
|
+
Which features to include?
|
|
103
|
+
1. User auth 2. Dashboard 3. API
|
|
104
|
+
↩️ Reply to this message to answer"
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
Edit `~/.yuri/config/channels.yaml`:
|
|
105
|
-
|
|
106
|
-
```yaml
|
|
107
|
-
server:
|
|
108
|
-
port: 7890
|
|
109
|
-
|
|
110
|
-
channels:
|
|
111
|
-
telegram:
|
|
112
|
-
enabled: true
|
|
113
|
-
token: "YOUR_BOT_TOKEN"
|
|
114
|
-
mode: polling
|
|
115
|
-
owner_chat_id: "" # Auto-bound on first /start
|
|
116
|
-
|
|
117
|
-
engine:
|
|
118
|
-
skill: yuri
|
|
119
|
-
tmux_session: yuri-gateway
|
|
120
|
-
startup_timeout: 30000
|
|
121
|
-
poll_interval: 2000
|
|
122
|
-
timeout: 300000
|
|
123
|
-
autocompact_pct: 80
|
|
124
|
-
compact_every: 50
|
|
106
|
+
You reply: "1 and 2, skip 3" ← forwarded to PM agent
|
|
125
107
|
```
|
|
126
108
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
1. Start the gateway: `orchestrix-yuri serve --telegram-token "YOUR_TOKEN"`
|
|
130
|
-
2. Open Telegram, find your bot, send `/start`
|
|
131
|
-
3. The first user to send `/start` becomes the owner (all others are rejected)
|
|
132
|
-
4. Send any message to interact with Yuri
|
|
109
|
+
**Normal messages** (not reply-to) always go to Yuri directly.
|
|
133
110
|
|
|
134
111
|
## Architecture
|
|
135
112
|
|
|
136
|
-
###
|
|
137
|
-
|
|
138
|
-
The Telegram gateway runs Claude Code in a persistent tmux session (`yuri-gateway`), not as a one-shot subprocess. This means:
|
|
113
|
+
### Gateway Engine (claude-sdk)
|
|
139
114
|
|
|
140
|
-
|
|
141
|
-
- **Conversation context is preserved** natively by Claude Code
|
|
142
|
-
- **No cold-start per message** — only the first message incurs startup latency
|
|
115
|
+
Each Telegram message is processed via `claude -p --output-format json`:
|
|
143
116
|
|
|
144
|
-
|
|
117
|
+
- **First message**: `--system-prompt` injects Yuri persona (SKILL.md) + L1 memory + channel instructions
|
|
118
|
+
- **Subsequent**: `--resume SESSION_ID` preserves conversation context
|
|
119
|
+
- **Output**: Structured JSON with `result` (clean text), `session_id`, `total_cost_usd`
|
|
145
120
|
|
|
146
|
-
|
|
147
|
-
|--------|-------|-------------|
|
|
148
|
-
| `○` | Idle | Waiting for input |
|
|
149
|
-
| `●` | Processing | Generating a response |
|
|
150
|
-
| `◐` | Approval | Permission prompt (auto-approved) |
|
|
151
|
-
| `[Verb]ed for Ns` | Complete | Response finished (e.g. "Baked for 31s") |
|
|
121
|
+
### Async Phase Orchestration
|
|
152
122
|
|
|
153
|
-
|
|
123
|
+
`*plan` and `*develop` run as **Node.js background tasks** (not inside a `claude -p` call):
|
|
154
124
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
125
|
+
```
|
|
126
|
+
*plan → PhaseOrchestrator.startPlan() → instant reply
|
|
127
|
+
↓
|
|
128
|
+
setInterval(30s) polls tmux agent → detects completion
|
|
129
|
+
↓
|
|
130
|
+
starts next agent → proactive Telegram notification
|
|
131
|
+
↓
|
|
132
|
+
user asks anything → normal Claude response (not blocked)
|
|
133
|
+
```
|
|
158
134
|
|
|
159
|
-
|
|
135
|
+
tmux is used **only for multi-agent orchestration** (plan/develop), not for the gateway itself.
|
|
160
136
|
|
|
161
|
-
|
|
137
|
+
### Memory System (4 layers)
|
|
162
138
|
|
|
163
139
|
```
|
|
164
|
-
~/.yuri/
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
│ └── preferences.yaml # Boss preferences
|
|
169
|
-
├── portfolio/
|
|
170
|
-
│ ├── registry.yaml # All projects (active/archived)
|
|
171
|
-
│ ├── priorities.yaml # Portfolio priorities
|
|
172
|
-
│ └── relationships.yaml # Project relationships
|
|
173
|
-
├── focus.yaml # Current focus & state
|
|
174
|
-
├── config/
|
|
175
|
-
│ └── channels.yaml # Gateway channel config
|
|
176
|
-
├── chat-history/ # JSONL per chat_id
|
|
177
|
-
├── inbox.jsonl # Observation signals
|
|
178
|
-
└── wisdom/ # Accumulated knowledge
|
|
140
|
+
L1 Global (~/.yuri/) — Yuri identity, boss profile, portfolio, focus
|
|
141
|
+
L2 Project ({project}/.yuri/) — Project identity, knowledge, decisions
|
|
142
|
+
L3 Phase State (state/) — Per-phase operational status
|
|
143
|
+
L4 Events (timeline/) — Append-only event history
|
|
179
144
|
```
|
|
180
145
|
|
|
181
|
-
|
|
146
|
+
**Reflect Engine**: Signals detected from user messages (preferences, identity, priorities) are written to `inbox.jsonl`, then processed by `reflect.js` into the corresponding YAML files before each Claude call.
|
|
182
147
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
148
|
+
### Context Management
|
|
149
|
+
|
|
150
|
+
- `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=80` triggers auto-compact at 80%
|
|
151
|
+
- Proactive `/compact` every 50 messages
|
|
152
|
+
- L1 hash tracking: when memory changes, a `[CONTEXT UPDATE]` prefix is injected into the next message
|
|
188
153
|
|
|
189
154
|
### File Structure
|
|
190
155
|
|
|
191
156
|
```
|
|
192
157
|
orchestrix-yuri/
|
|
193
158
|
├── bin/
|
|
194
|
-
│ ├── install.js
|
|
195
|
-
│
|
|
159
|
+
│ ├── install.js # CLI entry (install/start/stop/status)
|
|
160
|
+
│ ├── serve.js # Gateway launcher
|
|
161
|
+
│ ├── stop.js # PID-based stop
|
|
162
|
+
│ └── status.js # Gateway + cost status
|
|
196
163
|
├── lib/
|
|
197
|
-
│ ├── installer.js
|
|
198
|
-
│ ├── migrate.js
|
|
164
|
+
│ ├── installer.js # Global install logic
|
|
165
|
+
│ ├── migrate.js # v1 → v2 memory migration
|
|
199
166
|
│ └── gateway/
|
|
200
|
-
│ ├── index.js
|
|
201
|
-
│ ├── config.js
|
|
202
|
-
│ ├── router.js
|
|
203
|
-
│ ├── binding.js
|
|
204
|
-
│ ├── history.js
|
|
167
|
+
│ ├── index.js # startGateway()
|
|
168
|
+
│ ├── config.js # Config loading + defaults
|
|
169
|
+
│ ├── router.js # Message routing, phase detection, signal detection
|
|
170
|
+
│ ├── binding.js # Owner authentication
|
|
171
|
+
│ ├── history.js # Chat history (JSONL)
|
|
172
|
+
│ ├── log.js # Colored terminal logging
|
|
205
173
|
│ ├── channels/
|
|
206
|
-
│ │ └── telegram.js
|
|
174
|
+
│ │ └── telegram.js # grammy adapter (placeholder + edit pattern)
|
|
207
175
|
│ └── engine/
|
|
208
|
-
│
|
|
176
|
+
│ ├── claude-sdk.js # Claude -p JSON engine + session management
|
|
177
|
+
│ ├── phase-orchestrator.js # Background plan/develop execution
|
|
178
|
+
│ ├── reflect.js # Inbox signal processing → YAML memory
|
|
179
|
+
│ └── tmux-utils.js # Shared tmux operations
|
|
209
180
|
└── skill/
|
|
210
|
-
├── SKILL.md
|
|
211
|
-
├── tasks/
|
|
212
|
-
├── scripts/
|
|
213
|
-
├── templates/
|
|
214
|
-
├── data/
|
|
215
|
-
└── resources/
|
|
181
|
+
├── SKILL.md # Yuri persona + tmux command rules
|
|
182
|
+
├── tasks/ # Phase workflow instructions
|
|
183
|
+
├── scripts/ # Shell scripts (tmux, monitoring)
|
|
184
|
+
├── templates/ # Memory schema
|
|
185
|
+
├── data/ # Decision rules, signal taxonomy
|
|
186
|
+
└── resources/ # MCP config, hooks, tmux scripts
|
|
216
187
|
```
|
|
217
188
|
|
|
218
|
-
|
|
189
|
+
### tmux Sessions (for agent orchestration)
|
|
190
|
+
|
|
191
|
+
| Session | Purpose | Windows |
|
|
192
|
+
|---------|---------|---------|
|
|
193
|
+
| `op-{project}` | Planning phase | One per agent |
|
|
194
|
+
| `orchestrix-{repo-id}` | Development phase | 4 fixed (Architect, SM, Dev, QA) |
|
|
219
195
|
|
|
220
|
-
|
|
196
|
+
## Change Management
|
|
221
197
|
|
|
222
198
|
| Scope | Action |
|
|
223
199
|
|-------|--------|
|
|
224
200
|
| Small (≤5 files) | Dev `*solo` directly |
|
|
225
201
|
| Medium | PO `*route-change` → standard workflow |
|
|
226
202
|
| Large | Pause dev, partial re-plan |
|
|
227
|
-
| New iteration | PM `*start-iteration` →
|
|
228
|
-
|
|
229
|
-
## Deployment Options
|
|
230
|
-
|
|
231
|
-
| Region | Provider | Best For |
|
|
232
|
-
|--------|----------|----------|
|
|
233
|
-
| China | Sealos | Prototype / MVP |
|
|
234
|
-
| China | Aliyun ECS + Docker | Production |
|
|
235
|
-
| China | Vercel (CN) | Frontend / SSR |
|
|
236
|
-
| Overseas | Vercel | Frontend / Full-stack |
|
|
237
|
-
| Overseas | Railway | Backend APIs |
|
|
238
|
-
| Overseas | AWS / GCP | Enterprise |
|
|
203
|
+
| New iteration | PM `*start-iteration` → plan → dev → test |
|
|
239
204
|
|
|
240
205
|
## Troubleshooting
|
|
241
206
|
|
|
@@ -243,17 +208,23 @@ Yuri handles mid-project changes based on scope:
|
|
|
243
208
|
# Check prerequisites
|
|
244
209
|
tmux -V && which claude && node -v
|
|
245
210
|
|
|
246
|
-
# View gateway logs
|
|
247
|
-
orchestrix-yuri
|
|
211
|
+
# View gateway logs
|
|
212
|
+
orchestrix-yuri start 2>&1 | tee gateway.log
|
|
213
|
+
|
|
214
|
+
# Check status + cost
|
|
215
|
+
orchestrix-yuri status
|
|
216
|
+
|
|
217
|
+
# Stop the gateway
|
|
218
|
+
orchestrix-yuri stop
|
|
248
219
|
|
|
249
|
-
# Check
|
|
220
|
+
# Check running tmux sessions (plan/develop phases)
|
|
250
221
|
tmux ls
|
|
251
222
|
|
|
252
|
-
# Peek into
|
|
253
|
-
tmux attach -t
|
|
223
|
+
# Peek into a planning agent
|
|
224
|
+
tmux attach -t op-myproject
|
|
254
225
|
|
|
255
|
-
#
|
|
256
|
-
tmux
|
|
226
|
+
# Peek into dev agents
|
|
227
|
+
tmux attach -t orchestrix-myproject
|
|
257
228
|
```
|
|
258
229
|
|
|
259
230
|
## License
|
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/bin/status.js
CHANGED
|
@@ -30,6 +30,9 @@ function status() {
|
|
|
30
30
|
const age = Date.now() - new Date(s.savedAt).getTime();
|
|
31
31
|
if (age < 24 * 3600_000) {
|
|
32
32
|
sessionInfo = `${s.sessionId.slice(0, 8)}... (${s.messageCount || 0} messages)`;
|
|
33
|
+
if (s.totalCost) {
|
|
34
|
+
sessionInfo += ` | $${s.totalCost.toFixed(4)}`;
|
|
35
|
+
}
|
|
33
36
|
} else {
|
|
34
37
|
sessionInfo = 'expired';
|
|
35
38
|
}
|
|
@@ -39,16 +39,32 @@ class TelegramAdapter {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
try {
|
|
42
|
+
// Send placeholder immediately so user sees instant feedback
|
|
43
|
+
const placeholder = await ctx.reply('...').catch(() => null);
|
|
44
|
+
|
|
42
45
|
const reply = await this.onMessage(msg);
|
|
43
46
|
if (reply && reply.text) {
|
|
44
|
-
// Telegram has a 4096 char limit per message
|
|
45
47
|
const chunks = splitMessage(reply.text, 4000);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
|
|
49
|
+
if (placeholder && chunks.length === 1) {
|
|
50
|
+
// Single chunk: edit the placeholder message in-place
|
|
51
|
+
await ctx.api.editMessageText(ctx.chat.id, placeholder.message_id, chunks[0], { parse_mode: 'Markdown' }).catch(() => {
|
|
52
|
+
return ctx.api.editMessageText(ctx.chat.id, placeholder.message_id, chunks[0]).catch(() => {});
|
|
50
53
|
});
|
|
54
|
+
} else {
|
|
55
|
+
// Multi-chunk: delete placeholder, send chunks separately
|
|
56
|
+
if (placeholder) {
|
|
57
|
+
await ctx.api.deleteMessage(ctx.chat.id, placeholder.message_id).catch(() => {});
|
|
58
|
+
}
|
|
59
|
+
for (const chunk of chunks) {
|
|
60
|
+
await ctx.reply(chunk, { parse_mode: 'Markdown' }).catch(() => {
|
|
61
|
+
return ctx.reply(chunk);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
51
64
|
}
|
|
65
|
+
} else if (placeholder) {
|
|
66
|
+
// No reply: remove placeholder
|
|
67
|
+
await ctx.api.deleteMessage(ctx.chat.id, placeholder.message_id).catch(() => {});
|
|
52
68
|
}
|
|
53
69
|
} catch (err) {
|
|
54
70
|
log.error(`Message handling failed: ${err.message}`);
|
|
@@ -117,6 +117,8 @@ let _sessionId = null;
|
|
|
117
117
|
let _messageCount = 0;
|
|
118
118
|
let _messageQueue = Promise.resolve();
|
|
119
119
|
let _lastL1Hash = null;
|
|
120
|
+
let _totalCost = 0;
|
|
121
|
+
let _totalDuration = 0;
|
|
120
122
|
|
|
121
123
|
// ── System Prompt ──────────────────────────────────────────────────────────────
|
|
122
124
|
|
|
@@ -188,6 +190,8 @@ function saveSessionState() {
|
|
|
188
190
|
version: SESSION_VERSION,
|
|
189
191
|
sessionId: _sessionId,
|
|
190
192
|
messageCount: _messageCount,
|
|
193
|
+
totalCost: _totalCost,
|
|
194
|
+
totalDuration: _totalDuration,
|
|
191
195
|
savedAt: new Date().toISOString(),
|
|
192
196
|
}));
|
|
193
197
|
} catch { /* best effort */ }
|
|
@@ -290,6 +294,8 @@ async function callClaude(opts) {
|
|
|
290
294
|
log.engine(`Restoring session ${saved.sessionId.slice(0, 8)}...`);
|
|
291
295
|
_sessionId = saved.sessionId;
|
|
292
296
|
_messageCount = saved.messageCount || 0;
|
|
297
|
+
_totalCost = saved.totalCost || 0;
|
|
298
|
+
_totalDuration = saved.totalDuration || 0;
|
|
293
299
|
}
|
|
294
300
|
}
|
|
295
301
|
|
|
@@ -326,6 +332,8 @@ async function callClaude(opts) {
|
|
|
326
332
|
}
|
|
327
333
|
|
|
328
334
|
_messageCount++;
|
|
335
|
+
if (result.cost) _totalCost += result.cost;
|
|
336
|
+
if (result.duration) _totalDuration += result.duration;
|
|
329
337
|
saveSessionState();
|
|
330
338
|
|
|
331
339
|
// If --resume failed (session expired), retry with fresh session
|
|
@@ -385,10 +393,20 @@ function composePrompt(userMessage) {
|
|
|
385
393
|
* Destroy session state. Called on gateway shutdown.
|
|
386
394
|
*/
|
|
387
395
|
function destroySession() {
|
|
388
|
-
// Don't clear session file — allow restart to resume
|
|
389
396
|
log.engine('Gateway shutting down (session preserved for restart)');
|
|
390
397
|
}
|
|
391
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Get accumulated cost and usage stats.
|
|
401
|
+
*/
|
|
402
|
+
function getUsageStats() {
|
|
403
|
+
return {
|
|
404
|
+
messageCount: _messageCount,
|
|
405
|
+
totalCost: _totalCost,
|
|
406
|
+
totalDuration: _totalDuration,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
392
410
|
module.exports = {
|
|
393
411
|
callClaude,
|
|
394
412
|
composePrompt,
|
|
@@ -396,4 +414,8 @@ module.exports = {
|
|
|
396
414
|
resolveProjectRoot,
|
|
397
415
|
findClaudeBinary,
|
|
398
416
|
destroySession,
|
|
417
|
+
clearSessionState,
|
|
418
|
+
getUsageStats,
|
|
419
|
+
// Exported for testing
|
|
420
|
+
isEmptyTemplate,
|
|
399
421
|
};
|
package/lib/gateway/router.js
CHANGED
|
@@ -24,6 +24,11 @@ const PHASE_COMMANDS = {
|
|
|
24
24
|
cancel: /^\*cancel\b/i,
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
const META_COMMANDS = {
|
|
28
|
+
projects: /^\*projects\b/i,
|
|
29
|
+
switch: /^\*switch\s+(.+)/i,
|
|
30
|
+
};
|
|
31
|
+
|
|
27
32
|
const STATUS_PATTERNS = [
|
|
28
33
|
/^\*status\b/i,
|
|
29
34
|
/进度|状态|怎么样了|到哪了/,
|
|
@@ -128,6 +133,15 @@ class Router {
|
|
|
128
133
|
return this._handleStatusQuery(msg);
|
|
129
134
|
}
|
|
130
135
|
|
|
136
|
+
// ═══ META COMMANDS — *projects, *switch (always allowed) ═══
|
|
137
|
+
if (META_COMMANDS.projects.test(msg.text.trim())) {
|
|
138
|
+
return this._handleProjects(msg);
|
|
139
|
+
}
|
|
140
|
+
const switchMatch = msg.text.trim().match(META_COMMANDS.switch);
|
|
141
|
+
if (switchMatch) {
|
|
142
|
+
return this._handleSwitch(switchMatch[1].trim(), msg);
|
|
143
|
+
}
|
|
144
|
+
|
|
131
145
|
// ═══ AGENT REPLY — user replied to an agent question notification ═══
|
|
132
146
|
if (msg.replyToMessageId && this.orchestrator.isWaitingForInput()) {
|
|
133
147
|
const waitingId = this.orchestrator.getWaitingMessageId();
|
|
@@ -252,8 +266,14 @@ class Router {
|
|
|
252
266
|
}
|
|
253
267
|
}
|
|
254
268
|
|
|
269
|
+
// Cost tracking
|
|
270
|
+
const usage = engine.getUsageStats();
|
|
271
|
+
if (usage.totalCost > 0) {
|
|
272
|
+
parts.push(`\n💰 Cost: $${usage.totalCost.toFixed(4)} | ${usage.messageCount} messages | ${(usage.totalDuration / 1000).toFixed(0)}s total`);
|
|
273
|
+
}
|
|
274
|
+
|
|
255
275
|
if (parts.length === 0) {
|
|
256
|
-
parts.push('No active phase. Available commands: *create, *plan, *develop, *test, *deploy');
|
|
276
|
+
parts.push('No active phase. Available commands: *create, *plan, *develop, *test, *deploy, *projects, *switch');
|
|
257
277
|
}
|
|
258
278
|
|
|
259
279
|
this.history.append(msg.chatId, 'user', msg.text);
|
|
@@ -434,6 +454,80 @@ class Router {
|
|
|
434
454
|
}
|
|
435
455
|
}
|
|
436
456
|
|
|
457
|
+
// ── Multi-Project Commands ────────────────────────────────────────────────────
|
|
458
|
+
|
|
459
|
+
_handleProjects(msg) {
|
|
460
|
+
const registryPath = path.join(YURI_GLOBAL, 'portfolio', 'registry.yaml');
|
|
461
|
+
if (!fs.existsSync(registryPath)) {
|
|
462
|
+
return { text: 'No projects registered yet. Use *create to create one.' };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const registry = yaml.load(fs.readFileSync(registryPath, 'utf8')) || {};
|
|
466
|
+
const projects = registry.projects || [];
|
|
467
|
+
if (projects.length === 0) {
|
|
468
|
+
return { text: 'No projects registered yet. Use *create to create one.' };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Read active project
|
|
472
|
+
const focusPath = path.join(YURI_GLOBAL, 'focus.yaml');
|
|
473
|
+
let activeId = '';
|
|
474
|
+
if (fs.existsSync(focusPath)) {
|
|
475
|
+
const focus = yaml.load(fs.readFileSync(focusPath, 'utf8')) || {};
|
|
476
|
+
activeId = focus.active_project || '';
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const statusIcons = { active: '✅', paused: '💤', maintenance: '🔧', archived: '📦' };
|
|
480
|
+
const lines = projects.map((p, i) => {
|
|
481
|
+
const icon = statusIcons[p.status] || '❓';
|
|
482
|
+
const isCurrent = p.id === activeId ? ' ← current' : '';
|
|
483
|
+
return `${i + 1}. **${p.name || p.id}** ${icon} ${p.status} (Phase ${p.phase || '?'}: ${p.pulse || '?'})${isCurrent}`;
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const reply = `📂 **Projects**\n\n${lines.join('\n')}\n\nUse \`*switch <name>\` to change active project.`;
|
|
487
|
+
this.history.append(msg.chatId, 'user', msg.text);
|
|
488
|
+
this.history.append(msg.chatId, 'assistant', reply);
|
|
489
|
+
return { text: reply };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
_handleSwitch(query, msg) {
|
|
493
|
+
const registryPath = path.join(YURI_GLOBAL, 'portfolio', 'registry.yaml');
|
|
494
|
+
if (!fs.existsSync(registryPath)) {
|
|
495
|
+
return { text: '❌ No projects registered.' };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const registry = yaml.load(fs.readFileSync(registryPath, 'utf8')) || {};
|
|
499
|
+
const projects = registry.projects || [];
|
|
500
|
+
const q = query.toLowerCase();
|
|
501
|
+
|
|
502
|
+
// Fuzzy match: exact id, exact name, prefix of id, prefix of name
|
|
503
|
+
const match = projects.find((p) => p.id === q || (p.name && p.name.toLowerCase() === q))
|
|
504
|
+
|| projects.find((p) => p.id.startsWith(q) || (p.name && p.name.toLowerCase().startsWith(q)));
|
|
505
|
+
|
|
506
|
+
if (!match) {
|
|
507
|
+
return { text: `❌ No project matching "${query}". Use *projects to see available projects.` };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Update focus
|
|
511
|
+
const focusPath = path.join(YURI_GLOBAL, 'focus.yaml');
|
|
512
|
+
let focus = {};
|
|
513
|
+
if (fs.existsSync(focusPath)) {
|
|
514
|
+
focus = yaml.load(fs.readFileSync(focusPath, 'utf8')) || {};
|
|
515
|
+
}
|
|
516
|
+
focus.active_project = match.id;
|
|
517
|
+
focus.active_action = `switched to project: ${match.name || match.id}`;
|
|
518
|
+
focus.updated_at = new Date().toISOString();
|
|
519
|
+
fs.writeFileSync(focusPath, yaml.dump(focus, { lineWidth: -1 }));
|
|
520
|
+
|
|
521
|
+
// Clear Claude session so next message gets fresh system prompt for new project
|
|
522
|
+
engine.clearSessionState();
|
|
523
|
+
|
|
524
|
+
const reply = `✅ Switched to **${match.name || match.id}** (Phase ${match.phase || '?'}: ${match.pulse || '?'})`;
|
|
525
|
+
this.history.append(msg.chatId, 'user', msg.text);
|
|
526
|
+
this.history.append(msg.chatId, 'assistant', reply);
|
|
527
|
+
log.router(`Project switched to: ${match.id}`);
|
|
528
|
+
return { text: reply };
|
|
529
|
+
}
|
|
530
|
+
|
|
437
531
|
_updateGlobalFocus(msg, projectRoot) {
|
|
438
532
|
const focusPath = path.join(YURI_GLOBAL, 'focus.yaml');
|
|
439
533
|
if (!fs.existsSync(focusPath)) return;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orchestrix-yuri",
|
|
3
|
-
"version": "3.
|
|
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": {
|
|
7
7
|
"orchestrix-yuri": "bin/install.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "
|
|
10
|
+
"test": "node --test 'test/*.test.js'",
|
|
11
11
|
"serve": "node bin/serve.js"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|