@getjack/jack 0.1.2 → 0.1.3

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 (91) hide show
  1. package/package.json +54 -47
  2. package/src/commands/agents.ts +145 -10
  3. package/src/commands/down.ts +110 -102
  4. package/src/commands/feedback.ts +189 -0
  5. package/src/commands/init.ts +8 -12
  6. package/src/commands/login.ts +88 -0
  7. package/src/commands/logout.ts +14 -0
  8. package/src/commands/logs.ts +21 -0
  9. package/src/commands/mcp.ts +134 -7
  10. package/src/commands/new.ts +43 -17
  11. package/src/commands/open.ts +13 -6
  12. package/src/commands/projects.ts +269 -143
  13. package/src/commands/secrets.ts +413 -0
  14. package/src/commands/services.ts +96 -123
  15. package/src/commands/ship.ts +5 -1
  16. package/src/commands/whoami.ts +31 -0
  17. package/src/index.ts +218 -144
  18. package/src/lib/agent-files.ts +34 -0
  19. package/src/lib/agents.ts +390 -22
  20. package/src/lib/asset-hash.ts +50 -0
  21. package/src/lib/auth/client.ts +115 -0
  22. package/src/lib/auth/constants.ts +5 -0
  23. package/src/lib/auth/guard.ts +57 -0
  24. package/src/lib/auth/index.ts +18 -0
  25. package/src/lib/auth/store.ts +54 -0
  26. package/src/lib/binding-validator.ts +136 -0
  27. package/src/lib/build-helper.ts +211 -0
  28. package/src/lib/cloudflare-api.ts +24 -0
  29. package/src/lib/config.ts +5 -6
  30. package/src/lib/control-plane.ts +295 -0
  31. package/src/lib/debug.ts +3 -1
  32. package/src/lib/deploy-mode.ts +93 -0
  33. package/src/lib/deploy-upload.ts +92 -0
  34. package/src/lib/errors.ts +2 -0
  35. package/src/lib/github.ts +31 -1
  36. package/src/lib/hooks.ts +4 -12
  37. package/src/lib/intent.ts +88 -0
  38. package/src/lib/jsonc.ts +125 -0
  39. package/src/lib/local-paths.test.ts +902 -0
  40. package/src/lib/local-paths.ts +258 -0
  41. package/src/lib/managed-deploy.ts +175 -0
  42. package/src/lib/managed-down.ts +159 -0
  43. package/src/lib/mcp-config.ts +55 -34
  44. package/src/lib/names.ts +9 -29
  45. package/src/lib/project-operations.ts +676 -249
  46. package/src/lib/project-resolver.ts +476 -0
  47. package/src/lib/registry.ts +76 -37
  48. package/src/lib/resources.ts +196 -0
  49. package/src/lib/schema.ts +30 -1
  50. package/src/lib/storage/file-filter.ts +1 -0
  51. package/src/lib/storage/index.ts +5 -1
  52. package/src/lib/telemetry.ts +14 -0
  53. package/src/lib/tty.ts +15 -0
  54. package/src/lib/zip-packager.ts +255 -0
  55. package/src/mcp/resources/index.ts +8 -2
  56. package/src/mcp/server.ts +32 -4
  57. package/src/mcp/tools/index.ts +35 -13
  58. package/src/mcp/types.ts +6 -0
  59. package/src/mcp/utils.ts +1 -1
  60. package/src/templates/index.ts +42 -4
  61. package/src/templates/types.ts +13 -0
  62. package/templates/CLAUDE.md +166 -0
  63. package/templates/api/.jack.json +4 -0
  64. package/templates/api/bun.lock +1 -0
  65. package/templates/api/wrangler.jsonc +5 -0
  66. package/templates/hello/.jack.json +28 -0
  67. package/templates/hello/package.json +10 -0
  68. package/templates/hello/src/index.ts +11 -0
  69. package/templates/hello/tsconfig.json +11 -0
  70. package/templates/hello/wrangler.jsonc +5 -0
  71. package/templates/miniapp/.jack.json +15 -4
  72. package/templates/miniapp/bun.lock +135 -40
  73. package/templates/miniapp/index.html +1 -0
  74. package/templates/miniapp/package.json +3 -1
  75. package/templates/miniapp/public/.well-known/farcaster.json +7 -5
  76. package/templates/miniapp/public/icon.png +0 -0
  77. package/templates/miniapp/public/og.png +0 -0
  78. package/templates/miniapp/schema.sql +8 -0
  79. package/templates/miniapp/src/App.tsx +254 -3
  80. package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
  81. package/templates/miniapp/src/hooks/useAI.ts +35 -0
  82. package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
  83. package/templates/miniapp/src/hooks/useShare.ts +76 -0
  84. package/templates/miniapp/src/index.css +15 -0
  85. package/templates/miniapp/src/lib/api.ts +2 -1
  86. package/templates/miniapp/src/worker.ts +515 -1
  87. package/templates/miniapp/wrangler.jsonc +15 -3
  88. package/LICENSE +0 -190
  89. package/README.md +0 -55
  90. package/src/commands/cloud.ts +0 -230
  91. package/templates/api/wrangler.toml +0 -3
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Resource types and utilities for Jack CLI
3
+ *
4
+ * Resources are fetched on-demand from control plane (managed)
5
+ * or parsed from wrangler.jsonc (BYO).
6
+ */
7
+
8
+ import { existsSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { parseJsonc } from "./jsonc.ts";
11
+
12
+ // Resource types matching control plane schema
13
+ export type ResourceType =
14
+ | "worker"
15
+ | "d1"
16
+ | "r2_content"
17
+ | "kv"
18
+ | "queue"
19
+ | "ai"
20
+ | "hyperdrive"
21
+ | "vectorize";
22
+
23
+ // Resource from control plane API
24
+ export interface ControlPlaneResource {
25
+ id: string;
26
+ project_id: string;
27
+ resource_type: ResourceType;
28
+ resource_name: string;
29
+ provider_id: string;
30
+ status: "active" | "provisioning" | "error" | "deleted";
31
+ metadata?: Record<string, unknown>;
32
+ created_at: string;
33
+ }
34
+
35
+ // Unified resource view (used by CLI)
36
+ export interface ResolvedResources {
37
+ d1?: { binding: string; name: string; id?: string };
38
+ ai?: { binding: string };
39
+ assets?: { binding: string; directory: string };
40
+ kv?: Array<{ binding: string; id: string; name?: string }>;
41
+ r2?: Array<{ binding: string; name: string }>;
42
+ queues?: Array<{ binding: string; name: string }>;
43
+ vars?: Record<string, string>;
44
+ }
45
+
46
+ /**
47
+ * Convert control plane resources to unified format
48
+ */
49
+ export function convertControlPlaneResources(resources: ControlPlaneResource[]): ResolvedResources {
50
+ const result: ResolvedResources = {};
51
+
52
+ for (const r of resources) {
53
+ switch (r.resource_type) {
54
+ case "d1":
55
+ result.d1 = {
56
+ binding: "DB",
57
+ name: r.resource_name,
58
+ id: r.provider_id,
59
+ };
60
+ break;
61
+ case "kv":
62
+ result.kv = result.kv || [];
63
+ result.kv.push({
64
+ binding: r.resource_name.toUpperCase().replace(/-/g, "_"),
65
+ id: r.provider_id,
66
+ name: r.resource_name,
67
+ });
68
+ break;
69
+ case "r2_content":
70
+ result.r2 = result.r2 || [];
71
+ result.r2.push({
72
+ binding: "BUCKET",
73
+ name: r.resource_name,
74
+ });
75
+ break;
76
+ // AI doesn't need provider_id, just indicates it's available
77
+ case "ai":
78
+ result.ai = { binding: "AI" };
79
+ break;
80
+ }
81
+ }
82
+
83
+ return result;
84
+ }
85
+
86
+ /**
87
+ * Parse resources from wrangler.jsonc for BYO projects.
88
+ * Returns a unified resource view.
89
+ */
90
+ export async function parseWranglerResources(projectPath: string): Promise<ResolvedResources> {
91
+ const wranglerPath = join(projectPath, "wrangler.jsonc");
92
+
93
+ if (!existsSync(wranglerPath)) {
94
+ return {};
95
+ }
96
+
97
+ try {
98
+ // Read and parse JSONC (strip comments)
99
+ const content = await Bun.file(wranglerPath).text();
100
+ const config = parseJsonc<WranglerConfig>(content);
101
+
102
+ const resources: ResolvedResources = {};
103
+
104
+ // D1 databases
105
+ if (config.d1_databases?.[0]) {
106
+ const d1 = config.d1_databases[0];
107
+ resources.d1 = {
108
+ binding: d1.binding || "DB",
109
+ name: d1.database_name || d1.binding || "DB",
110
+ id: d1.database_id,
111
+ };
112
+ }
113
+
114
+ // AI binding
115
+ if (config.ai) {
116
+ resources.ai = {
117
+ binding: config.ai.binding || "AI",
118
+ };
119
+ }
120
+
121
+ // Assets
122
+ if (config.assets?.directory) {
123
+ resources.assets = {
124
+ binding: config.assets.binding || "ASSETS",
125
+ directory: config.assets.directory,
126
+ };
127
+ }
128
+
129
+ // KV namespaces
130
+ if (config.kv_namespaces && config.kv_namespaces.length > 0) {
131
+ resources.kv = config.kv_namespaces.map((kv) => ({
132
+ binding: kv.binding,
133
+ id: kv.id,
134
+ }));
135
+ }
136
+
137
+ // R2 buckets
138
+ if (config.r2_buckets && config.r2_buckets.length > 0) {
139
+ resources.r2 = config.r2_buckets.map((r2) => ({
140
+ binding: r2.binding,
141
+ name: r2.bucket_name,
142
+ }));
143
+ }
144
+
145
+ // Queues
146
+ if (config.queues?.producers && config.queues.producers.length > 0) {
147
+ resources.queues = config.queues.producers.map((q) => ({
148
+ binding: q.binding,
149
+ name: q.queue,
150
+ }));
151
+ }
152
+
153
+ // Environment variables (vars)
154
+ if (config.vars && Object.keys(config.vars).length > 0) {
155
+ resources.vars = config.vars;
156
+ }
157
+
158
+ return resources;
159
+ } catch {
160
+ // Failed to parse, return empty
161
+ return {};
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Wrangler config shape (partial, for resource parsing)
167
+ */
168
+ interface WranglerConfig {
169
+ d1_databases?: Array<{
170
+ binding: string;
171
+ database_name?: string;
172
+ database_id?: string;
173
+ }>;
174
+ ai?: {
175
+ binding?: string;
176
+ };
177
+ assets?: {
178
+ binding?: string;
179
+ directory: string;
180
+ };
181
+ kv_namespaces?: Array<{
182
+ binding: string;
183
+ id: string;
184
+ }>;
185
+ r2_buckets?: Array<{
186
+ binding: string;
187
+ bucket_name: string;
188
+ }>;
189
+ queues?: {
190
+ producers?: Array<{
191
+ binding: string;
192
+ queue: string;
193
+ }>;
194
+ };
195
+ vars?: Record<string, string>;
196
+ }
package/src/lib/schema.ts CHANGED
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { $ } from "bun";
4
4
  import { debug } from "./debug.ts";
5
+ import { parseJsonc } from "./jsonc.ts";
5
6
  import { output } from "./output.ts";
6
7
 
7
8
  /**
@@ -60,6 +61,31 @@ export async function hasD1Config(projectDir: string): Promise<boolean> {
60
61
  }
61
62
  }
62
63
 
64
+ export interface D1Binding {
65
+ binding?: string;
66
+ database_id?: string;
67
+ database_name?: string;
68
+ }
69
+
70
+ /**
71
+ * Read D1 bindings from wrangler.jsonc
72
+ */
73
+ export async function getD1Bindings(projectDir: string): Promise<D1Binding[]> {
74
+ const wranglerPath = join(projectDir, "wrangler.jsonc");
75
+
76
+ if (!existsSync(wranglerPath)) {
77
+ return [];
78
+ }
79
+
80
+ try {
81
+ const content = await Bun.file(wranglerPath).text();
82
+ const config = parseJsonc(content) as { d1_databases?: D1Binding[] };
83
+ return Array.isArray(config.d1_databases) ? config.d1_databases : [];
84
+ } catch {
85
+ return [];
86
+ }
87
+ }
88
+
63
89
  /**
64
90
  * Get the D1 database name from wrangler config
65
91
  * Returns the database_name field which is needed for wrangler d1 execute
@@ -74,7 +100,10 @@ export async function getD1DatabaseName(projectDir: string): Promise<string | nu
74
100
  try {
75
101
  const content = await Bun.file(wranglerPath).text();
76
102
  // Strip comments for parsing
77
- const cleaned = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
103
+ // Note: Only remove line comments at the start of a line to avoid breaking URLs
104
+ const cleaned = content
105
+ .replace(/\/\*[\s\S]*?\*\//g, "") // block comments
106
+ .replace(/^\s*\/\/.*$/gm, ""); // line comments at start of line only
78
107
  const config = JSON.parse(cleaned);
79
108
 
80
109
  return config.d1_databases?.[0]?.database_name || null;
@@ -43,6 +43,7 @@ export const DEFAULT_EXCLUDES: string[] = [
43
43
  ".env",
44
44
  ".env.*",
45
45
  ".dev.vars",
46
+ ".secrets.json",
46
47
  "*.log",
47
48
  ".DS_Store",
48
49
  "dist/**",
@@ -80,7 +80,11 @@ export async function getProjectNameFromDir(projectDir: string): Promise<string>
80
80
  try {
81
81
  const content = await Bun.file(jsoncPath).text();
82
82
  // Remove comments and parse JSON
83
- const jsonContent = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
83
+ // Note: Only remove line comments at the start of a line (with optional whitespace)
84
+ // to avoid breaking URLs like https://example.com
85
+ const jsonContent = content
86
+ .replace(/\/\*[\s\S]*?\*\//g, "") // block comments
87
+ .replace(/^\s*\/\/.*$/gm, ""); // line comments at start of line only
84
88
  const config = JSON.parse(jsonContent);
85
89
  if (config.name) {
86
90
  return config.name;
@@ -16,6 +16,20 @@ export const Events = {
16
16
  PROJECT_CREATED: "project_created",
17
17
  DEPLOY_STARTED: "deploy_started",
18
18
  CONFIG_CHANGED: "config_changed",
19
+
20
+ // Intent-driven creation events
21
+ INTENT_MATCHED: "intent_matched",
22
+ INTENT_NO_MATCH: "intent_no_match",
23
+ INTENT_CUSTOMIZATION_STARTED: "intent_customization_started",
24
+ INTENT_CUSTOMIZATION_COMPLETED: "intent_customization_completed",
25
+ INTENT_CUSTOMIZATION_FAILED: "intent_customization_failed",
26
+
27
+ // Deploy mode events
28
+ DEPLOY_MODE_SELECTED: "deploy_mode_selected",
29
+ MANAGED_PROJECT_CREATED: "managed_project_created",
30
+ MANAGED_DEPLOY_STARTED: "managed_deploy_started",
31
+ MANAGED_DEPLOY_COMPLETED: "managed_deploy_completed",
32
+ MANAGED_DEPLOY_FAILED: "managed_deploy_failed",
19
33
  } as const;
20
34
 
21
35
  type EventName = (typeof Events)[keyof typeof Events];
package/src/lib/tty.ts ADDED
@@ -0,0 +1,15 @@
1
+ export function restoreTty(): void {
2
+ if (!process.stdin.isTTY) return;
3
+
4
+ try {
5
+ process.stdin.setRawMode(false);
6
+ } catch {
7
+ // Ignore if stdin does not support raw mode
8
+ }
9
+
10
+ process.stdin.pause();
11
+
12
+ if (process.stderr.isTTY) {
13
+ process.stderr.write("\x1b[?25h");
14
+ }
15
+ }
@@ -0,0 +1,255 @@
1
+ import { existsSync } from "node:fs";
2
+ import { createWriteStream } from "node:fs";
3
+ import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join, relative } from "node:path";
6
+ import archiver from "archiver";
7
+ import { type AssetManifest, computeAssetHash } from "./asset-hash.ts";
8
+ import type { BuildOutput, WranglerConfig } from "./build-helper.ts";
9
+ import { scanProjectFiles } from "./storage/file-filter.ts";
10
+
11
+ export interface ZipPackageResult {
12
+ bundleZipPath: string;
13
+ sourceZipPath: string;
14
+ manifestPath: string;
15
+ schemaPath: string | null;
16
+ secretsPath: string | null;
17
+ assetsZipPath: string | null;
18
+ assetManifest: AssetManifest | null;
19
+ cleanup: () => Promise<void>;
20
+ }
21
+
22
+ export interface ManifestData {
23
+ version: 1;
24
+ entrypoint: string;
25
+ compatibility_date: string;
26
+ compatibility_flags?: string[];
27
+ module_format: "esm";
28
+ assets_dir?: string;
29
+ built_at: string;
30
+ bindings?: {
31
+ d1?: { binding: string };
32
+ ai?: { binding: string };
33
+ assets?: {
34
+ binding: string;
35
+ directory: string;
36
+ not_found_handling?: "single-page-application" | "404-page" | "none";
37
+ html_handling?:
38
+ | "auto-trailing-slash"
39
+ | "force-trailing-slash"
40
+ | "drop-trailing-slash"
41
+ | "none";
42
+ };
43
+ vars?: Record<string, string>;
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Creates a ZIP archive from source directory
49
+ * @param outputPath - Absolute path for output ZIP file
50
+ * @param sourceDir - Absolute path to directory to archive
51
+ * @param files - Optional list of specific files to include (relative to sourceDir)
52
+ * @returns Promise that resolves when ZIP is created
53
+ */
54
+ async function createZipArchive(
55
+ outputPath: string,
56
+ sourceDir: string,
57
+ files?: string[],
58
+ ): Promise<void> {
59
+ return new Promise((resolve, reject) => {
60
+ const output = createWriteStream(outputPath);
61
+ const archive = archiver("zip", { zlib: { level: 9 } });
62
+
63
+ output.on("close", () => resolve());
64
+ archive.on("error", (err) => reject(err));
65
+
66
+ archive.pipe(output);
67
+
68
+ if (files) {
69
+ // Add specific files
70
+ for (const file of files) {
71
+ const filePath = join(sourceDir, file);
72
+ archive.file(filePath, { name: file });
73
+ }
74
+ } else {
75
+ // Add entire directory
76
+ archive.directory(sourceDir, false);
77
+ }
78
+
79
+ archive.finalize();
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Recursively collects all file paths in a directory
85
+ */
86
+ async function collectAllFiles(dir: string, baseDir: string = dir): Promise<string[]> {
87
+ const entries = await readdir(dir, { withFileTypes: true });
88
+ const files: string[] = [];
89
+
90
+ for (const entry of entries) {
91
+ const fullPath = join(dir, entry.name);
92
+ if (entry.isDirectory()) {
93
+ files.push(...(await collectAllFiles(fullPath, baseDir)));
94
+ } else if (entry.isFile()) {
95
+ files.push(fullPath);
96
+ }
97
+ }
98
+
99
+ return files;
100
+ }
101
+
102
+ /**
103
+ * Creates an asset manifest for all files in a directory.
104
+ * Maps paths starting with "/" (e.g., "/index.html", "/assets/app.js")
105
+ * to their content-addressable hashes.
106
+ *
107
+ * @param assetsDir - Absolute path to the assets directory
108
+ * @returns Asset manifest mapping paths to hash entries
109
+ */
110
+ export async function createAssetManifest(assetsDir: string): Promise<AssetManifest> {
111
+ const allFiles = await collectAllFiles(assetsDir);
112
+
113
+ const entries = await Promise.all(
114
+ allFiles.map(async (filePath) => {
115
+ const content = await readFile(filePath);
116
+ const hash = await computeAssetHash(new Uint8Array(content), filePath);
117
+ const fileStats = await stat(filePath);
118
+ const relativePath = "/" + relative(assetsDir, filePath);
119
+ return [relativePath, { hash, size: fileStats.size }] as const;
120
+ }),
121
+ );
122
+
123
+ return Object.fromEntries(entries);
124
+ }
125
+
126
+ /**
127
+ * Extracts binding intent from wrangler config for the manifest.
128
+ * Returns undefined if no bindings are configured.
129
+ */
130
+ function extractBindingsFromConfig(config?: WranglerConfig): ManifestData["bindings"] | undefined {
131
+ if (!config) return undefined;
132
+
133
+ const bindings: NonNullable<ManifestData["bindings"]> = {};
134
+
135
+ // Extract D1 database binding (use first one if multiple)
136
+ if (config.d1_databases && config.d1_databases.length > 0) {
137
+ const firstDb = config.d1_databases[0];
138
+ if (firstDb) {
139
+ bindings.d1 = { binding: firstDb.binding };
140
+ }
141
+ }
142
+
143
+ // Extract AI binding (default binding name: "AI")
144
+ if (config.ai) {
145
+ bindings.ai = { binding: config.ai.binding || "AI" };
146
+ }
147
+
148
+ // Extract assets binding (defaults: binding="ASSETS", directory="./dist")
149
+ if (config.assets) {
150
+ bindings.assets = {
151
+ binding: config.assets.binding || "ASSETS",
152
+ directory: config.assets.directory || "./dist",
153
+ not_found_handling: config.assets.not_found_handling,
154
+ html_handling: config.assets.html_handling,
155
+ };
156
+ }
157
+
158
+ // Extract vars
159
+ if (config.vars && Object.keys(config.vars).length > 0) {
160
+ bindings.vars = config.vars;
161
+ }
162
+
163
+ // Return undefined if no bindings were extracted
164
+ return Object.keys(bindings).length > 0 ? bindings : undefined;
165
+ }
166
+
167
+ /**
168
+ * Packages a built project for deployment to jack cloud
169
+ * @param projectPath - Absolute path to project directory
170
+ * @param buildOutput - Build output from buildProject()
171
+ * @param config - Optional wrangler config to extract binding intent
172
+ * @returns Package result with ZIP paths and cleanup function
173
+ */
174
+ export async function packageForDeploy(
175
+ projectPath: string,
176
+ buildOutput: BuildOutput,
177
+ config?: WranglerConfig,
178
+ ): Promise<ZipPackageResult> {
179
+ // Create temp directory for package artifacts
180
+ const packageId = `jack-package-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
181
+ const packageDir = join(tmpdir(), packageId);
182
+ await mkdir(packageDir, { recursive: true });
183
+
184
+ // Define artifact paths
185
+ const bundleZipPath = join(packageDir, "bundle.zip");
186
+ const sourceZipPath = join(packageDir, "source.zip");
187
+ const manifestPath = join(packageDir, "manifest.json");
188
+
189
+ // 1. Create bundle.zip from build output directory
190
+ await createZipArchive(bundleZipPath, buildOutput.outDir);
191
+
192
+ // 2. Create source.zip from project files (filtered)
193
+ const projectFiles = await scanProjectFiles(projectPath);
194
+ const sourceFiles = projectFiles.map((f) => f.path);
195
+ await createZipArchive(sourceZipPath, projectPath, sourceFiles);
196
+
197
+ // 3. Create manifest.json
198
+ const manifest: ManifestData = {
199
+ version: 1,
200
+ entrypoint: buildOutput.entrypoint,
201
+ compatibility_date: buildOutput.compatibilityDate,
202
+ compatibility_flags:
203
+ buildOutput.compatibilityFlags.length > 0 ? buildOutput.compatibilityFlags : undefined,
204
+ module_format: "esm",
205
+ assets_dir: buildOutput.assetsDir ? "assets" : undefined,
206
+ built_at: new Date().toISOString(),
207
+ bindings: extractBindingsFromConfig(config),
208
+ };
209
+
210
+ await Bun.write(manifestPath, JSON.stringify(manifest, null, 2));
211
+
212
+ // 4. Check for optional files (schema.sql and .secrets.json)
213
+ let schemaPath: string | null = null;
214
+ const schemaSrcPath = join(projectPath, "schema.sql");
215
+ if (existsSync(schemaSrcPath)) {
216
+ schemaPath = join(packageDir, "schema.sql");
217
+ await Bun.write(schemaPath, await readFile(schemaSrcPath));
218
+ }
219
+
220
+ let secretsPath: string | null = null;
221
+ const secretsSrcPath = join(projectPath, ".secrets.json");
222
+ if (existsSync(secretsSrcPath)) {
223
+ secretsPath = join(packageDir, ".secrets.json");
224
+ await Bun.write(secretsPath, await readFile(secretsSrcPath));
225
+ }
226
+
227
+ // 5. If assets directory exists, create assets.zip and asset manifest
228
+ let assetsZipPath: string | null = null;
229
+ let assetManifest: AssetManifest | null = null;
230
+ if (buildOutput.assetsDir) {
231
+ // Create zip and manifest in parallel for speed
232
+ const [, manifest] = await Promise.all([
233
+ createZipArchive(join(packageDir, "assets.zip"), buildOutput.assetsDir),
234
+ createAssetManifest(buildOutput.assetsDir),
235
+ ]);
236
+ assetsZipPath = join(packageDir, "assets.zip");
237
+ assetManifest = manifest;
238
+ }
239
+
240
+ // Return package result with cleanup function
241
+ return {
242
+ bundleZipPath,
243
+ sourceZipPath,
244
+ manifestPath,
245
+ schemaPath,
246
+ secretsPath,
247
+ assetsZipPath,
248
+ assetManifest,
249
+ cleanup: async () => {
250
+ await rm(packageDir, { recursive: true, force: true });
251
+ // Also cleanup build output directory
252
+ await rm(buildOutput.outDir, { recursive: true, force: true });
253
+ },
254
+ };
255
+ }
@@ -5,11 +5,16 @@ import {
5
5
  ListResourcesRequestSchema,
6
6
  ReadResourceRequestSchema,
7
7
  } from "@modelcontextprotocol/sdk/types.js";
8
- import type { McpServerOptions } from "../types.ts";
8
+ import type { DebugLogger, McpServerOptions } from "../types.ts";
9
9
 
10
- export function registerResources(server: McpServer, options: McpServerOptions) {
10
+ export function registerResources(
11
+ server: McpServer,
12
+ options: McpServerOptions,
13
+ debug: DebugLogger,
14
+ ) {
11
15
  // Register resource list handler
12
16
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
17
+ debug("resources/list requested");
13
18
  return {
14
19
  resources: [
15
20
  {
@@ -26,6 +31,7 @@ export function registerResources(server: McpServer, options: McpServerOptions)
26
31
  // Register resource read handler
27
32
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
28
33
  const uri = request.params.uri;
34
+ debug("resources/read requested", { uri });
29
35
 
30
36
  if (uri === "agents://context") {
31
37
  const projectPath = options.projectPath ?? process.cwd();
package/src/mcp/server.ts CHANGED
@@ -5,7 +5,25 @@ import { registerResources } from "./resources/index.ts";
5
5
  import { registerTools } from "./tools/index.ts";
6
6
  import type { McpServerOptions } from "./types.ts";
7
7
 
8
+ /**
9
+ * Debug logger that writes to stderr (doesn't interfere with stdio MCP protocol)
10
+ */
11
+ export function createDebugLogger(enabled: boolean) {
12
+ return (message: string, data?: unknown) => {
13
+ if (!enabled) return;
14
+ const timestamp = new Date().toISOString();
15
+ const line = data
16
+ ? `[jack-mcp ${timestamp}] ${message}: ${JSON.stringify(data)}`
17
+ : `[jack-mcp ${timestamp}] ${message}`;
18
+ console.error(line);
19
+ };
20
+ }
21
+
8
22
  export async function createMcpServer(options: McpServerOptions = {}) {
23
+ const debug = createDebugLogger(options.debug ?? false);
24
+
25
+ debug("Creating MCP server", { version: pkg.version, options });
26
+
9
27
  const server = new McpServer(
10
28
  {
11
29
  name: "jack",
@@ -19,14 +37,24 @@ export async function createMcpServer(options: McpServerOptions = {}) {
19
37
  },
20
38
  );
21
39
 
22
- registerTools(server, options);
23
- registerResources(server, options);
40
+ registerTools(server, options, debug);
41
+ registerResources(server, options, debug);
24
42
 
25
- return server;
43
+ return { server, debug };
26
44
  }
27
45
 
28
46
  export async function startMcpServer(options: McpServerOptions = {}) {
29
- const server = await createMcpServer(options);
47
+ const { server, debug } = await createMcpServer(options);
30
48
  const transport = new StdioServerTransport();
49
+
50
+ debug("Starting MCP server on stdio transport");
51
+
52
+ // Always log startup to stderr so user knows it's running
53
+ console.error(
54
+ `[jack-mcp] Server started (v${pkg.version})${options.debug ? " [debug mode]" : ""}`,
55
+ );
56
+
31
57
  await server.connect(transport);
58
+
59
+ debug("MCP server connected and ready");
32
60
  }