@orchyn/mcp 1.1.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/dist/index.js ADDED
@@ -0,0 +1,564 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * orchyn-mcp — MCP server exposing a single `analyze_video` tool that runs
4
+ * AI video analysis through the user's orchyn account.
5
+ *
6
+ * Modes:
7
+ * orchyn-mcp stdio transport (default; Claude Desktop, Cursor)
8
+ * orchyn-mcp login browser-based Google sign-in to orchyn
9
+ * orchyn-mcp --http remote HTTP transport with OAuth (OpenAI Agents SDK)
10
+ */
11
+ import http from "node:http";
12
+ import { randomUUID } from "node:crypto";
13
+ import { z } from "zod";
14
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
17
+ import { getBaseUrl, getPublicUrl, getPort, getCredentialsFile, getTransportMode, DEFAULT_PORT, } from "./config.js";
18
+ import { AuthManager, OrchynAuthError, createHttpTokenProvider, createStdioTokenProvider } from "./auth.js";
19
+ import { OrchynClient, OrchynError } from "./orchyn.js";
20
+ import { OAuthManager } from "./oauth.js";
21
+ import { formatPaywallError, runVideoAnalysis, validateVideoUrl } from "./video.js";
22
+ const TOOL_NAME = "analyze_video";
23
+ const TOOL_DESCRIPTION = "Start an AI analysis of a TikTok, Instagram, or YouTube video from its link. " +
24
+ "Requires a connected orchyn account; consumes orchyn credits (first analysis free). " +
25
+ "Returns the analysis result once finished.";
26
+ const TOOL_INPUT_SCHEMA = z
27
+ .object({
28
+ url: z
29
+ .string()
30
+ .describe("Public video URL (tiktok.com, instagram.com, youtube.com, youtu.be, or common shortlinks)."),
31
+ })
32
+ .strict();
33
+ function toToolResult(proxy) {
34
+ const images = proxy.contentBlocks
35
+ .filter((c) => c.type === "image")
36
+ .map((c) => ({
37
+ type: "image",
38
+ data: String(c.data ?? ""),
39
+ mimeType: String(c.mimeType ?? "image/jpeg"),
40
+ }));
41
+ const text = JSON.stringify(proxy.structured ?? {}, null, 2);
42
+ return { content: [...images, { type: "text", text }] };
43
+ }
44
+ function toolError(prefix, err) {
45
+ const msg = err instanceof Error ? err.message : String(err);
46
+ return { content: [{ type: "text", text: `${prefix}: ${msg}` }], isError: true };
47
+ }
48
+ export function createServer(opts) {
49
+ const server = new McpServer({
50
+ name: "orchyn-mcp",
51
+ version: "1.0.0",
52
+ });
53
+ server.registerTool(TOOL_NAME, {
54
+ title: "Analyze Video",
55
+ description: TOOL_DESCRIPTION,
56
+ inputSchema: TOOL_INPUT_SCHEMA,
57
+ }, async (args, extra) => {
58
+ let session;
59
+ if (extra.authInfo?.token && opts.resolveSession) {
60
+ session = opts.resolveSession(extra.authInfo.token);
61
+ }
62
+ const client = opts.makeClient(session);
63
+ const validation = validateVideoUrl(args.url);
64
+ if (!validation.ok) {
65
+ return {
66
+ content: [{ type: "text", text: `Invalid url: ${validation.error}` }],
67
+ isError: true,
68
+ };
69
+ }
70
+ try {
71
+ const result = await runVideoAnalysis(client, validation.url);
72
+ return {
73
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
74
+ };
75
+ }
76
+ catch (err) {
77
+ if (err instanceof OrchynError && err.paywall) {
78
+ return {
79
+ content: [
80
+ {
81
+ type: "text",
82
+ text: `Analysis blocked: ${formatPaywallError(err)}\n\nHTTP ${err.status}: ${err.message}`,
83
+ },
84
+ ],
85
+ isError: true,
86
+ };
87
+ }
88
+ const msg = err instanceof Error ? err.message : String(err);
89
+ return {
90
+ content: [
91
+ {
92
+ type: "text",
93
+ text: `Video analysis failed: ${msg}`,
94
+ },
95
+ ],
96
+ isError: true,
97
+ };
98
+ }
99
+ });
100
+ server.registerTool("get_social_media", {
101
+ title: "Get Social Media",
102
+ description: "Fetch a social post's media from a TikTok, Instagram, YouTube or X/Twitter URL: " +
103
+ "contentType (video/image/carousel/slideshow), title, caption, author, stats and direct media URLs. " +
104
+ "Returns an inline thumbnail image. Consumes 1 orchyn credit.",
105
+ inputSchema: z
106
+ .object({
107
+ url: z.string().describe("Full public post URL."),
108
+ })
109
+ .strict(),
110
+ }, async (args, extra) => {
111
+ const client = makeClientFor(extra, opts);
112
+ try {
113
+ return toToolResult(await client.callTool("get_social_media", { url: args.url }));
114
+ }
115
+ catch (err) {
116
+ return toolError("get_social_media failed", err);
117
+ }
118
+ });
119
+ server.registerTool("discover_social_videos", {
120
+ title: "Discover Social Videos",
121
+ description: "Discover recent videos/posts for a niche. YouTube via search; TikTok & Instagram via Apify. " +
122
+ "Consumes 2 orchyn credits.",
123
+ inputSchema: z
124
+ .object({
125
+ niche: z.string().describe("Niche/topic, e.g. 'fitness'."),
126
+ keywords: z.string().optional().describe("Optional extra keywords."),
127
+ limit: z.number().int().optional().describe("Max results (default 6)."),
128
+ platform: z
129
+ .enum(["youtube", "tiktok", "instagram", "any"])
130
+ .optional()
131
+ .describe("Platform to search (default youtube)."),
132
+ })
133
+ .strict(),
134
+ }, async (args, extra) => {
135
+ const client = makeClientFor(extra, opts);
136
+ try {
137
+ return toToolResult(await client.callTool("discover_social_videos", { ...args }));
138
+ }
139
+ catch (err) {
140
+ return toolError("discover_social_videos failed", err);
141
+ }
142
+ });
143
+ server.registerTool("understand_social_post", {
144
+ title: "Understand Social Post",
145
+ description: "Import a social post URL AND understand it with multimodal AI over the actual video/images: " +
146
+ "summary, hook strength, viral triggers, format breakdown and variation ideas. Includes the thumbnail. " +
147
+ "Consumes 10 orchyn credits.",
148
+ inputSchema: z
149
+ .object({
150
+ url: z.string().describe("Full public post URL (TikTok/Instagram/YouTube)."),
151
+ focus: z
152
+ .string()
153
+ .optional()
154
+ .describe("Extra instruction, e.g. 'focus on the CTA'."),
155
+ })
156
+ .strict(),
157
+ }, async (args, extra) => {
158
+ const client = makeClientFor(extra, opts);
159
+ try {
160
+ return toToolResult(await client.callTool("understand_social_post", { ...args }));
161
+ }
162
+ catch (err) {
163
+ return toolError("understand_social_post failed", err);
164
+ }
165
+ });
166
+ return server;
167
+ }
168
+ /**
169
+ * Resolves the per-request OrchynClient: HTTP-mode MCP sessions carry their
170
+ * own orchyn identity via authInfo; stdio uses the shared logged-in session.
171
+ */
172
+ function makeClientFor(extra, opts) {
173
+ let session;
174
+ if (extra.authInfo?.token && opts.resolveSession) {
175
+ session = opts.resolveSession(extra.authInfo.token);
176
+ }
177
+ return opts.makeClient(session);
178
+ }
179
+ // ---------------------------------------------------------------------------
180
+ // stdio mode
181
+ // ---------------------------------------------------------------------------
182
+ export async function runStdio() {
183
+ const auth = new AuthManager(getBaseUrl(), getCredentialsFile());
184
+ const server = createServer({
185
+ makeClient: (session) => new OrchynClient(getBaseUrl(), createStdioTokenProvider(auth)),
186
+ });
187
+ const transport = new StdioServerTransport();
188
+ await server.connect(transport);
189
+ // Keep the process alive until the transport closes (handled by the SDK).
190
+ }
191
+ function sendJson(res, status, body) {
192
+ const payload = JSON.stringify(body);
193
+ res.writeHead(status, {
194
+ "content-type": "application/json",
195
+ "content-length": Buffer.byteLength(payload),
196
+ "cache-control": "no-store",
197
+ });
198
+ res.end(payload);
199
+ }
200
+ function readJsonBody(req) {
201
+ return new Promise((resolve, reject) => {
202
+ const chunks = [];
203
+ req.on("data", (c) => chunks.push(Buffer.from(c)));
204
+ req.on("end", () => {
205
+ if (chunks.length === 0)
206
+ return resolve(undefined);
207
+ const text = Buffer.concat(chunks).toString("utf8");
208
+ try {
209
+ resolve(JSON.parse(text));
210
+ }
211
+ catch {
212
+ resolve(undefined);
213
+ }
214
+ });
215
+ req.on("error", reject);
216
+ });
217
+ }
218
+ function sendHtml(res, status, body) {
219
+ const payload = `<!doctype html><html><head><meta charset="utf-8"><title>orchyn-mcp</title></head><body>${body}</body></html>`;
220
+ res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
221
+ res.end(payload);
222
+ }
223
+ function bearerToken(req) {
224
+ const header = req.headers.authorization;
225
+ if (!header)
226
+ return undefined;
227
+ const match = /^Bearer\s+(.+)$/i.exec(header);
228
+ return match ? match[1].trim() : undefined;
229
+ }
230
+ /**
231
+ * A request bearer token is valid when it was issued by our OAuth `/token`
232
+ * endpoint, or when it matches `ORCHYN_ACCESS_TOKEN` (pre-provisioned
233
+ * deployments and local testing — no browser OAuth round-trip needed).
234
+ */
235
+ function validMcpToken(token, oauth) {
236
+ if (oauth.verifyToken(token))
237
+ return true;
238
+ const envToken = process.env.ORCHYN_ACCESS_TOKEN;
239
+ return typeof envToken === "string" && envToken.length > 0 && token === envToken;
240
+ }
241
+ async function handleMcpRequest(state, req, res) {
242
+ const token = bearerToken(req);
243
+ const session = token ? state.oauth.verifyToken(token) : undefined;
244
+ if (!token || !validMcpToken(token, state.oauth)) {
245
+ return sendJson(res, 401, {
246
+ error: "Unauthorized",
247
+ error_description: "This MCP server requires OAuth authentication. Fetch an access token from " +
248
+ `${state.oauth.authorizationServerMetadata().authorization_endpoint} first.`,
249
+ });
250
+ }
251
+ // Hand the validated token to the SDK transport, which forwards it to tool
252
+ // handlers via RequestHandlerExtra.authInfo (see shared/protocol.js).
253
+ req.auth = {
254
+ token,
255
+ clientId: session?.clientId ?? "orchyn-mcp",
256
+ scopes: session?.scopes ?? ["analyze:video"],
257
+ expiresAt: session
258
+ ? Math.floor(session.expiresAt / 1000)
259
+ : Math.floor(Date.now() / 1000) + 3600,
260
+ };
261
+ const sessionId = req.headers["mcp-session-id"] ?? "";
262
+ let transport;
263
+ let parsedBody;
264
+ if (sessionId) {
265
+ transport = state.transports.get(sessionId);
266
+ }
267
+ else {
268
+ // No session id: an "initialize" request always starts a fresh session.
269
+ // Any other request is routed to the sole active session's transport
270
+ // (covers naive clients that never send the session id).
271
+ let isInitialize = false;
272
+ if (req.method === "POST") {
273
+ parsedBody = await readJsonBody(req);
274
+ const messages = Array.isArray(parsedBody) ? parsedBody : [parsedBody];
275
+ isInitialize = messages.some((m) => m !== null &&
276
+ typeof m === "object" &&
277
+ m.method === "initialize");
278
+ }
279
+ if (!isInitialize && state.transports.size === 1) {
280
+ transport = state.transports.values().next().value;
281
+ if (transport.sessionId) {
282
+ // The SDK's stateful transport requires the header on non-initialize
283
+ // requests; hono's node adapter reads req.rawHeaders, so patch both.
284
+ req.rawHeaders.push("mcp-session-id", transport.sessionId);
285
+ req.headers["mcp-session-id"] =
286
+ transport.sessionId;
287
+ }
288
+ }
289
+ }
290
+ if (!transport) {
291
+ transport = new StreamableHTTPServerTransport({
292
+ sessionIdGenerator: () => randomUUID(),
293
+ enableJsonResponse: true,
294
+ onsessioninitialized: (sid) => {
295
+ state.transports.set(sid, transport);
296
+ },
297
+ });
298
+ // The SDK's McpServer can only attach to one transport, so each MCP
299
+ // session gets its own server instance (tools are registered per
300
+ // instance; sessions resolve their orchyn identity via authInfo).
301
+ const mcpserver = state.serverFactory();
302
+ state.connections.set(transport, mcpserver);
303
+ transport.onclose = () => {
304
+ state.connections.delete(transport);
305
+ if (transport?.sessionId)
306
+ state.transports.delete(transport.sessionId);
307
+ };
308
+ await mcpserver.connect(transport);
309
+ }
310
+ try {
311
+ // If we already consumed the body to sniff for "initialize", hand the
312
+ // parsed payload to the transport so it doesn't try to re-read it.
313
+ if (parsedBody !== undefined) {
314
+ await transport.handleRequest(req, res, parsedBody);
315
+ }
316
+ else {
317
+ await transport.handleRequest(req, res);
318
+ }
319
+ }
320
+ catch (err) {
321
+ const msg = err instanceof Error ? err.message : String(err);
322
+ if (!res.headersSent) {
323
+ sendJson(res, 500, { error: `Internal error: ${msg}` });
324
+ }
325
+ }
326
+ }
327
+ async function handleHttpRequest(state, req, res) {
328
+ const url = new URL(req.url ?? "/", "http://localhost");
329
+ const pathname = url.pathname;
330
+ if (pathname === "/.well-known/oauth-authorization-server" && req.method === "GET") {
331
+ return sendJson(res, 200, state.oauth.authorizationServerMetadata());
332
+ }
333
+ if (pathname === "/.well-known/oauth-protected-resource" && req.method === "GET") {
334
+ return sendJson(res, 200, state.oauth.protectedResourceMetadata());
335
+ }
336
+ if (pathname === "/authorize" && req.method === "GET") {
337
+ return state.oauth.handleAuthorize(req, res);
338
+ }
339
+ if (pathname === "/token" && req.method === "POST") {
340
+ return state.oauth.handleToken(req, res);
341
+ }
342
+ if (pathname === "/oauth/callback" && req.method === "GET") {
343
+ return state.oauth.handleCallback(req, res);
344
+ }
345
+ if (pathname === "/" && req.method === "GET") {
346
+ const meta = state.oauth.authorizationServerMetadata();
347
+ return sendHtml(res, 200, `<h1>orchyn-mcp</h1><p>MCP server is running.</p>` +
348
+ `<p>Authorization endpoint: <code>${meta.authorization_endpoint}</code></p>` +
349
+ `<p>Token endpoint: <code>${meta.token_endpoint}</code></p>`);
350
+ }
351
+ if (pathname === "/mcp" || pathname === "/") {
352
+ return handleMcpRequest(state, req, res);
353
+ }
354
+ return sendJson(res, 404, { error: "Not found" });
355
+ }
356
+ export async function runHttp(port, publicUrl) {
357
+ const baseUrl = getBaseUrl();
358
+ const pub = publicUrl ?? getPublicUrl();
359
+ const auth = new AuthManager(baseUrl, getCredentialsFile());
360
+ const oauth = new OAuthManager({
361
+ publicUrl: pub,
362
+ client: new OrchynClient(baseUrl, {
363
+ getAccessToken: async () => undefined,
364
+ }),
365
+ onSession: async (session) => {
366
+ await auth.persistSession(session);
367
+ },
368
+ });
369
+ const state = {
370
+ oauth,
371
+ auth,
372
+ transports: new Map(),
373
+ connections: new Map(),
374
+ serverFactory: () => createServer({
375
+ makeClient: (session) => new OrchynClient(baseUrl, createHttpTokenProvider(auth, session
376
+ ? {
377
+ accessToken: session.orchynAccessToken,
378
+ refreshToken: session.orchynRefreshToken,
379
+ }
380
+ : undefined)),
381
+ resolveSession: (mcpAccessToken) => oauth.verifyToken(mcpAccessToken),
382
+ }),
383
+ };
384
+ const server = http.createServer((req, res) => {
385
+ handleHttpRequest(state, req, res).catch((err) => {
386
+ const msg = err instanceof Error ? err.message : String(err);
387
+ if (!res.headersSent)
388
+ sendJson(res, 500, { error: `Internal error: ${msg}` });
389
+ else
390
+ res.end();
391
+ });
392
+ });
393
+ server.listen(port, () => {
394
+ process.stdout.write(`[orchyn-mcp] HTTP server listening on ${pub.replace(/:\d+$/, "")}:${port}\n` +
395
+ `[orchyn-mcp] OAuth metadata: ${pub}/.well-known/oauth-authorization-server\n` +
396
+ `[orchyn-mcp] MCP endpoint: ${pub}/mcp\n`);
397
+ });
398
+ }
399
+ export async function runLogin(opts) {
400
+ const baseUrl = getBaseUrl();
401
+ const auth = new AuthManager(baseUrl, getCredentialsFile());
402
+ const client = new OrchynClient(baseUrl, {
403
+ getAccessToken: async () => undefined,
404
+ });
405
+ if (opts.email && opts.password) {
406
+ const session = await client.login(opts.email, opts.password);
407
+ await auth.persistSession(session);
408
+ process.stdout.write(`Signed in as ${session.user?.email ?? opts.email}. Credentials saved to ${auth.getCredentialsFile()}\n`);
409
+ return;
410
+ }
411
+ // Browser flow with a loopback listener.
412
+ const callbackPath = "/oauth/callback";
413
+ const callbackUrl = `http://127.0.0.1:${opts.port}${callbackPath}`;
414
+ const googleStart = new URL("/auth/google/start", baseUrl);
415
+ googleStart.searchParams.set("redirect", callbackUrl);
416
+ const listener = http.createServer((req, res) => {
417
+ const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
418
+ if (reqUrl.pathname === callbackPath && req.method === "GET") {
419
+ const code = reqUrl.searchParams.get("code") ?? "";
420
+ if (code) {
421
+ client
422
+ .exchangeCompletionCode(code)
423
+ .then(async (session) => {
424
+ await auth.persistSession(session);
425
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
426
+ res.end("<html><body><h1>Signed in!</h1><p>You can close this tab and return to your terminal.</p></body></html>");
427
+ process.stdout.write(`Signed in as ${session.user?.email ?? "unknown user"}. Credentials saved to ${auth.getCredentialsFile()}\n`);
428
+ process.exit(0);
429
+ })
430
+ .catch((err) => {
431
+ const msg = err instanceof Error ? err.message : String(err);
432
+ res.writeHead(502, { "content-type": "text/html; charset=utf-8" });
433
+ res.end(`<html><body><h1>Sign-in failed</h1><p>${msg}</p></body></html>`);
434
+ process.stderr.write(`Sign-in failed: ${msg}\n`);
435
+ process.exit(1);
436
+ });
437
+ }
438
+ else {
439
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
440
+ res.end("<html><body><h1>Sign-in failed</h1><p>No code returned.</p></body></html>");
441
+ process.exit(1);
442
+ }
443
+ return;
444
+ }
445
+ res.writeHead(404).end();
446
+ });
447
+ await new Promise((resolve, reject) => {
448
+ listener.once("error", reject);
449
+ listener.listen(opts.port, "127.0.0.1", resolve);
450
+ });
451
+ process.stdout.write(`Open this URL in your browser to sign in with your orchyn account:\n\n ${googleStart.toString()}\n\n`);
452
+ try {
453
+ const { default: open } = await import("open");
454
+ await open(googleStart.toString());
455
+ }
456
+ catch {
457
+ process.stdout.write("Could not open the browser automatically. Copy the URL above into your browser.\n");
458
+ }
459
+ // Time out after 5 minutes.
460
+ setTimeout(() => {
461
+ process.stderr.write("Timed out waiting for sign-in.\n");
462
+ process.exit(1);
463
+ }, 300_000).unref();
464
+ }
465
+ // ---------------------------------------------------------------------------
466
+ // CLI
467
+ // ---------------------------------------------------------------------------
468
+ function printHelp() {
469
+ process.stdout.write(`orchyn-mcp — MCP server for orchyn AI video analysis
470
+
471
+ Usage:
472
+ orchyn-mcp Start in stdio mode (default transport)
473
+ orchyn-mcp --stdio Same as above
474
+ orchyn-mcp --http [--port N] Start the remote HTTP transport with OAuth
475
+ (default port ${DEFAULT_PORT}; also ORCHYN_PORT)
476
+ orchyn-mcp login Sign in to orchyn via Google in your browser
477
+ orchyn-mcp login --email me@example.com --password '...' Password login
478
+ orchyn-mcp --help Show this help
479
+
480
+ Environment variables:
481
+ ORCHYN_BASE_URL orchyn server base URL (default http://localhost:8080)
482
+ ORCHYN_ACCESS_TOKEN orchyn JWT access token (bypasses login)
483
+ ORCHYN_CREDENTIALS_FILE token store path (default ~/.config/orchyn-mcp/credentials.json)
484
+ ORCHYN_PUBLIC_URL public base URL for the HTTP mode (default http://localhost:3457)
485
+ ORCHYN_PORT port for --http and login (default ${DEFAULT_PORT})
486
+ ORCHYN_TRANSPORT "stdio" or "http"
487
+
488
+ Client setup:
489
+ Claude Desktop / Cursor (stdio): after "orchyn-mcp login", use
490
+ "command": "npx", "args": ["orchyn-mcp"] (plus ORCHYN_ACCESS_TOKEN if needed)
491
+ OpenAI Agents SDK (remote HTTP): use the RemoteMCPClient with URL
492
+ <ORCHYN_PUBLIC_URL>/mcp — the OAuth flow will open your browser.
493
+
494
+ See README.md for full instructions.
495
+ `);
496
+ }
497
+ async function main() {
498
+ const args = process.argv.slice(2);
499
+ if (args.includes("--help") || args.includes("-h")) {
500
+ printHelp();
501
+ return;
502
+ }
503
+ if (args[0] === "login") {
504
+ const rest = args.slice(1);
505
+ const valueOf = (flag) => {
506
+ const idx = rest.indexOf(flag);
507
+ return idx >= 0 && rest[idx + 1] ? rest[idx + 1] : undefined;
508
+ };
509
+ const email = valueOf("--email");
510
+ const password = valueOf("--password");
511
+ let port = getPort();
512
+ const portRaw = valueOf("--port");
513
+ if (portRaw !== undefined)
514
+ port = Number.parseInt(portRaw, 10);
515
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
516
+ process.stderr.write("Invalid --port value.\n");
517
+ process.exit(1);
518
+ }
519
+ if (Boolean(email) !== Boolean(password)) {
520
+ process.stderr.write("Both --email and --password must be provided together.\n");
521
+ process.exit(1);
522
+ }
523
+ try {
524
+ await runLogin({ email, password, port });
525
+ }
526
+ catch (err) {
527
+ process.stderr.write(`Login failed: ${err instanceof Error ? err.message : String(err)}\n`);
528
+ process.exit(1);
529
+ }
530
+ return;
531
+ }
532
+ const isHttp = args.includes("--http") || getTransportMode() === "http";
533
+ let port = getPort();
534
+ if (args.includes("--port")) {
535
+ const idx = args.indexOf("--port");
536
+ if (args[idx + 1])
537
+ port = Number.parseInt(args[idx + 1], 10);
538
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
539
+ process.stderr.write("Invalid --port value.\n");
540
+ process.exit(1);
541
+ }
542
+ }
543
+ try {
544
+ if (isHttp) {
545
+ await runHttp(port);
546
+ }
547
+ else {
548
+ await runStdio();
549
+ }
550
+ }
551
+ catch (err) {
552
+ if (err instanceof OrchynAuthError) {
553
+ process.stderr.write(`${err.message}\n`);
554
+ }
555
+ else if (err instanceof OrchynError) {
556
+ process.stderr.write(`orchyn API error: ${err.message}\n`);
557
+ }
558
+ else {
559
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
560
+ }
561
+ process.exit(1);
562
+ }
563
+ }
564
+ main();