@moor-sh/cli 0.1.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moor-sh/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Command-line interface for moor - manage your moor server's projects, logs, env vars, and container lifecycle.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,136 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ type Client = "claude" | "codex";
5
+
6
+ const CLIENT_ALIASES: Record<string, Client> = {
7
+ claude: "claude",
8
+ "claude-code": "claude",
9
+ codex: "codex",
10
+ };
11
+
12
+ const DEFAULT_URL = "http://127.0.0.1:8080";
13
+ const PLACEHOLDER_KEY = "<your-api-key>";
14
+
15
+ /** Parse cwd's .env conservatively. Skips blank/comment lines, strips one
16
+ * matching pair of surrounding quotes, does not interpolate or expand. */
17
+ function parseDotEnv(path: string): Record<string, string> {
18
+ if (!existsSync(path)) return {};
19
+ const content = readFileSync(path, "utf8");
20
+ const env: Record<string, string> = {};
21
+ for (const rawLine of content.split("\n")) {
22
+ const line = rawLine.trim();
23
+ if (!line || line.startsWith("#")) continue;
24
+ const eq = line.indexOf("=");
25
+ if (eq === -1) continue;
26
+ const key = line.slice(0, eq).trim();
27
+ let value = line.slice(eq + 1).trim();
28
+ if (
29
+ value.length >= 2 &&
30
+ ((value.startsWith('"') && value.endsWith('"')) ||
31
+ (value.startsWith("'") && value.endsWith("'")))
32
+ ) {
33
+ value = value.slice(1, -1);
34
+ }
35
+ env[key] = value;
36
+ }
37
+ return env;
38
+ }
39
+
40
+ function resolveApiKey(flagValue: string | undefined): string {
41
+ if (flagValue) return flagValue;
42
+ if (process.env.MOOR_API_KEY) return process.env.MOOR_API_KEY;
43
+ const dotenv = parseDotEnv(join(process.cwd(), ".env"));
44
+ if (dotenv.MOOR_API_KEY) return dotenv.MOOR_API_KEY;
45
+ return PLACEHOLDER_KEY;
46
+ }
47
+
48
+ function configFor(client: Client, url: string, apiKey: string): string {
49
+ if (client === "claude") {
50
+ return JSON.stringify(
51
+ {
52
+ mcpServers: {
53
+ moor: {
54
+ command: "bunx",
55
+ args: ["@moor-sh/mcp"],
56
+ env: { MOOR_URL: url, MOOR_API_KEY: apiKey },
57
+ },
58
+ },
59
+ },
60
+ null,
61
+ 2,
62
+ );
63
+ }
64
+ // codex TOML. TOML basic-string escape rules (" and \) are a subset of
65
+ // JSON's, so JSON.stringify produces a valid quoted TOML string for any
66
+ // value - including URLs/keys that contain quotes or backslashes.
67
+ return [
68
+ "[mcp_servers.moor]",
69
+ 'command = "bunx"',
70
+ 'args = ["@moor-sh/mcp"]',
71
+ "",
72
+ "[mcp_servers.moor.env]",
73
+ `MOOR_URL = ${JSON.stringify(url)}`,
74
+ `MOOR_API_KEY = ${JSON.stringify(apiKey)}`,
75
+ ].join("\n");
76
+ }
77
+
78
+ function parseFlags(args: string[]): { client?: string; url?: string; apiKey?: string } {
79
+ const flags: { client?: string; url?: string; apiKey?: string } = {};
80
+ for (let i = 0; i < args.length; i++) {
81
+ const a = args[i];
82
+ if (a === "--client" && i + 1 < args.length) flags.client = args[++i];
83
+ else if (a === "--url" && i + 1 < args.length) flags.url = args[++i];
84
+ else if (a === "--api-key" && i + 1 < args.length) flags.apiKey = args[++i];
85
+ }
86
+ return flags;
87
+ }
88
+
89
+ function printHelp() {
90
+ console.log(`moor mcp config - Generate MCP client config snippet for moor
91
+
92
+ Usage: moor mcp config --client <name> [--url <url>] [--api-key <key>]
93
+
94
+ Required:
95
+ --client <name> One of: claude, claude-code, codex
96
+
97
+ Optional:
98
+ --url <url> moor URL the MCP server should reach (default: ${DEFAULT_URL})
99
+ --api-key <key> bearer token. Falls back to MOOR_API_KEY env, then
100
+ cwd's .env, then a placeholder.
101
+
102
+ Output is printed to stdout for you to paste into your client's config file:
103
+ Claude Code: ~/.claude.json
104
+ Codex: ~/.codex/config.toml`);
105
+ }
106
+
107
+ export function mcpCommand(args: string[]) {
108
+ const subcommand = args[0];
109
+
110
+ if (subcommand === "--help" || subcommand === "-h" || subcommand === "help" || !subcommand) {
111
+ printHelp();
112
+ return;
113
+ }
114
+
115
+ if (subcommand !== "config") {
116
+ console.error(`Unknown mcp subcommand: ${subcommand}\n`);
117
+ printHelp();
118
+ process.exit(1);
119
+ }
120
+
121
+ const flags = parseFlags(args.slice(1));
122
+ if (!flags.client) {
123
+ console.error("moor mcp config requires --client <claude|claude-code|codex>\n");
124
+ printHelp();
125
+ process.exit(1);
126
+ }
127
+ const client = CLIENT_ALIASES[flags.client];
128
+ if (!client) {
129
+ console.error(`Unknown client: ${flags.client}. Expected one of: claude, claude-code, codex`);
130
+ process.exit(1);
131
+ }
132
+
133
+ const url = flags.url || DEFAULT_URL;
134
+ const apiKey = resolveApiKey(flags.apiKey);
135
+ console.log(configFor(client, url, apiKey));
136
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { envCommand } from "./commands/env";
5
5
  import { execCommand } from "./commands/exec";
6
6
  import { logsCommand } from "./commands/logs";
7
+ import { mcpCommand } from "./commands/mcp";
7
8
  import { rebuildCommand } from "./commands/rebuild";
8
9
  import { restartCommand } from "./commands/restart";
9
10
  import { statsCommand } from "./commands/stats";
@@ -33,6 +34,7 @@ Commands:
33
34
  env list <project> List environment variables
34
35
  env set <project> K=V [K=V ...] Set environment variables
35
36
  stats Show server resource usage
37
+ mcp config --client <name> Generate MCP client config snippet
36
38
 
37
39
  Environment:
38
40
  MOOR_URL Server URL (e.g. https://moor.example.com)
@@ -64,6 +66,9 @@ switch (command) {
64
66
  case "stats":
65
67
  await statsCommand();
66
68
  break;
69
+ case "mcp":
70
+ mcpCommand(args.slice(1));
71
+ break;
67
72
  case "--help":
68
73
  case "-h":
69
74
  case "help":