@getjack/jack 0.1.28 → 0.1.30

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.
Files changed (125) hide show
  1. package/package.json +1 -1
  2. package/src/commands/cd.ts +163 -0
  3. package/src/commands/clone.ts +112 -68
  4. package/src/commands/domain.ts +506 -0
  5. package/src/commands/domains.ts +215 -0
  6. package/src/commands/down.ts +18 -12
  7. package/src/commands/hack.ts +185 -8
  8. package/src/commands/init.ts +52 -1
  9. package/src/commands/link.ts +25 -43
  10. package/src/commands/logs.ts +2 -2
  11. package/src/commands/mcp.ts +74 -3
  12. package/src/commands/new.ts +48 -54
  13. package/src/commands/projects.ts +53 -10
  14. package/src/commands/secrets.ts +5 -1
  15. package/src/commands/services.ts +16 -4
  16. package/src/commands/shell-init.ts +43 -0
  17. package/src/commands/ship.ts +2 -11
  18. package/src/commands/skills.ts +335 -0
  19. package/src/commands/update.ts +31 -0
  20. package/src/commands/upgrade.ts +14 -0
  21. package/src/index.ts +116 -24
  22. package/src/lib/agent-integration.ts +1 -2
  23. package/src/lib/agents.ts +2 -2
  24. package/src/lib/auth/login-flow.ts +1 -1
  25. package/src/lib/clone-core.ts +252 -0
  26. package/src/lib/config.ts +22 -0
  27. package/src/lib/control-plane.ts +31 -5
  28. package/src/lib/fuzzy.ts +93 -0
  29. package/src/lib/managed-deploy.ts +4 -1
  30. package/src/lib/managed-down.ts +20 -5
  31. package/src/lib/output.ts +90 -9
  32. package/src/lib/picker.ts +406 -0
  33. package/src/lib/project-detection.ts +5 -2
  34. package/src/lib/project-list.ts +66 -5
  35. package/src/lib/project-operations.ts +68 -6
  36. package/src/lib/prompts.ts +1 -1
  37. package/src/lib/services/db-execute.ts +8 -1
  38. package/src/lib/services/db-list.ts +4 -1
  39. package/src/lib/services/domain-operations.ts +379 -0
  40. package/src/lib/services/storage-config.ts +1 -5
  41. package/src/lib/services/storage-delete.ts +1 -1
  42. package/src/lib/services/storage-info.ts +2 -4
  43. package/src/lib/services/vectorize-config.ts +1 -5
  44. package/src/lib/services/vectorize-create.ts +3 -1
  45. package/src/lib/shell-integration.ts +202 -0
  46. package/src/lib/telemetry-config.ts +50 -4
  47. package/src/lib/telemetry.ts +71 -2
  48. package/src/lib/version-check.ts +1 -3
  49. package/src/lib/wrangler-config.test.ts +2 -2
  50. package/src/lib/wrangler-config.ts +1 -1
  51. package/src/lib/zip-packager.ts +1 -3
  52. package/src/mcp/tools/index.ts +261 -7
  53. package/src/templates/index.ts +10 -1
  54. package/templates/ai-chat/.jack.json +1 -5
  55. package/templates/ai-chat/public/chat.js +130 -130
  56. package/templates/ai-chat/src/index.ts +9 -13
  57. package/templates/ai-chat/src/jack-ai.ts +6 -2
  58. package/templates/saas/.jack.json +6 -1
  59. package/templates/saas/src/auth.ts +8 -4
  60. package/templates/saas/src/client/App.tsx +22 -7
  61. package/templates/saas/src/client/components/ProtectedRoute.tsx +9 -2
  62. package/templates/saas/src/client/components/ThemeToggle.tsx +1 -6
  63. package/templates/saas/src/client/components/ui/accordion.tsx +1 -1
  64. package/templates/saas/src/client/components/ui/alert-dialog.tsx +2 -2
  65. package/templates/saas/src/client/components/ui/alert.tsx +2 -2
  66. package/templates/saas/src/client/components/ui/avatar.tsx +1 -1
  67. package/templates/saas/src/client/components/ui/badge.tsx +2 -2
  68. package/templates/saas/src/client/components/ui/breadcrumb.tsx +1 -1
  69. package/templates/saas/src/client/components/ui/button-group.tsx +2 -2
  70. package/templates/saas/src/client/components/ui/button.tsx +2 -2
  71. package/templates/saas/src/client/components/ui/card.tsx +1 -1
  72. package/templates/saas/src/client/components/ui/carousel.tsx +2 -2
  73. package/templates/saas/src/client/components/ui/checkbox.tsx +1 -1
  74. package/templates/saas/src/client/components/ui/command.tsx +2 -2
  75. package/templates/saas/src/client/components/ui/context-menu.tsx +1 -1
  76. package/templates/saas/src/client/components/ui/dialog.tsx +1 -1
  77. package/templates/saas/src/client/components/ui/drawer.tsx +1 -1
  78. package/templates/saas/src/client/components/ui/dropdown-menu.tsx +1 -1
  79. package/templates/saas/src/client/components/ui/empty.tsx +1 -1
  80. package/templates/saas/src/client/components/ui/field.tsx +2 -2
  81. package/templates/saas/src/client/components/ui/form.tsx +5 -5
  82. package/templates/saas/src/client/components/ui/hover-card.tsx +1 -1
  83. package/templates/saas/src/client/components/ui/input-group.tsx +3 -3
  84. package/templates/saas/src/client/components/ui/input-otp.tsx +1 -1
  85. package/templates/saas/src/client/components/ui/input.tsx +1 -1
  86. package/templates/saas/src/client/components/ui/item.tsx +3 -3
  87. package/templates/saas/src/client/components/ui/label.tsx +1 -1
  88. package/templates/saas/src/client/components/ui/menubar.tsx +1 -1
  89. package/templates/saas/src/client/components/ui/navigation-menu.tsx +1 -1
  90. package/templates/saas/src/client/components/ui/pagination.tsx +2 -2
  91. package/templates/saas/src/client/components/ui/popover.tsx +1 -1
  92. package/templates/saas/src/client/components/ui/progress.tsx +1 -1
  93. package/templates/saas/src/client/components/ui/radio-group.tsx +1 -1
  94. package/templates/saas/src/client/components/ui/resizable.tsx +1 -1
  95. package/templates/saas/src/client/components/ui/scroll-area.tsx +1 -1
  96. package/templates/saas/src/client/components/ui/select.tsx +1 -1
  97. package/templates/saas/src/client/components/ui/separator.tsx +1 -1
  98. package/templates/saas/src/client/components/ui/sheet.tsx +1 -1
  99. package/templates/saas/src/client/components/ui/sidebar.tsx +4 -4
  100. package/templates/saas/src/client/components/ui/slider.tsx +1 -1
  101. package/templates/saas/src/client/components/ui/switch.tsx +1 -1
  102. package/templates/saas/src/client/components/ui/table.tsx +1 -1
  103. package/templates/saas/src/client/components/ui/tabs.tsx +1 -1
  104. package/templates/saas/src/client/components/ui/textarea.tsx +1 -1
  105. package/templates/saas/src/client/components/ui/toggle-group.tsx +3 -3
  106. package/templates/saas/src/client/components/ui/toggle.tsx +2 -2
  107. package/templates/saas/src/client/components/ui/tooltip.tsx +1 -1
  108. package/templates/saas/src/client/hooks/useSubscription.ts +5 -4
  109. package/templates/saas/src/client/lib/auth-client.ts +1 -1
  110. package/templates/saas/src/client/lib/plans.ts +1 -6
  111. package/templates/saas/src/client/lib/utils.ts +1 -1
  112. package/templates/saas/src/client/main.tsx +1 -1
  113. package/templates/saas/src/client/pages/DashboardPage.tsx +41 -9
  114. package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +11 -2
  115. package/templates/saas/src/client/pages/HomePage.tsx +11 -2
  116. package/templates/saas/src/client/pages/LoginPage.tsx +11 -2
  117. package/templates/saas/src/client/pages/PricingPage.tsx +20 -10
  118. package/templates/saas/src/client/pages/ResetPasswordPage.tsx +14 -11
  119. package/templates/saas/src/client/pages/SignupPage.tsx +11 -2
  120. package/templates/saas/src/index.ts +28 -19
  121. package/templates/saas/vite.config.ts +1 -1
  122. package/templates/semantic-search/.jack.json +1 -5
  123. package/templates/semantic-search/src/index.ts +8 -4
  124. package/templates/semantic-search/src/jack-ai.ts +6 -2
  125. package/templates/semantic-search/src/jack-vectorize.ts +5 -1
@@ -4,12 +4,16 @@ import { join } from "node:path";
4
4
  import { CONFIG_DIR } from "./config.ts";
5
5
 
6
6
  /**
7
- * Telemetry configuration structure
7
+ * Telemetry configuration structure (v2)
8
8
  */
9
9
  export interface TelemetryConfig {
10
10
  anonymousId: string; // UUID v4, generated once
11
11
  enabled: boolean; // false if user opted out
12
- version: number; // config schema version (start at 1)
12
+ version: number; // config schema version (2 for new configs)
13
+ // AARRR tracking fields
14
+ firstSeenAt: string; // ISO date when config was created
15
+ firstDeployAt?: string; // ISO date when first deploy succeeded
16
+ lastIdentifyDate?: string; // "YYYY-MM-DD" for dedupe
13
17
  }
14
18
 
15
19
  export const TELEMETRY_CONFIG_DIR = CONFIG_DIR;
@@ -37,13 +41,25 @@ async function ensureTelemetryConfigDir(): Promise<void> {
37
41
  }
38
42
 
39
43
  /**
40
- * Create a new telemetry config with generated anonymous ID
44
+ * Create a new telemetry config with generated anonymous ID (v2)
41
45
  */
42
46
  function createNewTelemetryConfig(): TelemetryConfig {
43
47
  return {
44
48
  anonymousId: crypto.randomUUID(),
45
49
  enabled: true,
46
- version: 1,
50
+ version: 2,
51
+ firstSeenAt: new Date().toISOString(),
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Migrate v1 config to v2 by adding AARRR tracking fields
57
+ */
58
+ function migrateV1ToV2(config: TelemetryConfig): TelemetryConfig {
59
+ return {
60
+ ...config,
61
+ version: 2,
62
+ firstSeenAt: new Date().toISOString(), // Best approximation for existing users
47
63
  };
48
64
  }
49
65
 
@@ -55,6 +71,19 @@ export async function loadOrCreateTelemetryConfig(): Promise<TelemetryConfig> {
55
71
  const newConfig = createNewTelemetryConfig();
56
72
  await ensureTelemetryConfigDir();
57
73
  await Bun.write(TELEMETRY_CONFIG_PATH, JSON.stringify(newConfig, null, 2));
74
+
75
+ // Track install event for new users (fire-and-forget)
76
+ queueMicrotask(async () => {
77
+ try {
78
+ const { track, Events } = await import("./telemetry.ts");
79
+ track(Events.USER_INSTALLED, {
80
+ install_date: newConfig.firstSeenAt,
81
+ });
82
+ } catch {
83
+ // Ignore - telemetry should not break CLI
84
+ }
85
+ });
86
+
58
87
  return newConfig;
59
88
  }
60
89
 
@@ -68,6 +97,12 @@ export async function loadOrCreateTelemetryConfig(): Promise<TelemetryConfig> {
68
97
  typeof config.enabled === "boolean" &&
69
98
  typeof config.version === "number"
70
99
  ) {
100
+ // Migrate v1 configs to v2
101
+ if (config.version === 1) {
102
+ const migratedConfig = migrateV1ToV2(config as TelemetryConfig);
103
+ await Bun.write(TELEMETRY_CONFIG_PATH, JSON.stringify(migratedConfig, null, 2));
104
+ return migratedConfig;
105
+ }
71
106
  return config as TelemetryConfig;
72
107
  }
73
108
  // Invalid config, regenerate
@@ -108,3 +143,14 @@ export async function setTelemetryEnabled(enabled: boolean): Promise<void> {
108
143
  // Update cache
109
144
  cachedTelemetryConfig = config;
110
145
  }
146
+
147
+ /**
148
+ * Save telemetry config to disk
149
+ */
150
+ export async function saveTelemetryConfig(config: TelemetryConfig): Promise<void> {
151
+ await ensureTelemetryConfigDir();
152
+ await Bun.write(TELEMETRY_CONFIG_PATH, JSON.stringify(config, null, 2));
153
+
154
+ // Update cache
155
+ cachedTelemetryConfig = config;
156
+ }
@@ -35,7 +35,15 @@ export const Events = {
35
35
  AUTO_DETECT_REJECTED: "auto_detect_rejected",
36
36
  // Service events
37
37
  SERVICE_CREATED: "service_created",
38
+ SERVICE_DELETED: "service_deleted",
38
39
  SQL_EXECUTED: "sql_executed",
40
+ // AARRR lifecycle events
41
+ USER_INSTALLED: "user_installed",
42
+ USER_ACTIVATED: "user_activated",
43
+ // BYO deploy events (parity with managed)
44
+ BYO_DEPLOY_STARTED: "byo_deploy_started",
45
+ BYO_DEPLOY_COMPLETED: "byo_deploy_completed",
46
+ BYO_DEPLOY_FAILED: "byo_deploy_failed",
39
47
  } as const;
40
48
 
41
49
  type EventName = (typeof Events)[keyof typeof Events];
@@ -121,7 +129,7 @@ export interface UserProperties {
121
129
  config_style?: "byoc" | "jack-cloud";
122
130
  }
123
131
 
124
- // Detect environment properties
132
+ // Detect environment properties (for user profile - stable properties)
125
133
  export function getEnvironmentProps(): Pick<
126
134
  UserProperties,
127
135
  "shell" | "terminal" | "terminal_width" | "is_tty" | "locale"
@@ -135,6 +143,25 @@ export function getEnvironmentProps(): Pick<
135
143
  };
136
144
  }
137
145
 
146
+ // ============================================
147
+ // INVOCATION CONTEXT (per-event, not per-user)
148
+ // ============================================
149
+ export interface InvocationContext {
150
+ is_tty: boolean;
151
+ is_ci: boolean;
152
+ terminal?: string;
153
+ shell?: string;
154
+ }
155
+
156
+ export function getInvocationContext(): InvocationContext {
157
+ return {
158
+ is_tty: process.stdout.isTTY ?? false,
159
+ is_ci: !!process.env.CI,
160
+ terminal: process.env.TERM_PROGRAM,
161
+ shell: process.env.SHELL?.split("/").pop(),
162
+ };
163
+ }
164
+
138
165
  export async function identify(properties: Partial<UserProperties>): Promise<void> {
139
166
  userProps = { ...userProps, ...properties };
140
167
  if (!(await isEnabled())) return;
@@ -203,11 +230,42 @@ export async function track(event: EventName, properties?: Record<string, unknow
203
230
  }
204
231
  }
205
232
 
233
+ export async function trackActivationIfFirst(deployMode: "managed" | "byo"): Promise<void> {
234
+ try {
235
+ const { getTelemetryConfig, saveTelemetryConfig } = await import("./telemetry-config.ts");
236
+ const config = await getTelemetryConfig();
237
+
238
+ // Already activated? Skip
239
+ if (config.firstDeployAt) {
240
+ return;
241
+ }
242
+
243
+ const now = new Date();
244
+ const firstSeen = config.firstSeenAt ? new Date(config.firstSeenAt) : now;
245
+ const daysToActivate = Math.floor(
246
+ (now.getTime() - firstSeen.getTime()) / (1000 * 60 * 60 * 24),
247
+ );
248
+
249
+ // Fire activation event
250
+ track(Events.USER_ACTIVATED, {
251
+ deploy_mode: deployMode,
252
+ days_to_activate: daysToActivate,
253
+ });
254
+
255
+ // Save activation timestamp
256
+ config.firstDeployAt = now.toISOString();
257
+ await saveTelemetryConfig(config);
258
+ } catch {
259
+ // Ignore - telemetry should not break CLI
260
+ }
261
+ }
262
+
206
263
  // ============================================
207
264
  // WRAPPER
208
265
  // ============================================
209
266
  export interface TelemetryOptions {
210
267
  platform?: "cli" | "mcp";
268
+ subcommand?: string;
211
269
  }
212
270
 
213
271
  // biome-ignore lint/suspicious/noExplicitAny: Required for flexible command wrapping
@@ -217,9 +275,16 @@ export function withTelemetry<T extends (...args: any[]) => Promise<any>>(
217
275
  options?: TelemetryOptions,
218
276
  ): T {
219
277
  const platform = options?.platform ?? "cli";
278
+ const subcommand = options?.subcommand;
220
279
 
221
280
  return (async (...args: Parameters<T>) => {
222
- track(Events.COMMAND_INVOKED, { command: commandName, platform });
281
+ const context = getInvocationContext();
282
+ track(Events.COMMAND_INVOKED, {
283
+ command: commandName,
284
+ platform,
285
+ ...(subcommand && { subcommand }),
286
+ ...context,
287
+ });
223
288
  const start = Date.now();
224
289
 
225
290
  try {
@@ -227,15 +292,19 @@ export function withTelemetry<T extends (...args: any[]) => Promise<any>>(
227
292
  track(Events.COMMAND_COMPLETED, {
228
293
  command: commandName,
229
294
  platform,
295
+ ...(subcommand && { subcommand }),
230
296
  duration_ms: Date.now() - start,
297
+ ...context,
231
298
  });
232
299
  return result;
233
300
  } catch (error) {
234
301
  track(Events.COMMAND_FAILED, {
235
302
  command: commandName,
236
303
  platform,
304
+ ...(subcommand && { subcommand }),
237
305
  error_type: classifyError(error),
238
306
  duration_ms: Date.now() - start,
307
+ ...context,
239
308
  });
240
309
  throw error;
241
310
  }
@@ -87,9 +87,7 @@ function isNewerVersion(latest: string, current: string): boolean {
87
87
  * Returns the latest version if newer, null otherwise
88
88
  * @param skipCache - If true, bypass the cache and always fetch from npm
89
89
  */
90
- export async function checkForUpdate(
91
- skipCache = false,
92
- ): Promise<string | null> {
90
+ export async function checkForUpdate(skipCache = false): Promise<string | null> {
93
91
  const currentVersion = getCurrentVersion();
94
92
  const now = Date.now();
95
93
 
@@ -6,9 +6,9 @@
6
6
 
7
7
  import { afterEach, beforeEach, describe, expect, it } from "bun:test";
8
8
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
9
- import { join } from "node:path";
10
9
  import { tmpdir } from "node:os";
11
- import { addD1Binding, getExistingD1Bindings, type D1BindingConfig } from "./wrangler-config.ts";
10
+ import { join } from "node:path";
11
+ import { type D1BindingConfig, addD1Binding, getExistingD1Bindings } from "./wrangler-config.ts";
12
12
 
13
13
  // ============================================================================
14
14
  // Test Helpers
@@ -387,7 +387,7 @@ function findLastObjectEndInArray(content: string, startIndex: number, endIndex:
387
387
  function shouldAddCommaBefore(content: string): boolean {
388
388
  // Strip trailing comments and whitespace to find last meaningful char
389
389
  let i = content.length - 1;
390
- let inLineComment = false;
390
+ const inLineComment = false;
391
391
 
392
392
  // First pass: find where any trailing line comment starts
393
393
  for (let j = content.length - 1; j >= 0; j--) {
@@ -54,9 +54,7 @@ export interface ManifestData {
54
54
  };
55
55
  }
56
56
 
57
- export interface ZipProgressCallback {
58
- (current: number, total: number): void;
59
- }
57
+ export type ZipProgressCallback = (current: number, total: number) => void;
60
58
 
61
59
  /**
62
60
  * Creates a ZIP archive from source directory
@@ -1,9 +1,9 @@
1
1
  import type { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3
3
  import { z } from "zod";
4
- import { JackError, JackErrorCode } from "../../lib/errors.ts";
5
4
  import { authFetch } from "../../lib/auth/index.ts";
6
5
  import { getControlApiUrl, startLogSession } from "../../lib/control-plane.ts";
6
+ import { JackError, JackErrorCode } from "../../lib/errors.ts";
7
7
  import { getDeployMode, getProjectId } from "../../lib/project-link.ts";
8
8
  import { createProject, deployProject, getProjectStatus } from "../../lib/project-operations.ts";
9
9
  import { listAllProjects } from "../../lib/project-resolver.ts";
@@ -15,6 +15,13 @@ import {
15
15
  wrapResultsForMcp,
16
16
  } from "../../lib/services/db-execute.ts";
17
17
  import { listDatabases } from "../../lib/services/db-list.ts";
18
+ import {
19
+ assignDomain,
20
+ connectDomain,
21
+ disconnectDomain,
22
+ listDomains,
23
+ unassignDomain,
24
+ } from "../../lib/services/domain-operations.ts";
18
25
  import { createStorageBucket } from "../../lib/services/storage-create.ts";
19
26
  import { deleteStorageBucket } from "../../lib/services/storage-delete.ts";
20
27
  import { getStorageBucketInfo } from "../../lib/services/storage-info.ts";
@@ -189,6 +196,25 @@ const TailLogsSchema = z.object({
189
196
  .describe("How long to listen before returning (default: 2000ms, max: 10000ms)"),
190
197
  });
191
198
 
199
+ const ListDomainsSchema = z.object({});
200
+
201
+ const ConnectDomainSchema = z.object({
202
+ hostname: z.string().describe("The domain hostname to connect (e.g., 'app.example.com')"),
203
+ });
204
+
205
+ const AssignDomainSchema = z.object({
206
+ hostname: z.string().describe("The domain hostname to assign"),
207
+ project_slug: z.string().describe("The project slug to assign the domain to"),
208
+ });
209
+
210
+ const UnassignDomainSchema = z.object({
211
+ hostname: z.string().describe("The domain hostname to unassign from its project"),
212
+ });
213
+
214
+ const DisconnectDomainSchema = z.object({
215
+ hostname: z.string().describe("The domain hostname to disconnect (fully remove)"),
216
+ });
217
+
192
218
  export function registerTools(server: McpServer, _options: McpServerOptions, debug: DebugLogger) {
193
219
  // Register tool list handler
194
220
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -512,6 +538,79 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
512
538
  required: ["name"],
513
539
  },
514
540
  },
541
+ {
542
+ name: "list_domains",
543
+ description:
544
+ "List all custom domains for the current user, including their status and assigned projects.",
545
+ inputSchema: {
546
+ type: "object",
547
+ properties: {},
548
+ },
549
+ },
550
+ {
551
+ name: "connect_domain",
552
+ description:
553
+ "Reserve a custom domain slot. This is the first step before assigning the domain to a project.",
554
+ inputSchema: {
555
+ type: "object",
556
+ properties: {
557
+ hostname: {
558
+ type: "string",
559
+ description: "The domain hostname to connect (e.g., 'app.example.com')",
560
+ },
561
+ },
562
+ required: ["hostname"],
563
+ },
564
+ },
565
+ {
566
+ name: "assign_domain",
567
+ description:
568
+ "Assign a reserved domain to a project. The domain must be connected first. Returns DNS verification instructions.",
569
+ inputSchema: {
570
+ type: "object",
571
+ properties: {
572
+ hostname: {
573
+ type: "string",
574
+ description: "The domain hostname to assign",
575
+ },
576
+ project_slug: {
577
+ type: "string",
578
+ description: "The project slug to assign the domain to",
579
+ },
580
+ },
581
+ required: ["hostname", "project_slug"],
582
+ },
583
+ },
584
+ {
585
+ name: "unassign_domain",
586
+ description:
587
+ "Unassign a domain from its project, keeping the domain slot reserved for future use.",
588
+ inputSchema: {
589
+ type: "object",
590
+ properties: {
591
+ hostname: {
592
+ type: "string",
593
+ description: "The domain hostname to unassign from its project",
594
+ },
595
+ },
596
+ required: ["hostname"],
597
+ },
598
+ },
599
+ {
600
+ name: "disconnect_domain",
601
+ description:
602
+ "Fully remove a domain, releasing the slot. Use this when you no longer need the domain.",
603
+ inputSchema: {
604
+ type: "object",
605
+ properties: {
606
+ hostname: {
607
+ type: "string",
608
+ description: "The domain hostname to disconnect (fully remove)",
609
+ },
610
+ },
611
+ required: ["hostname"],
612
+ },
613
+ },
515
614
  ],
516
615
  };
517
616
  });
@@ -736,7 +835,12 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
736
835
 
737
836
  const wrappedTailLogs = withTelemetry(
738
837
  "tail_logs",
739
- async (id: string, label: string | undefined, maxEvents: number, durationMs: number) => {
838
+ async (
839
+ id: string,
840
+ label: string | undefined,
841
+ maxEvents: number,
842
+ durationMs: number,
843
+ ) => {
740
844
  const session = await startLogSession(id, label);
741
845
  const streamUrl = `${getControlApiUrl()}${session.stream.url}`;
742
846
 
@@ -949,10 +1053,7 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
949
1053
  return {
950
1054
  success: false,
951
1055
  error: err.message,
952
- suggestion:
953
- "Destructive operations (DROP, TRUNCATE, ALTER, DELETE without WHERE) " +
954
- "must be run via CLI with explicit confirmation: " +
955
- `jack services db execute "${sql.slice(0, 50)}..." --write`,
1056
+ suggestion: `Destructive operations (DROP, TRUNCATE, ALTER, DELETE without WHERE) must be run via CLI with explicit confirmation: jack services db execute "${sql.slice(0, 50)}..." --write`,
956
1057
  risk_level: "destructive",
957
1058
  };
958
1059
  }
@@ -1250,7 +1351,9 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
1250
1351
  if (!info) {
1251
1352
  throw new JackError(
1252
1353
  JackErrorCode.RESOURCE_NOT_FOUND,
1253
- bucketName ? `Storage bucket '${bucketName}' not found` : "No storage buckets found",
1354
+ bucketName
1355
+ ? `Storage bucket '${bucketName}' not found`
1356
+ : "No storage buckets found",
1254
1357
  "Use list_storage_buckets to see available buckets or create_storage_bucket to create one",
1255
1358
  );
1256
1359
  }
@@ -1312,6 +1415,157 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
1312
1415
  };
1313
1416
  }
1314
1417
 
1418
+ case "list_domains": {
1419
+ ListDomainsSchema.parse(request.params.arguments ?? {});
1420
+
1421
+ const wrappedListDomains = withTelemetry(
1422
+ "list_domains",
1423
+ async () => {
1424
+ const result = await listDomains();
1425
+ return {
1426
+ domains: result.domains.map((d) => ({
1427
+ id: d.id,
1428
+ hostname: d.hostname,
1429
+ status: d.status,
1430
+ ssl_status: d.ssl_status,
1431
+ project_id: d.project_id,
1432
+ project_slug: d.project_slug,
1433
+ created_at: d.created_at,
1434
+ })),
1435
+ slots: result.slots,
1436
+ };
1437
+ },
1438
+ { platform: "mcp" },
1439
+ );
1440
+
1441
+ const result = await wrappedListDomains();
1442
+
1443
+ return {
1444
+ content: [
1445
+ {
1446
+ type: "text",
1447
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
1448
+ },
1449
+ ],
1450
+ };
1451
+ }
1452
+
1453
+ case "connect_domain": {
1454
+ const args = ConnectDomainSchema.parse(request.params.arguments ?? {});
1455
+
1456
+ const wrappedConnectDomain = withTelemetry(
1457
+ "connect_domain",
1458
+ async (hostname: string) => {
1459
+ const result = await connectDomain(hostname);
1460
+ return {
1461
+ id: result.id,
1462
+ hostname: result.hostname,
1463
+ status: result.status,
1464
+ };
1465
+ },
1466
+ { platform: "mcp" },
1467
+ );
1468
+
1469
+ const result = await wrappedConnectDomain(args.hostname);
1470
+
1471
+ return {
1472
+ content: [
1473
+ {
1474
+ type: "text",
1475
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
1476
+ },
1477
+ ],
1478
+ };
1479
+ }
1480
+
1481
+ case "assign_domain": {
1482
+ const args = AssignDomainSchema.parse(request.params.arguments ?? {});
1483
+
1484
+ const wrappedAssignDomain = withTelemetry(
1485
+ "assign_domain",
1486
+ async (hostname: string, projectSlug: string) => {
1487
+ const result = await assignDomain(hostname, projectSlug);
1488
+ return {
1489
+ id: result.id,
1490
+ hostname: result.hostname,
1491
+ status: result.status,
1492
+ ssl_status: result.ssl_status,
1493
+ project_id: result.project_id,
1494
+ project_slug: result.project_slug,
1495
+ verification: result.verification,
1496
+ ownership_verification: result.ownership_verification,
1497
+ };
1498
+ },
1499
+ { platform: "mcp" },
1500
+ );
1501
+
1502
+ const result = await wrappedAssignDomain(args.hostname, args.project_slug);
1503
+
1504
+ return {
1505
+ content: [
1506
+ {
1507
+ type: "text",
1508
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
1509
+ },
1510
+ ],
1511
+ };
1512
+ }
1513
+
1514
+ case "unassign_domain": {
1515
+ const args = UnassignDomainSchema.parse(request.params.arguments ?? {});
1516
+
1517
+ const wrappedUnassignDomain = withTelemetry(
1518
+ "unassign_domain",
1519
+ async (hostname: string) => {
1520
+ const result = await unassignDomain(hostname);
1521
+ return {
1522
+ id: result.id,
1523
+ hostname: result.hostname,
1524
+ status: result.status,
1525
+ };
1526
+ },
1527
+ { platform: "mcp" },
1528
+ );
1529
+
1530
+ const result = await wrappedUnassignDomain(args.hostname);
1531
+
1532
+ return {
1533
+ content: [
1534
+ {
1535
+ type: "text",
1536
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
1537
+ },
1538
+ ],
1539
+ };
1540
+ }
1541
+
1542
+ case "disconnect_domain": {
1543
+ const args = DisconnectDomainSchema.parse(request.params.arguments ?? {});
1544
+
1545
+ const wrappedDisconnectDomain = withTelemetry(
1546
+ "disconnect_domain",
1547
+ async (hostname: string) => {
1548
+ const result = await disconnectDomain(hostname);
1549
+ return {
1550
+ success: result.success,
1551
+ hostname: result.hostname,
1552
+ };
1553
+ },
1554
+ { platform: "mcp" },
1555
+ );
1556
+
1557
+ const result = await wrappedDisconnectDomain(args.hostname);
1558
+
1559
+ return {
1560
+ content: [
1561
+ {
1562
+ type: "text",
1563
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
1564
+ },
1565
+ ],
1566
+ };
1567
+ }
1568
+
1315
1569
  default:
1316
1570
  throw new Error(`Unknown tool: ${toolName}`);
1317
1571
  }
@@ -10,7 +10,16 @@ import type { Template } from "./types";
10
10
  // Resolve templates directory relative to this file (src/templates -> templates)
11
11
  const TEMPLATES_DIR = join(dirname(dirname(import.meta.dir)), "templates");
12
12
 
13
- export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api", "nextjs", "saas", "simple-api-starter", "ai-chat", "semantic-search"];
13
+ export const BUILTIN_TEMPLATES = [
14
+ "hello",
15
+ "miniapp",
16
+ "api",
17
+ "nextjs",
18
+ "saas",
19
+ "simple-api-starter",
20
+ "ai-chat",
21
+ "semantic-search",
22
+ ];
14
23
 
15
24
  /**
16
25
  * Resolved template with origin tracking for lineage
@@ -17,11 +17,7 @@
17
17
  {
18
18
  "action": "box",
19
19
  "title": "{{name}}",
20
- "lines": [
21
- "{{url}}",
22
- "",
23
- "jack open to view in browser"
24
- ]
20
+ "lines": ["{{url}}", "", "jack open to view in browser"]
25
21
  }
26
22
  ]
27
23
  }