orkestrate 0.1.13 → 0.1.15
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/dist/cli.js +523 -111
- package/dist/cli.js.map +1 -1
- package/dist/mcp-entry.js +162 -41
- package/dist/mcp-entry.js.map +1 -1
- package/package.json +50 -50
package/dist/mcp-entry.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/lib/config.ts
|
|
4
4
|
import Conf from "conf";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
6
|
+
import { dirname } from "path";
|
|
7
|
+
import { mkdirSync } from "fs";
|
|
5
8
|
var config = new Conf({
|
|
6
9
|
projectName: "orkestrate",
|
|
7
10
|
projectSuffix: "",
|
|
@@ -9,18 +12,78 @@ var config = new Conf({
|
|
|
9
12
|
credentials: null,
|
|
10
13
|
activeWorkspaceId: null,
|
|
11
14
|
activeWorkspaceName: null,
|
|
12
|
-
serverUrl: "https://orkestrate.space"
|
|
15
|
+
serverUrl: "https://orkestrate.space",
|
|
16
|
+
userToolSettings: {}
|
|
13
17
|
}
|
|
14
18
|
});
|
|
15
19
|
function getServerUrl() {
|
|
16
20
|
return config.get("serverUrl");
|
|
17
21
|
}
|
|
22
|
+
function setConfigWithRetry(key, value, maxAttempts = 5) {
|
|
23
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
24
|
+
try {
|
|
25
|
+
config.set(key, value);
|
|
26
|
+
return;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err?.code !== "EPERM" && err?.code !== "EBUSY") {
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
const delay = 50 * Math.pow(2, attempt);
|
|
32
|
+
if (attempt < maxAttempts - 1) {
|
|
33
|
+
const start = Date.now();
|
|
34
|
+
while (Date.now() - start < delay) {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const configPath = config.path;
|
|
41
|
+
const dir = dirname(configPath);
|
|
42
|
+
if (!existsSync(dir)) {
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
let currentConfig = {};
|
|
46
|
+
if (existsSync(configPath)) {
|
|
47
|
+
try {
|
|
48
|
+
currentConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
49
|
+
} catch {
|
|
50
|
+
currentConfig = {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
currentConfig[key] = value;
|
|
54
|
+
writeFileSync(configPath, JSON.stringify(currentConfig, null, 2), "utf-8");
|
|
55
|
+
return;
|
|
56
|
+
} catch (fallbackErr) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Failed to save config after ${maxAttempts} attempts. Original error: ${fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
18
62
|
function setCredentials(creds) {
|
|
19
|
-
|
|
63
|
+
setConfigWithRetry("credentials", creds);
|
|
20
64
|
}
|
|
21
65
|
function getCredentials() {
|
|
22
66
|
return config.get("credentials");
|
|
23
67
|
}
|
|
68
|
+
function getUserToolSettings() {
|
|
69
|
+
return config.get("userToolSettings") || {};
|
|
70
|
+
}
|
|
71
|
+
function getEnabledTools() {
|
|
72
|
+
const settings = getUserToolSettings();
|
|
73
|
+
return settings.enabledTools ?? null;
|
|
74
|
+
}
|
|
75
|
+
function getDisabledTools() {
|
|
76
|
+
const settings = getUserToolSettings();
|
|
77
|
+
return settings.disabledTools || [];
|
|
78
|
+
}
|
|
79
|
+
function isToolAllowed(toolName) {
|
|
80
|
+
const enabledTools = getEnabledTools();
|
|
81
|
+
const disabledTools = getDisabledTools();
|
|
82
|
+
if (enabledTools !== null) {
|
|
83
|
+
return enabledTools.includes(toolName);
|
|
84
|
+
}
|
|
85
|
+
return !disabledTools.includes(toolName);
|
|
86
|
+
}
|
|
24
87
|
|
|
25
88
|
// src/lib/auth.ts
|
|
26
89
|
import { createHash, randomBytes } from "crypto";
|
|
@@ -70,33 +133,36 @@ async function getValidToken() {
|
|
|
70
133
|
}
|
|
71
134
|
|
|
72
135
|
// src/commands/mcp.ts
|
|
73
|
-
async function mcpCommand() {
|
|
136
|
+
async function mcpCommand(opts) {
|
|
137
|
+
const PARENT_TOOL = opts.parentTool ?? null;
|
|
74
138
|
try {
|
|
75
139
|
const serverUrl = getServerUrl();
|
|
76
140
|
const mcpUrl = `${serverUrl}/api/mcp`;
|
|
77
141
|
process.stdin.setEncoding("utf-8");
|
|
78
142
|
let buffer = "";
|
|
79
|
-
|
|
80
|
-
|
|
143
|
+
const startupMsg = `[Orkestrate-MCP] Starting bridge to ${mcpUrl}${PARENT_TOOL ? ` (parent: ${PARENT_TOOL})` : ""}
|
|
144
|
+
`;
|
|
145
|
+
process.stderr.write(startupMsg);
|
|
81
146
|
process.stdin.on("data", async (chunk) => {
|
|
82
147
|
const rawChunk = String(chunk);
|
|
83
|
-
if (process.env.DEBUG)
|
|
148
|
+
if (process.env.DEBUG)
|
|
149
|
+
process.stderr.write(`[Orkestrate-MCP] Received chunk: ${rawChunk}
|
|
84
150
|
`);
|
|
85
151
|
buffer += rawChunk;
|
|
86
152
|
let lineEndIndex;
|
|
87
153
|
while ((lineEndIndex = buffer.indexOf("\n")) >= 0) {
|
|
88
|
-
|
|
154
|
+
const line = buffer.slice(0, lineEndIndex).trim();
|
|
89
155
|
buffer = buffer.slice(lineEndIndex + 1);
|
|
90
156
|
if (!line) continue;
|
|
91
157
|
if (line.includes("}{")) {
|
|
92
158
|
const parts = line.split("}{");
|
|
93
|
-
await processLine(parts[0] + "}", mcpUrl);
|
|
159
|
+
await processLine(parts[0] + "}", mcpUrl, PARENT_TOOL);
|
|
94
160
|
for (let i = 1; i < parts.length - 1; i++) {
|
|
95
|
-
await processLine("{" + parts[i] + "}", mcpUrl);
|
|
161
|
+
await processLine("{" + parts[i] + "}", mcpUrl, PARENT_TOOL);
|
|
96
162
|
}
|
|
97
|
-
await processLine("{" + parts[parts.length - 1], mcpUrl);
|
|
163
|
+
await processLine("{" + parts[parts.length - 1], mcpUrl, PARENT_TOOL);
|
|
98
164
|
} else {
|
|
99
|
-
await processLine(line, mcpUrl);
|
|
165
|
+
await processLine(line, mcpUrl, PARENT_TOOL);
|
|
100
166
|
}
|
|
101
167
|
}
|
|
102
168
|
});
|
|
@@ -112,14 +178,16 @@ async function mcpCommand() {
|
|
|
112
178
|
process.exit(1);
|
|
113
179
|
}
|
|
114
180
|
}
|
|
115
|
-
async function processLine(line, mcpUrl) {
|
|
116
|
-
if (process.env.DEBUG)
|
|
181
|
+
async function processLine(line, mcpUrl, parentTool) {
|
|
182
|
+
if (process.env.DEBUG)
|
|
183
|
+
process.stderr.write(`[Orkestrate-MCP] Processing line: ${line}
|
|
117
184
|
`);
|
|
118
185
|
let payload;
|
|
119
186
|
try {
|
|
120
187
|
payload = JSON.parse(line);
|
|
121
188
|
} catch {
|
|
122
|
-
if (process.env.DEBUG)
|
|
189
|
+
if (process.env.DEBUG)
|
|
190
|
+
process.stderr.write(`[Orkestrate-MCP] JSON Parse failed for: ${line}
|
|
123
191
|
`);
|
|
124
192
|
return;
|
|
125
193
|
}
|
|
@@ -128,52 +196,105 @@ async function processLine(line, mcpUrl) {
|
|
|
128
196
|
try {
|
|
129
197
|
const token = await getValidToken();
|
|
130
198
|
if (!token) {
|
|
131
|
-
throw new Error(
|
|
199
|
+
throw new Error(
|
|
200
|
+
"NOT_LOGGED_IN: Please run 'orkestrate login' to authenticate."
|
|
201
|
+
);
|
|
132
202
|
}
|
|
203
|
+
const headers = {
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
Authorization: `Bearer ${token}`,
|
|
206
|
+
"User-Agent": "Orkestrate-CLI-Proxy"
|
|
207
|
+
};
|
|
208
|
+
const enrichedPayload = parentTool ? { ...payload, parentTool } : payload;
|
|
133
209
|
const res = await fetch(mcpUrl, {
|
|
134
210
|
method: "POST",
|
|
135
|
-
headers
|
|
136
|
-
|
|
137
|
-
"Authorization": `Bearer ${token}`,
|
|
138
|
-
"User-Agent": "Orkestrate-CLI-Proxy"
|
|
139
|
-
},
|
|
140
|
-
body: line
|
|
211
|
+
headers,
|
|
212
|
+
body: JSON.stringify(enrichedPayload)
|
|
141
213
|
});
|
|
142
214
|
if (isNotification) return;
|
|
143
215
|
const responseBody = await res.text();
|
|
144
216
|
if (!res.ok) {
|
|
145
|
-
process.stderr.write(
|
|
146
|
-
`)
|
|
217
|
+
process.stderr.write(
|
|
218
|
+
`[Orkestrate-MCP] Backend error (${res.status}): ${responseBody}
|
|
219
|
+
`
|
|
220
|
+
);
|
|
147
221
|
if (!isNotification) {
|
|
148
|
-
process.stdout.write(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
222
|
+
process.stdout.write(
|
|
223
|
+
JSON.stringify({
|
|
224
|
+
jsonrpc: "2.0",
|
|
225
|
+
id: requestId,
|
|
226
|
+
error: {
|
|
227
|
+
code: -32603,
|
|
228
|
+
message: `Orkestrate Cloud Error (${res.status}): ${responseBody || "Unauthorized"}. Please try 'orkestrate login'.`
|
|
229
|
+
}
|
|
230
|
+
}) + "\n"
|
|
231
|
+
);
|
|
156
232
|
}
|
|
157
233
|
return;
|
|
158
234
|
}
|
|
159
235
|
if (responseBody) {
|
|
160
|
-
|
|
236
|
+
if (payload.method === "tools/list") {
|
|
237
|
+
const filtered = filterToolsList(responseBody);
|
|
238
|
+
process.stdout.write(filtered + "\n");
|
|
239
|
+
} else {
|
|
240
|
+
if (payload.method === "tools/call") {
|
|
241
|
+
const toolName = payload.params?.name;
|
|
242
|
+
if (toolName && !isToolAllowed(toolName)) {
|
|
243
|
+
process.stdout.write(
|
|
244
|
+
JSON.stringify({
|
|
245
|
+
jsonrpc: "2.0",
|
|
246
|
+
id: requestId,
|
|
247
|
+
error: {
|
|
248
|
+
code: -32600,
|
|
249
|
+
message: `Tool '${toolName}' is disabled. Use 'orkestrate tools --list' to see available tools and 'orkestrate tools --enable ${toolName}' to enable it.`
|
|
250
|
+
}
|
|
251
|
+
}) + "\n"
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
process.stdout.write(responseBody + "\n");
|
|
257
|
+
}
|
|
161
258
|
}
|
|
162
259
|
} catch (err) {
|
|
163
260
|
if (isNotification) return;
|
|
164
|
-
process.stdout.write(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
261
|
+
process.stdout.write(
|
|
262
|
+
JSON.stringify({
|
|
263
|
+
jsonrpc: "2.0",
|
|
264
|
+
id: requestId,
|
|
265
|
+
error: {
|
|
266
|
+
code: -32603,
|
|
267
|
+
message: err instanceof Error ? err.message : String(err)
|
|
268
|
+
}
|
|
269
|
+
}) + "\n"
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function filterToolsList(responseBody) {
|
|
274
|
+
try {
|
|
275
|
+
const parsed = JSON.parse(responseBody);
|
|
276
|
+
if (!parsed.result?.tools) return responseBody;
|
|
277
|
+
const enabledTools = getEnabledTools();
|
|
278
|
+
const disabledTools = getDisabledTools();
|
|
279
|
+
let filteredTools = parsed.result.tools;
|
|
280
|
+
if (enabledTools !== null) {
|
|
281
|
+
filteredTools = filteredTools.filter(
|
|
282
|
+
(tool) => enabledTools.includes(tool.name)
|
|
283
|
+
);
|
|
284
|
+
} else {
|
|
285
|
+
filteredTools = filteredTools.filter(
|
|
286
|
+
(tool) => !disabledTools.includes(tool.name)
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
parsed.result.tools = filteredTools;
|
|
290
|
+
return JSON.stringify(parsed);
|
|
291
|
+
} catch {
|
|
292
|
+
return responseBody;
|
|
172
293
|
}
|
|
173
294
|
}
|
|
174
295
|
|
|
175
296
|
// src/mcp-entry.ts
|
|
176
|
-
mcpCommand().catch((err) => {
|
|
297
|
+
mcpCommand({ parentTool: string }).catch((err) => {
|
|
177
298
|
process.stderr.write(`[Orkestrate-MCP] Fatal: ${err}
|
|
178
299
|
`);
|
|
179
300
|
process.exit(1);
|
package/dist/mcp-entry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/config.ts","../src/lib/auth.ts","../src/commands/mcp.ts","../src/mcp-entry.ts"],"sourcesContent":["/**\n * Orkestrate CLI — Configuration Management\n *\n * Stores credentials and preferences in the user's home directory.\n * Uses the `conf` package for cross-platform config storage.\n */\n\nimport Conf from \"conf\";\n\nexport interface StoredCredentials {\n clientId: string;\n accessToken: string;\n refreshToken: string;\n expiresAt: number; // epoch seconds\n userId: string;\n scope: string;\n githubAccessToken?: string;\n githubRefreshToken?: string;\n githubExpiresAt?: number; // epoch seconds\n}\n\nexport interface CliConfig {\n credentials: StoredCredentials | null;\n activeWorkspaceId: string | null;\n activeWorkspaceName: string | null;\n serverUrl: string;\n}\n\nconst config = new Conf<CliConfig>({\n projectName: \"orkestrate\",\n projectSuffix: \"\",\n defaults: {\n credentials: null,\n activeWorkspaceId: null,\n activeWorkspaceName: null,\n serverUrl: \"https://orkestrate.space\",\n },\n});\n\nexport function getConfig(): CliConfig {\n return {\n credentials: config.get(\"credentials\"),\n activeWorkspaceId: config.get(\"activeWorkspaceId\"),\n activeWorkspaceName: config.get(\"activeWorkspaceName\"),\n serverUrl: config.get(\"serverUrl\"),\n };\n}\n\nexport function getServerUrl(): string {\n return config.get(\"serverUrl\");\n}\n\nexport function setCredentials(creds: StoredCredentials): void {\n config.set(\"credentials\", creds);\n}\n\nexport function getCredentials(): StoredCredentials | null {\n return config.get(\"credentials\");\n}\n\nexport function clearCredentials(): void {\n config.set(\"credentials\", null);\n}\n\nexport function setActiveWorkspace(id: string, name: string): void {\n config.set(\"activeWorkspaceId\", id);\n config.set(\"activeWorkspaceName\", name);\n}\n\nexport function getActiveWorkspace(): {\n id: string | null;\n name: string | null;\n} {\n return {\n id: config.get(\"activeWorkspaceId\"),\n name: config.get(\"activeWorkspaceName\"),\n };\n}\n\nexport function setServerUrl(url: string): void {\n config.set(\"serverUrl\", url);\n}\n\nexport function clearAll(): void {\n config.clear();\n}\n\nexport function getConfigPath(): string {\n return config.path;\n}\n\n// --- GitHub Token Management ---\n\nexport interface GithubTokens {\n accessToken: string;\n refreshToken?: string;\n expiresAt: number; // epoch seconds\n}\n\nexport function setGithubTokens(tokens: GithubTokens): void {\n const creds = getCredentials();\n if (!creds) return;\n\n creds.githubAccessToken = tokens.accessToken;\n creds.githubRefreshToken = tokens.refreshToken;\n creds.githubExpiresAt = tokens.expiresAt;\n\n setCredentials(creds);\n}\n\nexport function getGithubTokens(): GithubTokens | null {\n const creds = getCredentials();\n if (!creds?.githubAccessToken || !creds.githubExpiresAt) return null;\n\n return {\n accessToken: creds.githubAccessToken,\n refreshToken: creds.githubRefreshToken,\n expiresAt: creds.githubExpiresAt,\n };\n}\n\nexport function getValidGithubToken(): string | null {\n const tokens = getGithubTokens();\n if (!tokens) return null;\n\n const now = Math.floor(Date.now() / 1000);\n\n // Consider expired if within 60 seconds of expiry\n if (tokens.expiresAt <= now + 60) return null;\n\n return tokens.accessToken;\n}\n\nexport function hasGithubToken(): boolean {\n return getValidGithubToken() !== null;\n}\n","/**\n * Orkestrate CLI — OAuth Authentication\n *\n * Implements OAuth 2.0 + PKCE flow with two providers:\n * 1. Orkestrate OAuth — identity (openid, profile, email, mcp scopes)\n * 2. GitHub OAuth — repo access (required for workspace creation)\n *\n * Flow:\n * 1. Orkestrate OAuth → Orkestrate identity tokens\n * 2. GitHub OAuth (via Orkestrate proxy) → GitHub access tokens\n * 3. Both stored locally in ~/.config/orkestrate/\n */\n\nimport { createHash, randomBytes } from \"node:crypto\";\nimport {\n createServer,\n type IncomingMessage,\n type ServerResponse,\n} from \"node:http\";\nimport {\n getServerUrl,\n setCredentials,\n getCredentials,\n setGithubTokens,\n type StoredCredentials,\n} from \"./config.js\";\n\n// ─── PKCE Helpers ──────────────────────────────────────────────────────────────\n\nfunction randomToken(bytes = 32): string {\n return randomBytes(bytes).toString(\"base64url\");\n}\n\nfunction pkceS256(verifier: string): string {\n return createHash(\"sha256\").update(verifier).digest(\"base64url\");\n}\n\n// ─── Token Types ───────────────────────────────────────────────────────────────\n\ninterface TokenResponse {\n token_type: string;\n access_token: string;\n expires_in: number;\n refresh_token: string;\n scope: string;\n id_token?: string;\n}\n\n// ─── Dynamic Client Registration ───────────────────────────────────────────────\n\nasync function registerClient(\n serverUrl: string,\n redirectUri: string,\n): Promise<{ clientId: string }> {\n const res = await fetch(`${serverUrl}/api/oauth/register`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n client_name: \"Orkestrate CLI\",\n redirect_uris: [redirectUri],\n grant_types: [\"authorization_code\", \"refresh_token\"],\n response_types: [\"code\"],\n token_endpoint_auth_method: \"none\",\n }),\n });\n\n if (!res.ok) {\n const body = await res.text();\n throw new Error(`Client registration failed (${res.status}): ${body}`);\n }\n\n const data = (await res.json()) as { client_id: string };\n return { clientId: data.client_id };\n}\n\n// ─── Callback Server ───────────────────────────────────────────────────────────\n\nfunction waitForCallback(\n port: number,\n): Promise<{ code: string; state: string }> {\n return new Promise((resolve, reject) => {\n let timeoutHandle: ReturnType<typeof setTimeout>;\n\n function cleanup() {\n clearTimeout(timeoutHandle);\n server.close();\n }\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url || \"/\", `http://localhost:${port}`);\n\n if (url.pathname !== \"/callback\") {\n res.writeHead(404);\n res.end(\"Not found\");\n return;\n }\n\n const code = url.searchParams.get(\"code\");\n const error = url.searchParams.get(\"error\");\n const state = url.searchParams.get(\"state\") || \"\";\n\n if (error) {\n const description = url.searchParams.get(\"error_description\") || error;\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(buildErrorPage(description));\n cleanup();\n reject(new Error(`OAuth error: ${description}`));\n return;\n }\n\n if (!code) {\n res.writeHead(400, { \"Content-Type\": \"text/html\" });\n res.end(buildErrorPage(\"No authorization code received.\"));\n cleanup();\n reject(new Error(\"No authorization code received\"));\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(buildSuccessPage());\n cleanup();\n resolve({ code, state });\n });\n\n server.listen(port, \"127.0.0.1\", () => {\n // ready\n });\n\n server.on(\"error\", (err: Error) => {\n clearTimeout(timeoutHandle);\n reject(new Error(`Could not start local server: ${err.message}`));\n });\n\n // 5-minute timeout — unref so it doesn't block process exit\n timeoutHandle = setTimeout(\n () => {\n server.close();\n reject(\n new Error(\"Authentication timed out (5 minutes). Please try again.\"),\n );\n },\n 5 * 60 * 1000,\n );\n timeoutHandle.unref();\n });\n}\n\n// ─── Token Exchange ────────────────────────────────────────────────────────────\n\nasync function exchangeCodeForTokens(\n serverUrl: string,\n code: string,\n clientId: string,\n codeVerifier: string,\n redirectUri: string,\n): Promise<TokenResponse> {\n const body = new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n code_verifier: codeVerifier,\n client_id: clientId,\n redirect_uri: redirectUri,\n });\n\n const res = await fetch(`${serverUrl}/api/oauth/token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Token exchange failed (${res.status}): ${text}`);\n }\n\n const data = (await res.json()) as TokenResponse;\n if (process.env.DEBUG) {\n console.error(\n `[DEBUG] Orkestrate Token: ${data.access_token?.slice(0, 5)}...`,\n );\n }\n return data;\n}\n\n// ─── GitHub OAuth (proxied through Orkestrate backend, device flow) ────────────\n\ninterface GithubDeviceCodeResponse {\n device_code: string;\n user_code: string;\n verification_uri: string;\n expires_in: number;\n interval: number;\n}\n\ninterface GithubTokenPollResponse {\n access_token?: string;\n refresh_token?: string;\n expires_in?: number;\n error?: string;\n error_description?: string;\n}\n\n/**\n * Start GitHub's Device Flow via Orkestrate proxy.\n * Returns device code + user code for the user to enter at verification_uri.\n */\nasync function startGithubDeviceFlow(\n serverUrl: string,\n accessToken: string,\n): Promise<GithubDeviceCodeResponse> {\n const res = await fetch(`${serverUrl}/api/oauth/github/auth-url`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n },\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(\n `Failed to start GitHub device flow (${res.status}): ${text}`,\n );\n }\n\n return (await res.json()) as GithubDeviceCodeResponse;\n}\n\n/**\n * Poll Orkestrate (which proxies to GitHub) for the GitHub token.\n * Returns null while authorization is still pending.\n * Throws on error or expired code.\n */\nasync function pollGithubToken(\n serverUrl: string,\n accessToken: string,\n userId: string,\n deviceCode: string,\n intervalSeconds: number,\n): Promise<{\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n}> {\n // Add a small buffer to the interval to avoid hitting rate limits\n const pollInterval = (intervalSeconds + 1) * 1000;\n\n while (true) {\n await new Promise((resolve) => setTimeout(resolve, pollInterval));\n\n const res = await fetch(`${serverUrl}/api/oauth/github/token`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ device_code: deviceCode, user_id: userId }),\n });\n\n const data = (await res.json()) as GithubTokenPollResponse;\n\n // Still pending — keep polling\n if (data.error === \"authorization_pending\" || data.error === \"slow_down\") {\n continue;\n }\n\n if (data.error) {\n throw new Error(\n `GitHub authorization failed: ${data.error_description || data.error}`,\n );\n }\n\n if (!data.access_token) {\n throw new Error(\"GitHub returned no access token\");\n }\n\n return {\n access_token: data.access_token,\n refresh_token: data.refresh_token,\n expires_in: data.expires_in ?? 3600,\n };\n }\n}\n\n// ─── Token Refresh ─────────────────────────────────────────────────────────────\n\n/**\n * Refresh the Orkestrate access token using the stored refresh token.\n */\nexport async function refreshAccessToken(): Promise<StoredCredentials | null> {\n const creds = getCredentials();\n if (!creds?.refreshToken) return null;\n\n const serverUrl = getServerUrl();\n const body = new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: creds.refreshToken,\n client_id: creds.clientId,\n });\n\n const res = await fetch(`${serverUrl}/api/oauth/token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n if (!res.ok) return null;\n\n const tokens = (await res.json()) as TokenResponse;\n const now = Math.floor(Date.now() / 1000);\n\n const newCreds: StoredCredentials = {\n clientId: creds.clientId,\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresAt: now + tokens.expires_in,\n userId: creds.userId,\n scope: tokens.scope,\n githubAccessToken: creds.githubAccessToken,\n githubRefreshToken: creds.githubRefreshToken,\n githubExpiresAt: creds.githubExpiresAt,\n };\n\n setCredentials(newCreds);\n return newCreds;\n}\n\n/**\n * Get a valid (non-expired) Orkestrate access token.\n * Automatically refreshes if within 60 seconds of expiry.\n */\nexport async function getValidToken(): Promise<string | null> {\n const creds = getCredentials();\n if (!creds) return null;\n\n const now = Math.floor(Date.now() / 1000);\n\n if (creds.expiresAt <= now + 60) {\n const refreshed = await refreshAccessToken();\n return refreshed?.accessToken || null;\n }\n\n return creds.accessToken;\n}\n\n// ─── Main Login Flow ──────────────────────────────────────────────────────────\n\n/**\n * Run the full Orkestrate + GitHub OAuth login flow.\n *\n * Phase 1: Orkestrate OAuth → identity (stored locally)\n * Phase 2: GitHub OAuth → repo access (proxied through Orkestrate backend)\n *\n * GitHub is best-effort — if it fails, login still succeeds but\n * workspace creation won't work until GitHub is re-connected.\n */\nexport async function performLogin(): Promise<{\n clientId: string;\n userId: string;\n accessToken: string;\n githubConnected: boolean;\n}> {\n const serverUrl = getServerUrl();\n const port = 19274; // \"ork\" on phone keypad, roughly\n const redirectUri = `http://127.0.0.1:${port}/callback`;\n\n // ── Phase 1: Orkestrate OAuth ──────────────────────────────────────────────\n\n const { clientId } = await registerClient(serverUrl, redirectUri);\n\n const codeVerifier = randomToken(48);\n const codeChallenge = pkceS256(codeVerifier);\n const state = randomToken(16);\n\n const authUrl = new URL(`${serverUrl}/api/oauth/authorize`);\n authUrl.searchParams.set(\"response_type\", \"code\");\n authUrl.searchParams.set(\"client_id\", clientId);\n authUrl.searchParams.set(\"redirect_uri\", redirectUri);\n authUrl.searchParams.set(\"code_challenge\", codeChallenge);\n authUrl.searchParams.set(\"code_challenge_method\", \"S256\");\n authUrl.searchParams.set(\"scope\", \"openid profile email mcp:read mcp:write\");\n authUrl.searchParams.set(\"state\", state);\n\n const callbackPromise = waitForCallback(port);\n\n const { default: openBrowser } = await import(\"open\");\n await openBrowser(authUrl.toString());\n\n const { code, state: returnedState } = await callbackPromise;\n\n if (returnedState !== state) {\n throw new Error(\n \"Orkestrate OAuth state mismatch — possible CSRF attack. Aborting.\",\n );\n }\n\n const tokens = await exchangeCodeForTokens(\n serverUrl,\n code,\n clientId,\n codeVerifier,\n redirectUri,\n );\n\n // Resolve userId\n let userId = \"\";\n try {\n const meRes = await fetch(`${serverUrl}/api/auth/me`, {\n headers: { Authorization: `Bearer ${tokens.access_token}` },\n });\n if (meRes.ok) {\n const me = (await meRes.json()) as { id?: string };\n userId = me.id || \"\";\n }\n } catch {\n // Non-fatal\n }\n\n const credentials: StoredCredentials = {\n clientId,\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresAt: Math.floor(Date.now() / 1000) + tokens.expires_in,\n userId,\n scope: tokens.scope,\n };\n\n setCredentials(credentials);\n\n // ── Phase 2: GitHub OAuth (Device Flow) ───────────────────────────────────\n\n let githubConnected = false;\n\n try {\n // Start GitHub device flow via Orkestrate proxy\n const deviceData = await startGithubDeviceFlow(\n serverUrl,\n tokens.access_token,\n );\n\n // Display user code and instructions\n console.error();\n console.error(\" GitHub Device Authorization\");\n console.error();\n console.error(` 1. Open: ${deviceData.verification_uri}`);\n console.error(` 2. Enter: ${deviceData.user_code}`);\n console.error();\n console.error(\" Waiting for authorization... (press Ctrl+C to cancel)\");\n\n // Poll until user completes GitHub authorization\n const githubTokens = await pollGithubToken(\n serverUrl,\n tokens.access_token,\n userId,\n deviceData.device_code,\n deviceData.interval,\n );\n\n setGithubTokens({\n accessToken: githubTokens.access_token,\n refreshToken: githubTokens.refresh_token,\n expiresAt:\n Math.floor(Date.now() / 1000) + (githubTokens.expires_in ?? 3600),\n });\n\n githubConnected = true;\n } catch (err) {\n // Best-effort — warn but don't fail the login\n console.error(\n `[Login] GitHub connection failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n console.error(\"[Login] Workspace creation requires GitHub access.\");\n console.error(\n \"[Login] Re-run \\`orkestrate login --github\\` to connect GitHub later.\",\n );\n }\n\n return {\n clientId,\n userId: credentials.userId,\n accessToken: credentials.accessToken,\n githubConnected,\n };\n}\n\n// ─── HTML Pages ───────────────────────────────────────────────────────────────\n\nfunction buildSuccessPage(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <title>Orkestrate — Authenticated</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #0a0a0a;\n color: #e5e5e5;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n text-align: center;\n padding: 3rem;\n border: 1px solid #262626;\n border-radius: 12px;\n background: #111;\n max-width: 420px;\n }\n .icon { font-size: 3rem; margin-bottom: 1rem; }\n h1 { font-size: 1.5rem; margin-bottom: 0.5rem; color: #fff; }\n p { color: #a3a3a3; line-height: 1.6; }\n .hint { margin-top: 1.5rem; font-size: 0.85rem; color: #525252; }\n </style>\n</head>\n<body>\n <div class=\"card\">\n <div class=\"icon\">✓</div>\n <h1>Authenticated</h1>\n <p>You're now logged in to Orkestrate. You can close this tab and return to your terminal.</p>\n <p class=\"hint\">This window will close automatically.</p>\n </div>\n <script>setTimeout(() => window.close(), 3000);</script>\n</body>\n</html>`;\n}\n\nfunction buildErrorPage(message: string): string {\n const escaped = message.replace(/</g, \"<\").replace(/>/g, \">\");\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <title>Orkestrate — Error</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #0a0a0a;\n color: #e5e5e5;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n text-align: center;\n padding: 3rem;\n border: 1px solid #371717;\n border-radius: 12px;\n background: #1a0a0a;\n max-width: 420px;\n }\n .icon { font-size: 3rem; margin-bottom: 1rem; }\n h1 { font-size: 1.5rem; margin-bottom: 0.5rem; color: #fca5a5; }\n p { color: #a3a3a3; line-height: 1.6; }\n </style>\n</head>\n<body>\n <div class=\"card\">\n <div class=\"icon\">✗</div>\n <h1>Authentication Failed</h1>\n <p>${escaped}</p>\n </div>\n</body>\n</html>`;\n}\n","\nimport { getServerUrl } from \"../lib/config.js\";\nimport { getValidToken } from \"../lib/auth.js\";\n\n/**\n * Orkestrate MCP Proxy\n * \n * Acts as a local MCP server (stdio) that forwards all requests to the \n * Orkestrate cloud MCP endpoint (HTTP), injecting the local auth token.\n */\nexport async function mcpCommand() {\n try {\n const serverUrl = getServerUrl();\n const mcpUrl = `${serverUrl}/api/mcp`;\n\n process.stdin.setEncoding(\"utf-8\");\n let buffer = \"\";\n\n process.stderr.write(`[Orkestrate-MCP] Starting bridge to ${mcpUrl}\\n`);\n\n process.stdin.on(\"data\", async (chunk) => {\n const rawChunk = String(chunk);\n if (process.env.DEBUG) process.stderr.write(`[Orkestrate-MCP] Received chunk: ${rawChunk}\\n`);\n buffer += rawChunk;\n \n let lineEndIndex;\n while ((lineEndIndex = buffer.indexOf(\"\\n\")) >= 0) {\n let line = buffer.slice(0, lineEndIndex).trim();\n buffer = buffer.slice(lineEndIndex + 1);\n \n if (!line) continue;\n \n // Handle back-to-back JSON objects missing newlines\n if (line.includes(\"}{\")) {\n const parts = line.split(\"}{\");\n await processLine(parts[0] + \"}\", mcpUrl);\n for (let i = 1; i < parts.length - 1; i++) {\n await processLine(\"{\" + parts[i] + \"}\", mcpUrl);\n }\n await processLine(\"{\" + parts[parts.length - 1], mcpUrl);\n } else {\n await processLine(line, mcpUrl);\n }\n }\n });\n\n process.stdin.on(\"end\", async () => {\n // Small delay to ensure last processing finishes\n await new Promise(r => setTimeout(r, 100));\n process.exit(0);\n });\n\n // Stay alive\n await new Promise(() => {});\n } catch (err) {\n process.stderr.write(`[Orkestrate] Fatal error: ${err}\\n`);\n process.exit(1);\n }\n}\n\nasync function processLine(line: string, mcpUrl: string) {\n if (process.env.DEBUG) process.stderr.write(`[Orkestrate-MCP] Processing line: ${line}\\n`);\n let payload: any;\n try {\n payload = JSON.parse(line);\n } catch { \n if (process.env.DEBUG) process.stderr.write(`[Orkestrate-MCP] JSON Parse failed for: ${line}\\n`);\n return; \n }\n\n const isNotification = !Object.prototype.hasOwnProperty.call(payload, \"id\");\n const requestId = payload.id;\n\n try {\n const token = await getValidToken();\n \n if (!token) {\n throw new Error(\"NOT_LOGGED_IN: Please run 'orkestrate login' to authenticate.\");\n }\n\n const res = await fetch(mcpUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${token}`,\n \"User-Agent\": \"Orkestrate-CLI-Proxy\"\n },\n body: line\n });\n\n // Notifications MUST NOT be responded to on stdout\n if (isNotification) return;\n\n const responseBody = await res.text();\n if (!res.ok) {\n process.stderr.write(`[Orkestrate-MCP] Backend error (${res.status}): ${responseBody}\\n`);\n if (!isNotification) {\n process.stdout.write(JSON.stringify({\n jsonrpc: \"2.0\",\n id: requestId,\n error: {\n code: -32603,\n message: `Orkestrate Cloud Error (${res.status}): ${responseBody || \"Unauthorized\"}. Please try 'orkestrate login'.`\n }\n }) + \"\\n\");\n }\n return;\n }\n \n if (responseBody) {\n process.stdout.write(responseBody + \"\\n\");\n }\n } catch (err) {\n if (isNotification) return;\n\n process.stdout.write(JSON.stringify({\n jsonrpc: \"2.0\",\n id: requestId,\n error: {\n code: -32603,\n message: err instanceof Error ? err.message : String(err)\n }\n }) + \"\\n\");\n }\n}\n","\nimport { mcpCommand } from \"./commands/mcp.js\";\n\n// Standalone MCP bridge entry point. \n// DO NOT PRINT TO STDOUT ANY NON-JSON DATA.\nmcpCommand().catch(err => {\n process.stderr.write(`[Orkestrate-MCP] Fatal: ${err}\\n`);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/lib/config.ts","../src/lib/auth.ts","../src/commands/mcp.ts","../src/mcp-entry.ts"],"sourcesContent":["/**\n * Orkestrate CLI — Configuration Management\n *\n * Stores credentials and preferences in the user's home directory.\n * Uses the `conf` package for cross-platform config storage.\n */\n\nimport Conf from \"conf\";\nimport { readFileSync, writeFileSync, existsSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { mkdirSync } from \"node:fs\";\n\nexport interface UserToolSettings {\n enabledTools?: string[] | null; // null = all enabled\n disabledTools?: string[]; // explicit disable list\n}\n\nexport interface CliConfig {\n credentials: StoredCredentials | null;\n activeWorkspaceId: string | null;\n activeWorkspaceName: string | null;\n serverUrl: string;\n userToolSettings?: UserToolSettings;\n}\n\nexport interface StoredCredentials {\n clientId: string;\n accessToken: string;\n refreshToken: string;\n expiresAt: number; // epoch seconds\n userId: string;\n scope: string;\n githubAccessToken?: string;\n githubRefreshToken?: string;\n githubExpiresAt?: number; // epoch seconds\n}\n\nconst config = new Conf<CliConfig>({\n projectName: \"orkestrate\",\n projectSuffix: \"\",\n defaults: {\n credentials: null,\n activeWorkspaceId: null,\n activeWorkspaceName: null,\n serverUrl: \"https://orkestrate.space\",\n userToolSettings: {},\n },\n});\n\nexport function getConfig(): CliConfig {\n return {\n credentials: config.get(\"credentials\"),\n activeWorkspaceId: config.get(\"activeWorkspaceId\"),\n activeWorkspaceName: config.get(\"activeWorkspaceName\"),\n serverUrl: config.get(\"serverUrl\"),\n userToolSettings: config.get(\"userToolSettings\"),\n };\n}\n\nexport function getServerUrl(): string {\n return config.get(\"serverUrl\");\n}\n\n// Helper for config writes with retry and exponential backoff\nfunction setConfigWithRetry(\n key: string,\n value: unknown,\n maxAttempts = 5,\n): void {\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n config.set(key, value);\n return;\n } catch (err: any) {\n // Only retry on EPERM/EBUSY (Windows file locking)\n if (err?.code !== \"EPERM\" && err?.code !== \"EBUSY\") {\n throw err;\n }\n // Exponential backoff: 50ms, 100ms, 200ms, 400ms, 800ms\n const delay = 50 * Math.pow(2, attempt);\n if (attempt < maxAttempts - 1) {\n const start = Date.now();\n while (Date.now() - start < delay) {\n // busy wait\n }\n }\n }\n }\n\n // Fallback: direct file write bypassing conf's atomic operations\n try {\n const configPath = config.path;\n const dir = dirname(configPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n let currentConfig: Record<string, unknown> = {};\n if (existsSync(configPath)) {\n try {\n currentConfig = JSON.parse(readFileSync(configPath, \"utf-8\"));\n } catch {\n currentConfig = {};\n }\n }\n currentConfig[key] = value;\n writeFileSync(configPath, JSON.stringify(currentConfig, null, 2), \"utf-8\");\n return;\n } catch (fallbackErr) {\n throw new Error(\n `Failed to save config after ${maxAttempts} attempts. Original error: ${fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)}`,\n );\n }\n}\n\nexport function setCredentials(creds: StoredCredentials): void {\n setConfigWithRetry(\"credentials\", creds);\n}\n\nexport function getCredentials(): StoredCredentials | null {\n return config.get(\"credentials\");\n}\n\nexport function clearCredentials(): void {\n setConfigWithRetry(\"credentials\", null);\n}\n\nexport function setActiveWorkspace(id: string, name: string): void {\n setConfigWithRetry(\"activeWorkspaceId\", id);\n setConfigWithRetry(\"activeWorkspaceName\", name);\n}\n\nexport function getActiveWorkspace(): {\n id: string | null;\n name: string | null;\n} {\n return {\n id: config.get(\"activeWorkspaceId\"),\n name: config.get(\"activeWorkspaceName\"),\n };\n}\n\nexport function setServerUrl(url: string): void {\n setConfigWithRetry(\"serverUrl\", url);\n}\n\nexport function clearAll(): void {\n config.clear();\n}\n\nexport function getConfigPath(): string {\n return config.path;\n}\n\n// --- User Tool Settings Management ---\n\nexport function getUserToolSettings(): UserToolSettings {\n return config.get(\"userToolSettings\") || {};\n}\n\nexport function setUserToolSettings(settings: UserToolSettings): void {\n setConfigWithRetry(\"userToolSettings\", settings);\n}\n\nexport function getEnabledTools(): string[] | null {\n const settings = getUserToolSettings();\n return settings.enabledTools ?? null;\n}\n\nexport function getDisabledTools(): string[] {\n const settings = getUserToolSettings();\n return settings.disabledTools || [];\n}\n\nexport function setEnabledTools(tools: string[] | null): void {\n const settings = getUserToolSettings();\n settings.enabledTools = tools;\n setUserToolSettings(settings);\n}\n\nexport function setDisabledTools(tools: string[]): void {\n const settings = getUserToolSettings();\n settings.disabledTools = tools;\n setUserToolSettings(settings);\n}\n\nexport function isToolAllowed(toolName: string): boolean {\n const enabledTools = getEnabledTools();\n const disabledTools = getDisabledTools();\n\n // If enabledTools is set (not null), only those tools are allowed\n if (enabledTools !== null) {\n return enabledTools.includes(toolName);\n }\n\n // Otherwise, check disabledTools list\n return !disabledTools.includes(toolName);\n}\n\nexport function enableTool(toolName: string): void {\n const enabledTools = getEnabledTools();\n const disabledTools = getDisabledTools();\n\n if (enabledTools !== null) {\n // Additive mode: add to enabled list if not present\n if (!enabledTools.includes(toolName)) {\n setEnabledTools([...enabledTools, toolName]);\n }\n } else {\n // Subtractive mode: remove from disabled list\n if (disabledTools.includes(toolName)) {\n setDisabledTools(disabledTools.filter((t) => t !== toolName));\n }\n }\n}\n\nexport function disableTool(toolName: string): void {\n const enabledTools = getEnabledTools();\n const disabledTools = getDisabledTools();\n\n if (enabledTools !== null) {\n // Additive mode: remove from enabled list\n if (enabledTools.includes(toolName)) {\n setEnabledTools(enabledTools.filter((t) => t !== toolName));\n }\n } else {\n // Subtractive mode: add to disabled list\n if (!disabledTools.includes(toolName)) {\n setDisabledTools([...disabledTools, toolName]);\n }\n }\n}\n\n// --- GitHub Token Management ---\n\nexport interface GithubTokens {\n accessToken: string;\n refreshToken?: string;\n expiresAt: number; // epoch seconds\n}\n\nexport function setGithubTokens(tokens: GithubTokens): void {\n const creds = getCredentials();\n if (!creds) return;\n\n creds.githubAccessToken = tokens.accessToken;\n creds.githubRefreshToken = tokens.refreshToken;\n creds.githubExpiresAt = tokens.expiresAt;\n\n setCredentials(creds);\n}\n\nexport function getGithubTokens(): GithubTokens | null {\n const creds = getCredentials();\n if (!creds?.githubAccessToken || !creds.githubExpiresAt) return null;\n\n return {\n accessToken: creds.githubAccessToken,\n refreshToken: creds.githubRefreshToken,\n expiresAt: creds.githubExpiresAt,\n };\n}\n\nexport function getValidGithubToken(): string | null {\n const tokens = getGithubTokens();\n if (!tokens) return null;\n\n const now = Math.floor(Date.now() / 1000);\n\n // Consider expired if within 60 seconds of expiry\n if (tokens.expiresAt <= now + 60) return null;\n\n return tokens.accessToken;\n}\n\nexport function hasGithubToken(): boolean {\n return getValidGithubToken() !== null;\n}\n","/**\n * Orkestrate CLI — OAuth Authentication\n *\n * Implements OAuth 2.0 + PKCE flow with two providers:\n * 1. Orkestrate OAuth — identity (openid, profile, email, mcp scopes)\n * 2. GitHub OAuth — repo access (required for workspace creation)\n *\n * Flow:\n * 1. Orkestrate OAuth → Orkestrate identity tokens\n * 2. GitHub OAuth (via Orkestrate proxy) → GitHub access tokens\n * 3. Both stored locally in ~/.config/orkestrate/\n */\n\nimport { createHash, randomBytes } from \"node:crypto\";\nimport {\n createServer,\n type IncomingMessage,\n type ServerResponse,\n} from \"node:http\";\nimport {\n getServerUrl,\n setCredentials,\n getCredentials,\n setGithubTokens,\n type StoredCredentials,\n} from \"./config.js\";\n\n// ─── PKCE Helpers ──────────────────────────────────────────────────────────────\n\nfunction randomToken(bytes = 32): string {\n return randomBytes(bytes).toString(\"base64url\");\n}\n\nfunction pkceS256(verifier: string): string {\n return createHash(\"sha256\").update(verifier).digest(\"base64url\");\n}\n\n// ─── Token Types ───────────────────────────────────────────────────────────────\n\ninterface TokenResponse {\n token_type: string;\n access_token: string;\n expires_in: number;\n refresh_token: string;\n scope: string;\n id_token?: string;\n}\n\n// ─── Dynamic Client Registration ───────────────────────────────────────────────\n\nasync function registerClient(\n serverUrl: string,\n redirectUri: string,\n): Promise<{ clientId: string }> {\n const res = await fetch(`${serverUrl}/api/oauth/register`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n client_name: \"Orkestrate CLI\",\n redirect_uris: [redirectUri],\n grant_types: [\"authorization_code\", \"refresh_token\"],\n response_types: [\"code\"],\n token_endpoint_auth_method: \"none\",\n }),\n });\n\n if (!res.ok) {\n const body = await res.text();\n throw new Error(`Client registration failed (${res.status}): ${body}`);\n }\n\n const data = (await res.json()) as { client_id: string };\n return { clientId: data.client_id };\n}\n\n// ─── Callback Server ───────────────────────────────────────────────────────────\n\nfunction waitForCallback(\n port: number,\n): Promise<{ code: string; state: string }> {\n return new Promise((resolve, reject) => {\n let timeoutHandle: ReturnType<typeof setTimeout>;\n\n function cleanup() {\n clearTimeout(timeoutHandle);\n server.close();\n }\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url || \"/\", `http://localhost:${port}`);\n\n if (url.pathname !== \"/callback\") {\n res.writeHead(404);\n res.end(\"Not found\");\n return;\n }\n\n const code = url.searchParams.get(\"code\");\n const error = url.searchParams.get(\"error\");\n const state = url.searchParams.get(\"state\") || \"\";\n\n if (error) {\n const description = url.searchParams.get(\"error_description\") || error;\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(buildErrorPage(description));\n cleanup();\n reject(new Error(`OAuth error: ${description}`));\n return;\n }\n\n if (!code) {\n res.writeHead(400, { \"Content-Type\": \"text/html\" });\n res.end(buildErrorPage(\"No authorization code received.\"));\n cleanup();\n reject(new Error(\"No authorization code received\"));\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(buildSuccessPage());\n cleanup();\n resolve({ code, state });\n });\n\n server.listen(port, \"127.0.0.1\", () => {\n // ready\n });\n\n server.on(\"error\", (err: Error) => {\n clearTimeout(timeoutHandle);\n reject(new Error(`Could not start local server: ${err.message}`));\n });\n\n // 5-minute timeout — unref so it doesn't block process exit\n timeoutHandle = setTimeout(\n () => {\n server.close();\n reject(\n new Error(\"Authentication timed out (5 minutes). Please try again.\"),\n );\n },\n 5 * 60 * 1000,\n );\n timeoutHandle.unref();\n });\n}\n\n// ─── Token Exchange ────────────────────────────────────────────────────────────\n\nasync function exchangeCodeForTokens(\n serverUrl: string,\n code: string,\n clientId: string,\n codeVerifier: string,\n redirectUri: string,\n): Promise<TokenResponse> {\n const body = new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n code_verifier: codeVerifier,\n client_id: clientId,\n redirect_uri: redirectUri,\n });\n\n const res = await fetch(`${serverUrl}/api/oauth/token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(`Token exchange failed (${res.status}): ${text}`);\n }\n\n const data = (await res.json()) as TokenResponse;\n if (process.env.DEBUG) {\n console.error(\n `[DEBUG] Orkestrate Token: ${data.access_token?.slice(0, 5)}...`,\n );\n }\n return data;\n}\n\n// ─── GitHub OAuth (proxied through Orkestrate backend, device flow) ────────────\n\ninterface GithubDeviceCodeResponse {\n device_code: string;\n user_code: string;\n verification_uri: string;\n expires_in: number;\n interval: number;\n}\n\ninterface GithubTokenPollResponse {\n access_token?: string;\n refresh_token?: string;\n expires_in?: number;\n error?: string;\n error_description?: string;\n}\n\n/**\n * Start GitHub's Device Flow via Orkestrate proxy.\n * Returns device code + user code for the user to enter at verification_uri.\n */\nasync function startGithubDeviceFlow(\n serverUrl: string,\n accessToken: string,\n): Promise<GithubDeviceCodeResponse> {\n const res = await fetch(`${serverUrl}/api/oauth/github/auth-url`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n },\n });\n\n if (!res.ok) {\n const text = await res.text();\n throw new Error(\n `Failed to start GitHub device flow (${res.status}): ${text}`,\n );\n }\n\n return (await res.json()) as GithubDeviceCodeResponse;\n}\n\n/**\n * Poll Orkestrate (which proxies to GitHub) for the GitHub token.\n * Returns null while authorization is still pending.\n * Throws on error or expired code.\n */\nasync function pollGithubToken(\n serverUrl: string,\n accessToken: string,\n userId: string,\n deviceCode: string,\n intervalSeconds: number,\n): Promise<{\n access_token: string;\n refresh_token?: string;\n expires_in: number;\n}> {\n // Add a small buffer to the interval to avoid hitting rate limits\n const pollInterval = (intervalSeconds + 1) * 1000;\n\n while (true) {\n await new Promise((resolve) => setTimeout(resolve, pollInterval));\n\n const res = await fetch(`${serverUrl}/api/oauth/github/token`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ device_code: deviceCode, user_id: userId }),\n });\n\n const data = (await res.json()) as GithubTokenPollResponse;\n\n // Still pending — keep polling\n if (data.error === \"authorization_pending\" || data.error === \"slow_down\") {\n continue;\n }\n\n if (data.error) {\n throw new Error(\n `GitHub authorization failed: ${data.error_description || data.error}`,\n );\n }\n\n if (!data.access_token) {\n throw new Error(\"GitHub returned no access token\");\n }\n\n return {\n access_token: data.access_token,\n refresh_token: data.refresh_token,\n expires_in: data.expires_in ?? 3600,\n };\n }\n}\n\n// ─── Token Refresh ─────────────────────────────────────────────────────────────\n\n/**\n * Refresh the Orkestrate access token using the stored refresh token.\n */\nexport async function refreshAccessToken(): Promise<StoredCredentials | null> {\n const creds = getCredentials();\n if (!creds?.refreshToken) return null;\n\n const serverUrl = getServerUrl();\n const body = new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: creds.refreshToken,\n client_id: creds.clientId,\n });\n\n const res = await fetch(`${serverUrl}/api/oauth/token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n\n if (!res.ok) return null;\n\n const tokens = (await res.json()) as TokenResponse;\n const now = Math.floor(Date.now() / 1000);\n\n const newCreds: StoredCredentials = {\n clientId: creds.clientId,\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresAt: now + tokens.expires_in,\n userId: creds.userId,\n scope: tokens.scope,\n githubAccessToken: creds.githubAccessToken,\n githubRefreshToken: creds.githubRefreshToken,\n githubExpiresAt: creds.githubExpiresAt,\n };\n\n setCredentials(newCreds);\n return newCreds;\n}\n\n/**\n * Get a valid (non-expired) Orkestrate access token.\n * Automatically refreshes if within 60 seconds of expiry.\n */\nexport async function getValidToken(): Promise<string | null> {\n const creds = getCredentials();\n if (!creds) return null;\n\n const now = Math.floor(Date.now() / 1000);\n\n if (creds.expiresAt <= now + 60) {\n const refreshed = await refreshAccessToken();\n return refreshed?.accessToken || null;\n }\n\n return creds.accessToken;\n}\n\n// ─── Main Login Flow ──────────────────────────────────────────────────────────\n\n/**\n * Run the full Orkestrate + GitHub OAuth login flow.\n *\n * Phase 1: Orkestrate OAuth → identity (stored locally)\n * Phase 2: GitHub OAuth → repo access (proxied through Orkestrate backend)\n *\n * GitHub is best-effort — if it fails, login still succeeds but\n * workspace creation won't work until GitHub is re-connected.\n */\nexport async function performLogin(): Promise<{\n clientId: string;\n userId: string;\n accessToken: string;\n githubConnected: boolean;\n}> {\n const serverUrl = getServerUrl();\n const port = 19274; // \"ork\" on phone keypad, roughly\n const redirectUri = `http://127.0.0.1:${port}/callback`;\n\n // ── Phase 1: Orkestrate OAuth ──────────────────────────────────────────────\n\n const { clientId } = await registerClient(serverUrl, redirectUri);\n\n const codeVerifier = randomToken(48);\n const codeChallenge = pkceS256(codeVerifier);\n const state = randomToken(16);\n\n const authUrl = new URL(`${serverUrl}/api/oauth/authorize`);\n authUrl.searchParams.set(\"response_type\", \"code\");\n authUrl.searchParams.set(\"client_id\", clientId);\n authUrl.searchParams.set(\"redirect_uri\", redirectUri);\n authUrl.searchParams.set(\"code_challenge\", codeChallenge);\n authUrl.searchParams.set(\"code_challenge_method\", \"S256\");\n authUrl.searchParams.set(\"scope\", \"openid profile email mcp:read mcp:write\");\n authUrl.searchParams.set(\"state\", state);\n\n const callbackPromise = waitForCallback(port);\n\n const { default: openBrowser } = await import(\"open\");\n await openBrowser(authUrl.toString());\n\n const { code, state: returnedState } = await callbackPromise;\n\n if (returnedState !== state) {\n throw new Error(\n \"Orkestrate OAuth state mismatch — possible CSRF attack. Aborting.\",\n );\n }\n\n const tokens = await exchangeCodeForTokens(\n serverUrl,\n code,\n clientId,\n codeVerifier,\n redirectUri,\n );\n\n // Resolve userId\n let userId = \"\";\n try {\n const meRes = await fetch(`${serverUrl}/api/auth/me`, {\n headers: { Authorization: `Bearer ${tokens.access_token}` },\n });\n if (meRes.ok) {\n const me = (await meRes.json()) as {\n id?: string;\n user?: { id?: string };\n };\n userId = me.user?.id || me.id || \"\";\n }\n } catch {\n // Non-fatal\n }\n\n const credentials: StoredCredentials = {\n clientId,\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresAt: Math.floor(Date.now() / 1000) + tokens.expires_in,\n userId,\n scope: tokens.scope,\n };\n\n setCredentials(credentials);\n\n // ── Phase 2: GitHub OAuth (Device Flow) ───────────────────────────────────\n\n let githubConnected = false;\n\n try {\n // Start GitHub device flow via Orkestrate proxy\n const deviceData = await startGithubDeviceFlow(\n serverUrl,\n tokens.access_token,\n );\n\n // Display user code and instructions\n console.error();\n console.error(\" GitHub Device Authorization\");\n console.error();\n console.error(` 1. Open: ${deviceData.verification_uri}`);\n console.error(` 2. Enter: ${deviceData.user_code}`);\n console.error();\n console.error(\" Waiting for authorization... (press Ctrl+C to cancel)\");\n\n // Poll until user completes GitHub authorization\n const githubTokens = await pollGithubToken(\n serverUrl,\n tokens.access_token,\n userId,\n deviceData.device_code,\n deviceData.interval,\n );\n\n setGithubTokens({\n accessToken: githubTokens.access_token,\n refreshToken: githubTokens.refresh_token,\n expiresAt:\n Math.floor(Date.now() / 1000) + (githubTokens.expires_in ?? 3600),\n });\n\n githubConnected = true;\n } catch (err) {\n // Best-effort — warn but don't fail the login\n console.error(\n `[Login] GitHub connection failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n console.error(\"[Login] Workspace creation requires GitHub access.\");\n console.error(\n \"[Login] Re-run \\`orkestrate login --github\\` to connect GitHub later.\",\n );\n }\n\n return {\n clientId,\n userId: credentials.userId,\n accessToken: credentials.accessToken,\n githubConnected,\n };\n}\n\n// ─── HTML Pages ───────────────────────────────────────────────────────────────\n\nfunction buildSuccessPage(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <title>Orkestrate — Authenticated</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #0a0a0a;\n color: #e5e5e5;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n text-align: center;\n padding: 3rem;\n border: 1px solid #262626;\n border-radius: 12px;\n background: #111;\n max-width: 420px;\n }\n .icon { font-size: 3rem; margin-bottom: 1rem; }\n h1 { font-size: 1.5rem; margin-bottom: 0.5rem; color: #fff; }\n p { color: #a3a3a3; line-height: 1.6; }\n .hint { margin-top: 1.5rem; font-size: 0.85rem; color: #525252; }\n </style>\n</head>\n<body>\n <div class=\"card\">\n <div class=\"icon\">✓</div>\n <h1>Authenticated</h1>\n <p>You're now logged in to Orkestrate. You can close this tab and return to your terminal.</p>\n <p class=\"hint\">This window will close automatically.</p>\n </div>\n <script>setTimeout(() => window.close(), 3000);</script>\n</body>\n</html>`;\n}\n\nfunction buildErrorPage(message: string): string {\n const escaped = message.replace(/</g, \"<\").replace(/>/g, \">\");\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <title>Orkestrate — Error</title>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #0a0a0a;\n color: #e5e5e5;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n text-align: center;\n padding: 3rem;\n border: 1px solid #371717;\n border-radius: 12px;\n background: #1a0a0a;\n max-width: 420px;\n }\n .icon { font-size: 3rem; margin-bottom: 1rem; }\n h1 { font-size: 1.5rem; margin-bottom: 0.5rem; color: #fca5a5; }\n p { color: #a3a3a3; line-height: 1.6; }\n </style>\n</head>\n<body>\n <div class=\"card\">\n <div class=\"icon\">✗</div>\n <h1>Authentication Failed</h1>\n <p>${escaped}</p>\n </div>\n</body>\n</html>`;\n}\n","import {\n getServerUrl,\n isToolAllowed,\n getEnabledTools,\n getDisabledTools,\n} from \"../lib/config.js\";\nimport { getValidToken } from \"../lib/auth.js\";\n\n/**\n * Orkestrate MCP Proxy\n *\n * Acts as a local MCP server (stdio) that forwards all requests to the\n * Orkestrate cloud MCP endpoint (HTTP), injecting the local auth token.\n */\n\nexport async function mcpCommand(opts: { parentTool?: string }) {\n const PARENT_TOOL = opts.parentTool ?? null;\n try {\n const serverUrl = getServerUrl();\n const mcpUrl = `${serverUrl}/api/mcp`;\n\n process.stdin.setEncoding(\"utf-8\");\n let buffer = \"\";\n\n const startupMsg = `[Orkestrate-MCP] Starting bridge to ${mcpUrl}${PARENT_TOOL ? ` (parent: ${PARENT_TOOL})` : \"\"}\\n`;\n process.stderr.write(startupMsg);\n\n process.stdin.on(\"data\", async (chunk) => {\n const rawChunk = String(chunk);\n if (process.env.DEBUG)\n process.stderr.write(`[Orkestrate-MCP] Received chunk: ${rawChunk}\\n`);\n buffer += rawChunk;\n\n let lineEndIndex;\n while ((lineEndIndex = buffer.indexOf(\"\\n\")) >= 0) {\n const line = buffer.slice(0, lineEndIndex).trim();\n buffer = buffer.slice(lineEndIndex + 1);\n\n if (!line) continue;\n\n // Handle back-to-back JSON objects missing newlines\n if (line.includes(\"}{\")) {\n const parts = line.split(\"}{\");\n await processLine(parts[0] + \"}\", mcpUrl, PARENT_TOOL);\n for (let i = 1; i < parts.length - 1; i++) {\n await processLine(\"{\" + parts[i] + \"}\", mcpUrl, PARENT_TOOL);\n }\n await processLine(\"{\" + parts[parts.length - 1], mcpUrl, PARENT_TOOL);\n } else {\n await processLine(line, mcpUrl, PARENT_TOOL);\n }\n }\n });\n\n process.stdin.on(\"end\", async () => {\n // Small delay to ensure last processing finishes\n await new Promise((r) => setTimeout(r, 100));\n process.exit(0);\n });\n\n // Stay alive\n await new Promise(() => {});\n } catch (err) {\n process.stderr.write(`[Orkestrate] Fatal error: ${err}\\n`);\n process.exit(1);\n }\n}\n\nasync function processLine(\n line: string,\n mcpUrl: string,\n parentTool: string | null,\n) {\n if (process.env.DEBUG)\n process.stderr.write(`[Orkestrate-MCP] Processing line: ${line}\\n`);\n let payload: any;\n try {\n payload = JSON.parse(line);\n } catch {\n if (process.env.DEBUG)\n process.stderr.write(`[Orkestrate-MCP] JSON Parse failed for: ${line}\\n`);\n return;\n }\n\n const isNotification = !Object.prototype.hasOwnProperty.call(payload, \"id\");\n const requestId = payload.id;\n\n try {\n const token = await getValidToken();\n\n if (!token) {\n throw new Error(\n \"NOT_LOGGED_IN: Please run 'orkestrate login' to authenticate.\",\n );\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n \"User-Agent\": \"Orkestrate-CLI-Proxy\",\n };\n\n // Inject parentTool into the payload so the backend can use it as the agent family\n const enrichedPayload = parentTool\n ? { ...payload, parentTool: parentTool }\n : payload;\n\n const res = await fetch(mcpUrl, {\n method: \"POST\",\n headers,\n body: JSON.stringify(enrichedPayload),\n });\n\n // Notifications MUST NOT be responded to on stdout\n if (isNotification) return;\n\n const responseBody = await res.text();\n if (!res.ok) {\n process.stderr.write(\n `[Orkestrate-MCP] Backend error (${res.status}): ${responseBody}\\n`,\n );\n if (!isNotification) {\n process.stdout.write(\n JSON.stringify({\n jsonrpc: \"2.0\",\n id: requestId,\n error: {\n code: -32603,\n message: `Orkestrate Cloud Error (${res.status}): ${responseBody || \"Unauthorized\"}. Please try 'orkestrate login'.`,\n },\n }) + \"\\n\",\n );\n }\n return;\n }\n\n if (responseBody) {\n // Handle tools/list: filter tools based on user settings\n if (payload.method === \"tools/list\") {\n const filtered = filterToolsList(responseBody);\n process.stdout.write(filtered + \"\\n\");\n } else {\n // Handle tools/call: check if tool is allowed\n if (payload.method === \"tools/call\") {\n const toolName = payload.params?.name;\n if (toolName && !isToolAllowed(toolName)) {\n process.stdout.write(\n JSON.stringify({\n jsonrpc: \"2.0\",\n id: requestId,\n error: {\n code: -32600,\n message: `Tool '${toolName}' is disabled. Use 'orkestrate tools --list' to see available tools and 'orkestrate tools --enable ${toolName}' to enable it.`,\n },\n }) + \"\\n\",\n );\n return;\n }\n }\n process.stdout.write(responseBody + \"\\n\");\n }\n }\n } catch (err) {\n if (isNotification) return;\n\n process.stdout.write(\n JSON.stringify({\n jsonrpc: \"2.0\",\n id: requestId,\n error: {\n code: -32603,\n message: err instanceof Error ? err.message : String(err),\n },\n }) + \"\\n\",\n );\n }\n}\n\n/**\n * Filter tools/list response based on user tool settings\n */\nfunction filterToolsList(responseBody: string): string {\n try {\n const parsed = JSON.parse(responseBody);\n if (!parsed.result?.tools) return responseBody;\n\n const enabledTools = getEnabledTools();\n const disabledTools = getDisabledTools();\n\n let filteredTools = parsed.result.tools;\n\n if (enabledTools !== null) {\n // Additive mode: only include enabled tools\n filteredTools = filteredTools.filter((tool: any) =>\n enabledTools.includes(tool.name),\n );\n } else {\n // Subtractive mode: exclude disabled tools\n filteredTools = filteredTools.filter(\n (tool: any) => !disabledTools.includes(tool.name),\n );\n }\n\n parsed.result.tools = filteredTools;\n return JSON.stringify(parsed);\n } catch {\n // If parsing fails, return original response\n return responseBody;\n }\n}\n","import { mcpCommand } from \"./commands/mcp.js\";\n\n// Standalone MCP bridge entry point.\n// DO NOT PRINT TO STDOUT ANY NON-JSON DATA.\nmcpCommand({ parentTool: string }).catch((err) => {\n process.stderr.write(`[Orkestrate-MCP] Fatal: ${err}\\n`);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "orkestrate",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI for Orkestrate — the coordination layer for autonomous AI coding agents",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"orkestrate": "./dist/cli.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist",
|
|
11
|
-
"README.md",
|
|
12
|
-
"LICENSE"
|
|
13
|
-
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"build": "tsup",
|
|
16
|
-
"dev": "tsup --watch",
|
|
17
|
-
"prepublishOnly": "tsup"
|
|
18
|
-
},
|
|
19
|
-
"keywords": [
|
|
20
|
-
"orkestrate",
|
|
21
|
-
"mcp",
|
|
22
|
-
"ai-agents",
|
|
23
|
-
"multi-agent",
|
|
24
|
-
"coordination",
|
|
25
|
-
"claude",
|
|
26
|
-
"opencode",
|
|
27
|
-
"cursor",
|
|
28
|
-
"codex",
|
|
29
|
-
"cli"
|
|
30
|
-
],
|
|
31
|
-
"author": "system1970",
|
|
32
|
-
"license": "MIT",
|
|
33
|
-
"repository": {
|
|
34
|
-
"type": "git",
|
|
35
|
-
"url": "https://github.com/system1970/Orkestrate.git",
|
|
36
|
-
"directory": "packages/cli"
|
|
37
|
-
},
|
|
38
|
-
"homepage": "https://orkestrate.space",
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"commander": "^13.1.0",
|
|
41
|
-
"conf": "^13.1.0",
|
|
42
|
-
"open": "^10.1.2",
|
|
43
|
-
"picocolors": "^1.1.1"
|
|
44
|
-
},
|
|
45
|
-
"devDependencies": {
|
|
46
|
-
"@types/node": "^22.15.0",
|
|
47
|
-
"tsup": "^8.4.0",
|
|
48
|
-
"typescript": "^5.8.0"
|
|
49
|
-
}
|
|
50
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "orkestrate",
|
|
3
|
+
"version": "0.1.15",
|
|
4
|
+
"description": "CLI for Orkestrate — the coordination layer for autonomous AI coding agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"orkestrate": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup",
|
|
16
|
+
"dev": "tsup --watch",
|
|
17
|
+
"prepublishOnly": "tsup"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"orkestrate",
|
|
21
|
+
"mcp",
|
|
22
|
+
"ai-agents",
|
|
23
|
+
"multi-agent",
|
|
24
|
+
"coordination",
|
|
25
|
+
"claude",
|
|
26
|
+
"opencode",
|
|
27
|
+
"cursor",
|
|
28
|
+
"codex",
|
|
29
|
+
"cli"
|
|
30
|
+
],
|
|
31
|
+
"author": "system1970",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/system1970/Orkestrate.git",
|
|
36
|
+
"directory": "packages/cli"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://orkestrate.space",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"commander": "^13.1.0",
|
|
41
|
+
"conf": "^13.1.0",
|
|
42
|
+
"open": "^10.1.2",
|
|
43
|
+
"picocolors": "^1.1.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.15.0",
|
|
47
|
+
"tsup": "^8.4.0",
|
|
48
|
+
"typescript": "^5.8.0"
|
|
49
|
+
}
|
|
50
|
+
}
|