orchestrix 15.14.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.
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+ const http = require('http');
5
+ const { MCP_SERVER_URL } = require('./merge');
6
+
7
+ /**
8
+ * Validate license key by calling MCP server tools/list
9
+ * Returns: { valid: true, tier: string } or { valid: false, error: string }
10
+ */
11
+ async function validateKey(key) {
12
+ try {
13
+ const response = await mcpRequest(key, 'tools/list', {});
14
+ if (response.result) {
15
+ return { valid: true, tier: 'pro' }; // If tools/list works, key is valid
16
+ }
17
+ return { valid: false, error: response.error?.message || 'Unknown error' };
18
+ } catch (err) {
19
+ return { valid: false, error: err.message };
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Fetch init data from MCP server
25
+ * Returns parsed response or null on failure
26
+ */
27
+ async function fetchInit(key, type = 'all') {
28
+ try {
29
+ const response = await mcpRequest(key, 'tools/call', {
30
+ name: 'orchestrix-init',
31
+ arguments: { type },
32
+ });
33
+ if (response.result && response.result.content) {
34
+ return response.result.content[0]?.text || null;
35
+ }
36
+ return null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Send a JSON-RPC request to the MCP server
44
+ */
45
+ function mcpRequest(key, method, params) {
46
+ return new Promise((resolve, reject) => {
47
+ const url = new URL(MCP_SERVER_URL);
48
+ const transport = url.protocol === 'https:' ? https : http;
49
+
50
+ const body = JSON.stringify({
51
+ jsonrpc: '2.0',
52
+ method,
53
+ params,
54
+ id: 1,
55
+ });
56
+
57
+ // Detect proxy from environment
58
+ const proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY ||
59
+ process.env.http_proxy || process.env.HTTP_PROXY;
60
+
61
+ const options = {
62
+ hostname: url.hostname,
63
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
64
+ path: url.pathname,
65
+ method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ 'Content-Length': Buffer.byteLength(body),
69
+ Authorization: `Bearer ${key}`,
70
+ },
71
+ timeout: 15000,
72
+ };
73
+
74
+ // If proxy is set, try to use it (basic support)
75
+ if (proxyUrl) {
76
+ try {
77
+ const proxy = new URL(proxyUrl);
78
+ options.hostname = proxy.hostname;
79
+ options.port = proxy.port;
80
+ options.path = MCP_SERVER_URL;
81
+ options.headers.Host = url.hostname;
82
+ } catch {
83
+ // Invalid proxy URL, proceed without proxy
84
+ }
85
+ }
86
+
87
+ const req = transport.request(options, (res) => {
88
+ let data = '';
89
+ res.on('data', (chunk) => { data += chunk; });
90
+ res.on('end', () => {
91
+ try {
92
+ const json = JSON.parse(data);
93
+ resolve(json);
94
+ } catch {
95
+ reject(new Error(`Invalid response from MCP server: ${data.slice(0, 200)}`));
96
+ }
97
+ });
98
+ });
99
+
100
+ req.on('error', (err) => reject(err));
101
+ req.on('timeout', () => {
102
+ req.destroy();
103
+ reject(new Error('MCP server request timed out (15s)'));
104
+ });
105
+
106
+ req.write(body);
107
+ req.end();
108
+ });
109
+ }
110
+
111
+ module.exports = { validateKey, fetchInit, mcpRequest };
package/lib/merge.js ADDED
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const MCP_SERVER_URL = 'https://orchestrix-mcp.youlidao.ai/api/mcp';
7
+
8
+ /**
9
+ * Deep merge .mcp.json — preserves existing entries, adds/updates orchestrix
10
+ * Returns: 'create' | 'update' | 'skip'
11
+ */
12
+ function mergeMcpJson(projectDir) {
13
+ const filePath = path.join(projectDir, '.mcp.json');
14
+ let existing = {};
15
+
16
+ if (fs.existsSync(filePath)) {
17
+ try {
18
+ existing = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
19
+ } catch {
20
+ // Invalid JSON, start fresh but warn
21
+ existing = {};
22
+ }
23
+ }
24
+
25
+ if (!existing.mcpServers) {
26
+ existing.mcpServers = {};
27
+ }
28
+
29
+ let changed = false;
30
+
31
+ // Add/update orchestrix entry
32
+ const orchestrixEntry = {
33
+ type: 'http',
34
+ url: MCP_SERVER_URL,
35
+ headers: {
36
+ Authorization: 'Bearer {{env:ORCHESTRIX_LICENSE_KEY}}',
37
+ },
38
+ };
39
+
40
+ const existingOrchestrix = existing.mcpServers.orchestrix;
41
+ if (!existingOrchestrix) {
42
+ existing.mcpServers.orchestrix = orchestrixEntry;
43
+ changed = true;
44
+ } else {
45
+ // Update URL and headers if different
46
+ if (existingOrchestrix.url !== orchestrixEntry.url ||
47
+ existingOrchestrix.type !== orchestrixEntry.type) {
48
+ existing.mcpServers.orchestrix = { ...existingOrchestrix, ...orchestrixEntry };
49
+ changed = true;
50
+ }
51
+ }
52
+
53
+ // Add sequential-thinking if not present (don't overwrite)
54
+ if (!existing.mcpServers['sequential-thinking']) {
55
+ existing.mcpServers['sequential-thinking'] = {
56
+ command: 'npx',
57
+ args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
58
+ };
59
+ changed = true;
60
+ }
61
+
62
+ if (!changed) {
63
+ return 'skip';
64
+ }
65
+
66
+ const action = fs.existsSync(filePath) ? 'update' : 'create';
67
+ fs.writeFileSync(filePath, JSON.stringify(existing, null, 2) + '\n');
68
+ return action;
69
+ }
70
+
71
+ /**
72
+ * Merge handoff-detector hook into .claude/settings.local.json
73
+ * Returns: 'create' | 'update' | 'skip'
74
+ */
75
+ function mergeSettingsLocal(projectDir) {
76
+ const filePath = path.join(projectDir, '.claude', 'settings.local.json');
77
+ let existing = {};
78
+
79
+ if (fs.existsSync(filePath)) {
80
+ try {
81
+ existing = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
82
+ } catch {
83
+ existing = {};
84
+ }
85
+ }
86
+
87
+ if (!existing.hooks) {
88
+ existing.hooks = {};
89
+ }
90
+ if (!existing.hooks.Stop) {
91
+ existing.hooks.Stop = [];
92
+ }
93
+
94
+ // Check if handoff-detector hook already exists
95
+ const hookCommand = "bash -c 'cd \"$(git rev-parse --show-toplevel)\" && .orchestrix-core/scripts/handoff-detector.sh'";
96
+ const hasHook = existing.hooks.Stop.some((entry) => {
97
+ if (!entry || !entry.hooks) return false;
98
+ return entry.hooks.some((h) => h.command && h.command.includes('handoff-detector'));
99
+ });
100
+
101
+ if (hasHook) {
102
+ return 'skip';
103
+ }
104
+
105
+ existing.hooks.Stop.push({
106
+ matcher: '',
107
+ hooks: [
108
+ {
109
+ type: 'command',
110
+ command: hookCommand,
111
+ },
112
+ ],
113
+ });
114
+
115
+ const action = fs.existsSync(filePath) ? 'update' : 'create';
116
+
117
+ // Ensure directory exists
118
+ const dir = path.dirname(filePath);
119
+ if (!fs.existsSync(dir)) {
120
+ fs.mkdirSync(dir, { recursive: true });
121
+ }
122
+
123
+ fs.writeFileSync(filePath, JSON.stringify(existing, null, 2) + '\n');
124
+ return action;
125
+ }
126
+
127
+ /**
128
+ * Remove orchestrix entry from .mcp.json (for uninstall)
129
+ * Returns: true if file was modified
130
+ */
131
+ function removeFromMcpJson(projectDir) {
132
+ const filePath = path.join(projectDir, '.mcp.json');
133
+ if (!fs.existsSync(filePath)) return false;
134
+
135
+ try {
136
+ const existing = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
137
+ if (!existing.mcpServers || !existing.mcpServers.orchestrix) return false;
138
+
139
+ delete existing.mcpServers.orchestrix;
140
+ // Also remove sequential-thinking if we added it
141
+ delete existing.mcpServers['sequential-thinking'];
142
+
143
+ // If mcpServers is now empty, remove the whole file
144
+ if (Object.keys(existing.mcpServers).length === 0 && Object.keys(existing).length === 1) {
145
+ fs.unlinkSync(filePath);
146
+ } else {
147
+ fs.writeFileSync(filePath, JSON.stringify(existing, null, 2) + '\n');
148
+ }
149
+ return true;
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Remove handoff-detector hook from settings.local.json (for uninstall)
157
+ * Returns: true if file was modified
158
+ */
159
+ function removeFromSettingsLocal(projectDir) {
160
+ const filePath = path.join(projectDir, '.claude', 'settings.local.json');
161
+ if (!fs.existsSync(filePath)) return false;
162
+
163
+ try {
164
+ const existing = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
165
+ if (!existing.hooks || !existing.hooks.Stop) return false;
166
+
167
+ const before = existing.hooks.Stop.length;
168
+ existing.hooks.Stop = existing.hooks.Stop.filter((entry) => {
169
+ if (!entry || !entry.hooks) return true;
170
+ return !entry.hooks.some((h) => h.command && h.command.includes('handoff-detector'));
171
+ });
172
+
173
+ if (existing.hooks.Stop.length === before) return false;
174
+
175
+ // Clean up empty structures
176
+ if (existing.hooks.Stop.length === 0) {
177
+ delete existing.hooks.Stop;
178
+ }
179
+ if (Object.keys(existing.hooks).length === 0) {
180
+ delete existing.hooks;
181
+ }
182
+
183
+ if (Object.keys(existing).length === 0) {
184
+ fs.unlinkSync(filePath);
185
+ } else {
186
+ fs.writeFileSync(filePath, JSON.stringify(existing, null, 2) + '\n');
187
+ }
188
+ return true;
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
193
+
194
+ module.exports = {
195
+ mergeMcpJson,
196
+ mergeSettingsLocal,
197
+ removeFromMcpJson,
198
+ removeFromSettingsLocal,
199
+ MCP_SERVER_URL,
200
+ };
package/lib/ui.js ADDED
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+
5
+ // ANSI color codes
6
+ const colors = {
7
+ reset: '\x1b[0m',
8
+ bold: '\x1b[1m',
9
+ dim: '\x1b[2m',
10
+ green: '\x1b[32m',
11
+ yellow: '\x1b[33m',
12
+ red: '\x1b[31m',
13
+ cyan: '\x1b[36m',
14
+ magenta: '\x1b[35m',
15
+ gray: '\x1b[90m',
16
+ };
17
+
18
+ function log(msg = '') {
19
+ console.log(` ${msg}`);
20
+ }
21
+
22
+ function success(msg) {
23
+ console.log(` ${colors.green}✓${colors.reset} ${msg}`);
24
+ }
25
+
26
+ function warn(msg) {
27
+ console.log(` ${colors.yellow}⚠${colors.reset} ${msg}`);
28
+ }
29
+
30
+ function error(msg) {
31
+ console.log(` ${colors.red}✗${colors.reset} ${msg}`);
32
+ }
33
+
34
+ function info(msg) {
35
+ console.log(` ${colors.cyan}ℹ${colors.reset} ${msg}`);
36
+ }
37
+
38
+ function header(msg) {
39
+ console.log();
40
+ console.log(` ${colors.bold}${msg}${colors.reset}`);
41
+ console.log();
42
+ }
43
+
44
+ function step(num, total, msg) {
45
+ console.log(` ${colors.dim}[${num}/${total}]${colors.reset} ${msg}`);
46
+ }
47
+
48
+ function fileAction(action, filePath) {
49
+ const actionColors = {
50
+ create: colors.green,
51
+ update: colors.yellow,
52
+ merge: colors.cyan,
53
+ skip: colors.gray,
54
+ overwrite: colors.yellow,
55
+ };
56
+ const c = actionColors[action] || colors.reset;
57
+ console.log(` ${c}${action.padEnd(10)}${colors.reset} ${filePath}`);
58
+ }
59
+
60
+ async function prompt(question, defaultValue) {
61
+ const rl = readline.createInterface({
62
+ input: process.stdin,
63
+ output: process.stdout,
64
+ });
65
+
66
+ const suffix = defaultValue ? ` (${defaultValue})` : '';
67
+
68
+ return new Promise((resolve) => {
69
+ rl.question(` ${question}${suffix}: `, (answer) => {
70
+ rl.close();
71
+ resolve(answer.trim() || defaultValue || '');
72
+ });
73
+ });
74
+ }
75
+
76
+ async function confirm(question, defaultYes = false) {
77
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
78
+ const answer = await prompt(`${question} ${hint}`);
79
+ if (!answer) return defaultYes;
80
+ return answer.toLowerCase().startsWith('y');
81
+ }
82
+
83
+ function banner() {
84
+ console.log();
85
+ console.log(` ${colors.magenta}${colors.bold}o8x${colors.reset} ${colors.dim}— Orchestrix Installer${colors.reset}`);
86
+ console.log();
87
+ }
88
+
89
+ function done() {
90
+ console.log();
91
+ console.log(` ${colors.green}${colors.bold}Done!${colors.reset}`);
92
+ console.log();
93
+ }
94
+
95
+ module.exports = {
96
+ colors,
97
+ log,
98
+ success,
99
+ warn,
100
+ error,
101
+ info,
102
+ header,
103
+ step,
104
+ fileAction,
105
+ prompt,
106
+ confirm,
107
+ banner,
108
+ done,
109
+ };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const ui = require('./ui');
6
+ const { removeFromMcpJson, removeFromSettingsLocal } = require('./merge');
7
+
8
+ async function uninstall(flags) {
9
+ ui.banner();
10
+ ui.header('Uninstalling Orchestrix');
11
+
12
+ const projectDir = process.cwd();
13
+
14
+ // 1. Remove slash commands
15
+ const commandsDir = path.join(projectDir, '.claude', 'commands');
16
+ const commandFiles = ['o.md', 'o-help.md', 'o-status.md'];
17
+ for (const f of commandFiles) {
18
+ const p = path.join(commandsDir, f);
19
+ if (fs.existsSync(p)) {
20
+ fs.unlinkSync(p);
21
+ ui.fileAction('remove', `.claude/commands/${f}`);
22
+ }
23
+ }
24
+
25
+ // 2. Remove from .mcp.json
26
+ if (removeFromMcpJson(projectDir)) {
27
+ ui.fileAction('update', '.mcp.json (removed orchestrix entries)');
28
+ }
29
+
30
+ // 3. Remove handoff hook from settings.local.json
31
+ if (removeFromSettingsLocal(projectDir)) {
32
+ ui.fileAction('update', '.claude/settings.local.json (removed hook)');
33
+ }
34
+
35
+ // 4. Remove .orchestrix-core (ask unless --force or --yes)
36
+ const orchestrixDir = path.join(projectDir, '.orchestrix-core');
37
+ if (fs.existsSync(orchestrixDir)) {
38
+ let shouldRemove = flags.force || flags.yes;
39
+ if (!shouldRemove) {
40
+ shouldRemove = await ui.confirm('Remove .orchestrix-core/ (includes project config)?', false);
41
+ }
42
+
43
+ if (shouldRemove) {
44
+ fs.rmSync(orchestrixDir, { recursive: true, force: true });
45
+ ui.fileAction('remove', '.orchestrix-core/');
46
+ } else {
47
+ ui.fileAction('skip', '.orchestrix-core/ (preserved)');
48
+ }
49
+ }
50
+
51
+ // Note: We do NOT remove .env.local or ORCHESTRIX_LICENSE_KEY from it
52
+ ui.info('.env.local preserved (manage license key manually)');
53
+
54
+ ui.done();
55
+ }
56
+
57
+ module.exports = { uninstall };
package/lib/upgrade.js ADDED
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const ui = require('./ui');
4
+ const { install } = require('./install');
5
+
6
+ /**
7
+ * Upgrade is just a force re-install that preserves core-config.yaml
8
+ * (install already skips core-config.yaml if it exists)
9
+ */
10
+ async function upgrade(flags) {
11
+ ui.banner();
12
+ ui.info('Upgrading Orchestrix installation...');
13
+ console.log();
14
+
15
+ // Re-run install with force flag
16
+ await install({ ...flags, force: true });
17
+ }
18
+
19
+ module.exports = { upgrade };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "orchestrix",
3
+ "version": "15.14.0",
4
+ "description": "Install Orchestrix multi-agent infrastructure into any project. One command: npx o8x install",
5
+ "bin": {
6
+ "o8x": "bin/o8x.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "lib/"
11
+ ],
12
+ "scripts": {
13
+ "test": "node --test 'test/*.test.js'"
14
+ },
15
+ "keywords": [
16
+ "orchestrix",
17
+ "ai-agent",
18
+ "multi-agent",
19
+ "claude-code",
20
+ "opencode",
21
+ "tmux-automation",
22
+ "handoff",
23
+ "cli"
24
+ ],
25
+ "author": "dorayo",
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "dependencies": {}
31
+ }