@getjack/jack 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,22 @@ jack list
34
34
  - [Bun](https://bun.sh) >= 1.0.0
35
35
  - Cloudflare account (for deployments)
36
36
 
37
+ ## Releasing
38
+
39
+ This project uses [Semantic Versioning](https://semver.org/).
40
+
41
+ ```bash
42
+ # Bump version, commit, and tag
43
+ npm version patch # 0.1.1 → 0.1.2 (bug fixes)
44
+ npm version minor # 0.1.1 → 0.2.0 (new features)
45
+ npm version major # 0.1.1 → 1.0.0 (breaking changes)
46
+
47
+ # Push to trigger publish
48
+ git push && git push --tags
49
+ ```
50
+
51
+ GitHub Actions automatically publishes to npm when a version tag is pushed.
52
+
37
53
  ## License
38
54
 
39
55
  Apache-2.0
package/package.json CHANGED
@@ -1,41 +1,49 @@
1
1
  {
2
- "name": "@getjack/jack",
3
- "version": "0.1.0",
4
- "description": "CLI for vibecoders to rapidly deploy to Cloudflare Workers",
5
- "license": "Apache-2.0",
6
- "type": "module",
7
- "bin": {
8
- "jack": "./src/index.ts"
9
- },
10
- "files": [
11
- "src",
12
- "templates"
13
- ],
14
- "engines": {
15
- "bun": ">=1.0.0"
16
- },
17
- "repository": {
18
- "type": "git",
19
- "url": "https://github.com/getjack-org/jack"
20
- },
21
- "publishConfig": {
22
- "access": "public"
23
- },
24
- "keywords": ["cli", "cloudflare", "workers", "deploy", "vibecode"],
25
- "scripts": {
26
- "dev": "bun run src/index.ts",
27
- "build": "bun build --compile src/index.ts --outfile dist/jack",
28
- "lint": "biome check .",
29
- "format": "biome format --write ."
30
- },
31
- "devDependencies": {
32
- "@biomejs/biome": "^1.9.0",
33
- "bun-types": "latest"
34
- },
35
- "dependencies": {
36
- "@inquirer/prompts": "^7.0.0",
37
- "meow": "^14.0.0",
38
- "posthog-node": "^5.17.4",
39
- "yocto-spinner": "^1.0.0"
40
- }
2
+ "name": "@getjack/jack",
3
+ "version": "0.1.1",
4
+ "description": "CLI for vibecoders to rapidly deploy to Cloudflare Workers",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "bin": {
8
+ "jack": "./src/index.ts"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "templates"
13
+ ],
14
+ "engines": {
15
+ "bun": ">=1.0.0"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/getjack-org/jack"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "keywords": [
25
+ "cli",
26
+ "cloudflare",
27
+ "workers",
28
+ "deploy",
29
+ "vibecode"
30
+ ],
31
+ "scripts": {
32
+ "dev": "bun run src/index.ts",
33
+ "build": "bun build --compile src/index.ts --outfile dist/jack",
34
+ "lint": "biome check .",
35
+ "format": "biome format --write ."
36
+ },
37
+ "devDependencies": {
38
+ "@biomejs/biome": "^1.9.0",
39
+ "bun-types": "latest"
40
+ },
41
+ "dependencies": {
42
+ "@inquirer/prompts": "^7.0.0",
43
+ "@modelcontextprotocol/sdk": "^1.25.1",
44
+ "meow": "^14.0.0",
45
+ "posthog-node": "^5.17.4",
46
+ "yocto-spinner": "^1.0.0",
47
+ "zod": "^4.2.1"
48
+ }
41
49
  }
@@ -103,7 +103,7 @@ async function scanAndPrompt(): Promise<void> {
103
103
 
104
104
  console.error("");
105
105
  success(`Found ${newAgents.length} new agent(s):`);
106
- for (const { id, path } of newAgents) {
106
+ for (const { id, path, launch } of newAgents) {
107
107
  const definition = getAgentDefinition(id);
108
108
  item(`${definition?.name}: ${path}`);
109
109
 
@@ -112,6 +112,7 @@ async function scanAndPrompt(): Promise<void> {
112
112
  active: true,
113
113
  path,
114
114
  detectedAt: new Date().toISOString(),
115
+ launch,
115
116
  });
116
117
  }
117
118
 
@@ -120,6 +121,17 @@ async function scanAndPrompt(): Promise<void> {
120
121
  info("Future projects will include context files for these agents");
121
122
  }
122
123
 
124
+ function getFlagValues(args: string[], flag: string): string[] {
125
+ const values: string[] = [];
126
+ for (let i = 0; i < args.length; i++) {
127
+ if (args[i] === flag && args[i + 1]) {
128
+ values.push(args[i + 1]);
129
+ i++;
130
+ }
131
+ }
132
+ return values;
133
+ }
134
+
123
135
  /**
124
136
  * Manually add an agent
125
137
  */
@@ -128,7 +140,7 @@ async function addAgentCommand(args: string[]): Promise<void> {
128
140
 
129
141
  if (!agentId) {
130
142
  error("Agent ID required");
131
- info("Usage: jack agents add <id> [--path /custom/path]");
143
+ info("Usage: jack agents add <id> [--command cmd] [--arg value]");
132
144
  info(`Available IDs: ${AGENT_REGISTRY.map((a) => a.id).join(", ")}`);
133
145
  process.exit(1);
134
146
  }
@@ -140,15 +152,32 @@ async function addAgentCommand(args: string[]): Promise<void> {
140
152
  process.exit(1);
141
153
  }
142
154
 
143
- // Parse --path flag
144
- const pathIndex = rest.indexOf("--path");
145
- let customPath: string | undefined;
146
- if (pathIndex >= 0 && rest[pathIndex + 1]) {
147
- customPath = rest[pathIndex + 1];
155
+ // Parse flags
156
+ let customCommand: string | undefined;
157
+ for (let i = 0; i < rest.length; i++) {
158
+ if (rest[i] === "--command" && rest[i + 1]) {
159
+ customCommand = rest[i + 1];
160
+ break;
161
+ }
162
+ }
163
+ const customArgs = getFlagValues(rest, "--arg");
164
+
165
+ if (customArgs.length > 0 && !customCommand) {
166
+ error("Use --command with --arg");
167
+ info("Usage: jack agents add <id> [--command cmd] [--arg value]");
168
+ process.exit(1);
148
169
  }
149
170
 
150
171
  try {
151
- await addAgent(agentId, customPath);
172
+ const launchOverride = customCommand
173
+ ? {
174
+ type: "cli" as const,
175
+ command: customCommand,
176
+ args: customArgs.length ? customArgs : undefined,
177
+ }
178
+ : undefined;
179
+
180
+ await addAgent(agentId, { launch: launchOverride });
152
181
 
153
182
  success(`Added ${definition.name}`);
154
183
  const config = await readConfig();
@@ -161,7 +190,9 @@ async function addAgentCommand(args: string[]): Promise<void> {
161
190
  if (err instanceof Error) {
162
191
  error(err.message);
163
192
  if (err.message.includes("Could not detect")) {
164
- info(`Specify path manually: jack agents add ${agentId} --path /path/to/agent`);
193
+ info(
194
+ `Specify launch manually: jack agents add ${agentId} --command /path/to/command`,
195
+ );
165
196
  }
166
197
  }
167
198
  process.exit(1);
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { confirm } from "@inquirer/prompts";
3
+ import { select } from "@inquirer/prompts";
4
4
  import { formatRelativeTime, formatSize } from "../lib/format.ts";
5
5
  import { error, info, item, output as outputSpinner, success } from "../lib/output.ts";
6
6
  import { scanProjectFiles } from "../lib/storage/file-filter.ts";
@@ -200,12 +200,16 @@ async function deleteCommand(args: string[]): Promise<void> {
200
200
  console.error("");
201
201
 
202
202
  // Confirm deletion
203
- const confirmed = await confirm({
203
+ console.error(" Esc to skip\n");
204
+ const action = await select({
204
205
  message: `Delete project '${projectName}' from cloud storage?`,
205
- default: false,
206
+ choices: [
207
+ { name: "1. Yes", value: "yes" },
208
+ { name: "2. No", value: "no" },
209
+ ],
206
210
  });
207
211
 
208
- if (!confirmed) {
212
+ if (action === "no") {
209
213
  info("Cancelled");
210
214
  return;
211
215
  }
@@ -1,5 +1,6 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { join } from "node:path";
2
- import { confirm, select } from "@inquirer/prompts";
3
+ import { select } from "@inquirer/prompts";
3
4
  import {
4
5
  checkWorkerExists,
5
6
  deleteDatabase,
@@ -7,9 +8,58 @@ import {
7
8
  exportDatabase,
8
9
  } from "../lib/cloudflare-api.ts";
9
10
  import { error, info, item, output, success, warn } from "../lib/output.ts";
10
- import { getProject, updateProject } from "../lib/registry.ts";
11
+ import {
12
+ type Project,
13
+ getProject,
14
+ getProjectDatabaseName,
15
+ updateProject,
16
+ updateProjectDatabase,
17
+ } from "../lib/registry.ts";
11
18
  import { deleteCloudProject, getProjectNameFromDir } from "../lib/storage/index.ts";
12
19
 
20
+ /**
21
+ * Get database name for a project, with fallback to wrangler config
22
+ */
23
+ async function resolveDbName(project: Project): Promise<string | null> {
24
+ // First check registry
25
+ const dbFromRegistry = getProjectDatabaseName(project);
26
+ if (dbFromRegistry) {
27
+ return dbFromRegistry;
28
+ }
29
+
30
+ // Fallback: read from wrangler config file
31
+ if (project.localPath && existsSync(project.localPath)) {
32
+ const jsoncPath = join(project.localPath, "wrangler.jsonc");
33
+ if (existsSync(jsoncPath)) {
34
+ try {
35
+ const content = await Bun.file(jsoncPath).text();
36
+ const jsonContent = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
37
+ const config = JSON.parse(jsonContent);
38
+ if (config.d1_databases?.[0]?.database_name) {
39
+ return config.d1_databases[0].database_name;
40
+ }
41
+ } catch {
42
+ // Ignore parse errors
43
+ }
44
+ }
45
+
46
+ const tomlPath = join(project.localPath, "wrangler.toml");
47
+ if (existsSync(tomlPath)) {
48
+ try {
49
+ const content = await Bun.file(tomlPath).text();
50
+ const match = content.match(/database_name\s*=\s*"([^"]+)"/);
51
+ if (match?.[1]) {
52
+ return match[1];
53
+ }
54
+ } catch {
55
+ // Ignore read errors
56
+ }
57
+ }
58
+ }
59
+
60
+ return null;
61
+ }
62
+
13
63
  export interface DownFlags {
14
64
  force?: boolean;
15
65
  }
@@ -32,17 +82,17 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
32
82
  const project = await getProject(name);
33
83
  if (!project) {
34
84
  warn(`Project '${name}' not found in registry`);
35
- info("Will attempt to undeploy worker if it exists");
85
+ info("Will attempt to undeploy if deployed");
36
86
  }
37
87
 
38
88
  // Check if worker exists
39
- output.start("Checking worker...");
89
+ output.start("Checking deployment...");
40
90
  const workerExists = await checkWorkerExists(name);
41
91
  output.stop();
42
92
 
43
93
  if (!workerExists) {
44
94
  console.error("");
45
- warn(`Worker '${name}' not found`);
95
+ warn(`'${name}' is not deployed`);
46
96
  info("Nothing to undeploy");
47
97
  return;
48
98
  }
@@ -50,10 +100,10 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
50
100
  // Force mode - quick deletion without prompts
51
101
  if (flags.force) {
52
102
  console.error("");
53
- info(`Undeploying worker '${name}'`);
103
+ info(`Undeploying '${name}'`);
54
104
  console.error("");
55
105
 
56
- output.start("Deleting worker...");
106
+ output.start("Undeploying...");
57
107
  await deleteWorker(name);
58
108
  output.stop();
59
109
 
@@ -66,8 +116,8 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
66
116
  }
67
117
 
68
118
  console.error("");
69
- success(`Worker '${name}' undeployed`);
70
- info("D1 databases and R2 storage were not affected");
119
+ success(`'${name}' undeployed`);
120
+ info("Databases and cloud storage were not affected");
71
121
  console.error("");
72
122
  return;
73
123
  }
@@ -76,46 +126,47 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
76
126
  console.error("");
77
127
  info(`Project: ${name}`);
78
128
  if (project?.workerUrl) {
79
- item(`Worker URL: ${project.workerUrl}`);
129
+ item(`URL: ${project.workerUrl}`);
80
130
  }
81
- if (project?.resources.d1Databases && project.resources.d1Databases.length > 0) {
82
- item(`D1 Databases: ${project.resources.d1Databases.length}`);
83
- for (const db of project.resources.d1Databases) {
84
- item(` - ${db}`);
85
- }
131
+ const dbName = project ? await resolveDbName(project) : null;
132
+ if (dbName) {
133
+ item(`Database: ${dbName}`);
86
134
  }
87
135
  console.error("");
88
136
 
89
- // Prompt for worker deletion
90
- const workerAction = await select({
91
- message: "What should we do with the worker?",
137
+ // Confirm undeploy
138
+ console.error(" Esc to skip\n");
139
+ const action = await select({
140
+ message: "Undeploy this project?",
92
141
  choices: [
93
- { name: "Delete (undeploy and remove)", value: "delete" },
94
- { name: "Keep (cancel operation)", value: "keep" },
142
+ { name: "1. Yes", value: "yes" },
143
+ { name: "2. No", value: "no" },
95
144
  ],
96
- default: "delete",
97
145
  });
98
146
 
99
- if (workerAction === "keep") {
147
+ if (action === "no") {
100
148
  info("Cancelled");
101
149
  return;
102
150
  }
103
151
 
104
- // Handle D1 databases if they exist
105
- const databases = project?.resources.d1Databases || [];
106
- const databasesToDelete: string[] = [];
152
+ // Handle database if it exists
153
+ let shouldDeleteDb = false;
107
154
 
108
- for (const dbName of databases) {
155
+ if (dbName) {
109
156
  console.error("");
110
- info(`Found D1 database: ${dbName}`);
157
+ info(`Found database: ${dbName}`);
111
158
 
112
159
  // Ask if they want to export first
113
- const shouldExport = await confirm({
160
+ console.error(" Esc to skip\n");
161
+ const exportAction = await select({
114
162
  message: `Export database '${dbName}' before deleting?`,
115
- default: true,
163
+ choices: [
164
+ { name: "1. Yes", value: "yes" },
165
+ { name: "2. No", value: "no" },
166
+ ],
116
167
  });
117
168
 
118
- if (shouldExport) {
169
+ if (exportAction === "yes") {
119
170
  const exportPath = join(process.cwd(), `${dbName}-backup.sql`);
120
171
  output.start(`Exporting database to ${exportPath}...`);
121
172
  try {
@@ -125,11 +176,15 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
125
176
  } catch (err) {
126
177
  output.stop();
127
178
  error(`Failed to export database: ${err instanceof Error ? err.message : String(err)}`);
128
- const continueAnyway = await confirm({
179
+ console.error(" Esc to skip\n");
180
+ const continueAction = await select({
129
181
  message: "Continue without exporting?",
130
- default: false,
182
+ choices: [
183
+ { name: "1. Yes", value: "yes" },
184
+ { name: "2. No", value: "no" },
185
+ ],
131
186
  });
132
- if (!continueAnyway) {
187
+ if (continueAction === "no") {
133
188
  info("Cancelled");
134
189
  return;
135
190
  }
@@ -137,25 +192,31 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
137
192
  }
138
193
 
139
194
  // Ask if they want to delete the database
140
- const shouldDelete = await confirm({
141
- message: `Delete D1 database '${dbName}'?`,
142
- default: false,
195
+ console.error(" Esc to skip\n");
196
+ const deleteAction = await select({
197
+ message: `Delete database '${dbName}'?`,
198
+ choices: [
199
+ { name: "1. Yes", value: "yes" },
200
+ { name: "2. No", value: "no" },
201
+ ],
143
202
  });
144
203
 
145
- if (shouldDelete) {
146
- databasesToDelete.push(dbName);
147
- }
204
+ shouldDeleteDb = deleteAction === "yes";
148
205
  }
149
206
 
150
207
  // Handle R2 backup deletion
151
208
  let shouldDeleteR2 = false;
152
209
  if (project) {
153
210
  console.error("");
154
- const deleteR2Backup = await confirm({
155
- message: "Delete R2 backup storage for this project?",
156
- default: false,
211
+ console.error(" Esc to skip\n");
212
+ const deleteR2Action = await select({
213
+ message: "Delete cloud backup for this project?",
214
+ choices: [
215
+ { name: "1. Yes", value: "yes" },
216
+ { name: "2. No", value: "no" },
217
+ ],
157
218
  });
158
- shouldDeleteR2 = deleteR2Backup;
219
+ shouldDeleteR2 = deleteR2Action === "yes";
159
220
  }
160
221
 
161
222
  // Execute deletions
@@ -163,25 +224,28 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
163
224
  info("Executing cleanup...");
164
225
  console.error("");
165
226
 
166
- // Delete worker
167
- output.start("Deleting worker...");
227
+ // Undeploy service
228
+ output.start("Undeploying...");
168
229
  try {
169
230
  await deleteWorker(name);
170
231
  output.stop();
171
- success(`Worker '${name}' deleted`);
232
+ success(`'${name}' undeployed`);
172
233
  } catch (err) {
173
234
  output.stop();
174
- error(`Failed to delete worker: ${err instanceof Error ? err.message : String(err)}`);
235
+ error(`Failed to undeploy: ${err instanceof Error ? err.message : String(err)}`);
175
236
  process.exit(1);
176
237
  }
177
238
 
178
- // Delete D1 databases
179
- for (const dbName of databasesToDelete) {
239
+ // Delete database if requested
240
+ if (shouldDeleteDb && dbName) {
180
241
  output.start(`Deleting database '${dbName}'...`);
181
242
  try {
182
243
  await deleteDatabase(dbName);
183
244
  output.stop();
184
245
  success(`Database '${dbName}' deleted`);
246
+
247
+ // Update registry
248
+ await updateProjectDatabase(name, null);
185
249
  } catch (err) {
186
250
  output.stop();
187
251
  warn(
@@ -190,42 +254,29 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
190
254
  }
191
255
  }
192
256
 
193
- // Delete R2 backup if requested
257
+ // Delete cloud backup if requested
194
258
  if (shouldDeleteR2) {
195
- output.start("Deleting R2 backup...");
259
+ output.start("Deleting cloud backup...");
196
260
  try {
197
261
  const deleted = await deleteCloudProject(name);
198
262
  output.stop();
199
263
  if (deleted) {
200
- success("R2 backup deleted");
264
+ success("Cloud backup deleted");
201
265
  } else {
202
- warn("No R2 backup found or already deleted");
266
+ warn("No cloud backup found or already deleted");
203
267
  }
204
268
  } catch (err) {
205
269
  output.stop();
206
- warn(`Failed to delete R2 backup: ${err instanceof Error ? err.message : String(err)}`);
270
+ warn(`Failed to delete cloud backup: ${err instanceof Error ? err.message : String(err)}`);
207
271
  }
208
272
  }
209
273
 
210
274
  // Update registry - keep entry but clear worker URL
211
275
  if (project) {
212
- const updates: {
213
- workerUrl: null;
214
- lastDeployed: null;
215
- resources?: { d1Databases: string[] };
216
- } = {
276
+ await updateProject(name, {
217
277
  workerUrl: null,
218
278
  lastDeployed: null,
219
- };
220
-
221
- // Remove deleted databases from registry
222
- if (databasesToDelete.length > 0) {
223
- updates.resources = {
224
- d1Databases: databases.filter((db) => !databasesToDelete.includes(db)),
225
- };
226
- }
227
-
228
- await updateProject(name, updates);
279
+ });
229
280
  }
230
281
 
231
282
  console.error("");
@@ -5,6 +5,11 @@ import {
5
5
  updateAgent,
6
6
  } from "../lib/agents.ts";
7
7
  import { readConfig, writeConfig } from "../lib/config.ts";
8
+ import {
9
+ getIdeDisplayName,
10
+ installMcpConfigsToAllIdes,
11
+ saveMcpConfig,
12
+ } from "../lib/mcp-config.ts";
8
13
  import { info, item, spinner, success } from "../lib/output.ts";
9
14
  import { ensureAuth, ensureWrangler, isAuthenticated } from "../lib/wrangler.ts";
10
15
 
@@ -14,7 +19,11 @@ export async function isInitialized(): Promise<boolean> {
14
19
  return await isAuthenticated();
15
20
  }
16
21
 
17
- export default async function init(): Promise<void> {
22
+ interface InitOptions {
23
+ skipMcp?: boolean;
24
+ }
25
+
26
+ export default async function init(options: InitOptions = {}): Promise<void> {
18
27
  // Immediate feedback
19
28
  const spin = spinner("Checking setup...");
20
29
 
@@ -47,7 +56,7 @@ export default async function init(): Promise<void> {
47
56
  let preferredAgent: string | undefined;
48
57
  if (detectionResult.detected.length > 0) {
49
58
  success(`Found ${detectionResult.detected.length} agent(s)`);
50
- for (const { id, path } of detectionResult.detected) {
59
+ for (const { id, path, launch } of detectionResult.detected) {
51
60
  const definition = getAgentDefinition(id);
52
61
  item(`${definition?.name}: ${path}`);
53
62
 
@@ -56,6 +65,7 @@ export default async function init(): Promise<void> {
56
65
  active: true,
57
66
  path: path,
58
67
  detectedAt: new Date().toISOString(),
68
+ launch,
59
69
  });
60
70
  }
61
71
 
@@ -69,7 +79,35 @@ export default async function init(): Promise<void> {
69
79
  info("No agents detected (you can add them later with: jack agents add)");
70
80
  }
71
81
 
72
- // Step 4: Save config (preserve existing agents, just update init status)
82
+ // Step 4: Install MCP configs to detected IDEs (unless --skip-mcp)
83
+ if (!options.skipMcp) {
84
+ const mcpSpinner = spinner("Installing MCP server configs...");
85
+ try {
86
+ const installedIdes = await installMcpConfigsToAllIdes();
87
+ mcpSpinner.stop();
88
+
89
+ if (installedIdes.length > 0) {
90
+ success(`MCP server installed to ${installedIdes.length} IDE(s)`);
91
+ for (const ideId of installedIdes) {
92
+ item(` ${getIdeDisplayName(ideId)}`);
93
+ }
94
+ } else {
95
+ info("No supported IDEs detected for MCP installation");
96
+ }
97
+ } catch (err) {
98
+ mcpSpinner.stop();
99
+ // Don't fail init if MCP install fails - just warn
100
+ info("Could not install MCP configs (non-critical)");
101
+ }
102
+
103
+ try {
104
+ await saveMcpConfig();
105
+ } catch {
106
+ // Non-critical; config persistence shouldn't block init
107
+ }
108
+ }
109
+
110
+ // Step 5: Save config (preserve existing agents, just update init status)
73
111
  const existingConfig = await readConfig();
74
112
  await writeConfig({
75
113
  version: 1,
@@ -0,0 +1,18 @@
1
+ import { error, info } from "../lib/output.ts";
2
+ import { startMcpServer } from "../mcp/server.ts";
3
+
4
+ interface McpOptions {
5
+ project?: string;
6
+ }
7
+
8
+ export default async function mcp(subcommand?: string, options: McpOptions = {}): Promise<void> {
9
+ if (subcommand !== "serve") {
10
+ error("Unknown subcommand. Use: jack mcp serve");
11
+ info("Usage: jack mcp serve [--project /path/to/project]");
12
+ process.exit(1);
13
+ }
14
+
15
+ await startMcpServer({
16
+ projectPath: options.project,
17
+ });
18
+ }