@getjack/jack 0.1.30 → 0.1.31

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.
@@ -5,6 +5,10 @@ import { formatSize } from "../lib/format.ts";
5
5
  import { error, info, item, output as outputSpinner, success, warn } from "../lib/output.ts";
6
6
  import { readProjectLink } from "../lib/project-link.ts";
7
7
  import { parseWranglerResources } from "../lib/resources.ts";
8
+ import { createCronSchedule } from "../lib/services/cron-create.ts";
9
+ import { deleteCronSchedule } from "../lib/services/cron-delete.ts";
10
+ import { listCronSchedules } from "../lib/services/cron-list.ts";
11
+ import { COMMON_CRON_PATTERNS, testCronExpression } from "../lib/services/cron-test.ts";
8
12
  import { createDatabase } from "../lib/services/db-create.ts";
9
13
  import {
10
14
  DestructiveOperationError,
@@ -111,9 +115,11 @@ export default async function services(
111
115
  return await dbCommand(args, options);
112
116
  case "storage":
113
117
  return await storageCommand(args, options);
118
+ case "cron":
119
+ return await cronCommand(args, options);
114
120
  default:
115
121
  error(`Unknown service: ${subcommand}`);
116
- info("Available: db, storage");
122
+ info("Available: db, storage, cron");
117
123
  process.exit(1);
118
124
  }
119
125
  }
@@ -125,6 +131,7 @@ function showHelp(): void {
125
131
  console.error("Commands:");
126
132
  console.error(" db Manage database");
127
133
  console.error(" storage Manage storage (R2 buckets)");
134
+ console.error(" cron Manage scheduled tasks");
128
135
  console.error("");
129
136
  console.error("Run 'jack services <command>' for more information.");
130
137
  console.error("");
@@ -1181,3 +1188,295 @@ async function storageDelete(args: string[], options: ServiceOptions): Promise<v
1181
1188
  process.exit(1);
1182
1189
  }
1183
1190
  }
1191
+
1192
+ // ============================================================================
1193
+ // Cron Commands
1194
+ // ============================================================================
1195
+
1196
+ function showCronHelp(): void {
1197
+ console.error("");
1198
+ info("jack services cron - Manage scheduled tasks");
1199
+ console.error("");
1200
+ console.error("Actions:");
1201
+ console.error(' create <expression> Add a cron schedule (e.g., "0 * * * *")');
1202
+ console.error(" list List all cron schedules");
1203
+ console.error(" delete <expression> Remove a cron schedule");
1204
+ console.error(" test <expression> Validate and preview a cron expression");
1205
+ console.error("");
1206
+ console.error("Examples:");
1207
+ console.error(' jack services cron create "0 * * * *" Schedule hourly runs');
1208
+ console.error(" jack services cron list Show all schedules");
1209
+ console.error(' jack services cron delete "0 * * * *" Remove a schedule');
1210
+ console.error(' jack services cron test "*/15 * * * *" Preview next run times');
1211
+ console.error("");
1212
+ console.error("Common patterns:");
1213
+ for (const pattern of COMMON_CRON_PATTERNS) {
1214
+ console.error(` ${pattern.expression.padEnd(15)} ${pattern.description}`);
1215
+ }
1216
+ console.error("");
1217
+ console.error("Note: Cron schedules require Jack Cloud (managed) projects.");
1218
+ console.error("All times are in UTC.");
1219
+ console.error("");
1220
+ }
1221
+
1222
+ async function cronCommand(args: string[], options: ServiceOptions): Promise<void> {
1223
+ // Check if any argument is --help or -h
1224
+ if (args.includes("--help") || args.includes("-h")) {
1225
+ return showCronHelp();
1226
+ }
1227
+
1228
+ const action = args[0] || "list"; // Default to list
1229
+
1230
+ switch (action) {
1231
+ case "help":
1232
+ return showCronHelp();
1233
+ case "create":
1234
+ return await cronCreate(args.slice(1), options);
1235
+ case "list":
1236
+ return await cronList(options);
1237
+ case "delete":
1238
+ return await cronDelete(args.slice(1), options);
1239
+ case "test":
1240
+ return await cronTest(args.slice(1), options);
1241
+ default:
1242
+ error(`Unknown action: ${action}`);
1243
+ info("Available: create, list, delete, test");
1244
+ process.exit(1);
1245
+ }
1246
+ }
1247
+
1248
+ /**
1249
+ * Create a new cron schedule
1250
+ */
1251
+ async function cronCreate(args: string[], options: ServiceOptions): Promise<void> {
1252
+ const expression = args[0];
1253
+
1254
+ if (!expression) {
1255
+ console.error("");
1256
+ error("Cron expression required");
1257
+ info('Usage: jack services cron create "<expression>"');
1258
+ console.error("");
1259
+ console.error("Common patterns:");
1260
+ for (const pattern of COMMON_CRON_PATTERNS) {
1261
+ console.error(` ${pattern.expression.padEnd(15)} ${pattern.description}`);
1262
+ }
1263
+ console.error("");
1264
+ process.exit(1);
1265
+ }
1266
+
1267
+ outputSpinner.start("Creating cron schedule...");
1268
+ try {
1269
+ const result = await createCronSchedule(process.cwd(), expression, {
1270
+ interactive: true,
1271
+ });
1272
+ outputSpinner.stop();
1273
+
1274
+ // Track telemetry
1275
+ track(Events.SERVICE_CREATED, {
1276
+ service_type: "cron",
1277
+ created: result.created,
1278
+ });
1279
+
1280
+ console.error("");
1281
+ success("Cron schedule created");
1282
+ console.error("");
1283
+ item(`Expression: ${result.expression}`);
1284
+ item(`Schedule: ${result.description}`);
1285
+ item(`Next run: ${result.nextRunAt}`);
1286
+ console.error("");
1287
+ info("Make sure your Worker handles POST /__scheduled requests.");
1288
+ console.error("");
1289
+ } catch (err) {
1290
+ outputSpinner.stop();
1291
+ console.error("");
1292
+ error(`Failed to create cron schedule: ${err instanceof Error ? err.message : String(err)}`);
1293
+ process.exit(1);
1294
+ }
1295
+ }
1296
+
1297
+ /**
1298
+ * List all cron schedules
1299
+ */
1300
+ async function cronList(options: ServiceOptions): Promise<void> {
1301
+ outputSpinner.start("Fetching cron schedules...");
1302
+ try {
1303
+ const schedules = await listCronSchedules(process.cwd());
1304
+ outputSpinner.stop();
1305
+
1306
+ if (schedules.length === 0) {
1307
+ console.error("");
1308
+ info("No cron schedules found for this project.");
1309
+ console.error("");
1310
+ info('Create one with: jack services cron create "0 * * * *"');
1311
+ console.error("");
1312
+ return;
1313
+ }
1314
+
1315
+ console.error("");
1316
+ success(`Found ${schedules.length} cron schedule${schedules.length === 1 ? "" : "s"}:`);
1317
+ console.error("");
1318
+
1319
+ for (const schedule of schedules) {
1320
+ const statusIcon = schedule.enabled ? "+" : "-";
1321
+ item(`${statusIcon} ${schedule.expression}`);
1322
+ item(` ${schedule.description}`);
1323
+ item(` Next: ${schedule.nextRunAt}`);
1324
+ if (schedule.lastRunAt) {
1325
+ const statusLabel =
1326
+ schedule.lastRunStatus === "success"
1327
+ ? "success"
1328
+ : `${schedule.lastRunStatus} (${schedule.consecutiveFailures} failures)`;
1329
+ item(` Last: ${schedule.lastRunAt} - ${statusLabel}`);
1330
+ if (schedule.lastRunDurationMs !== null) {
1331
+ item(` Duration: ${schedule.lastRunDurationMs}ms`);
1332
+ }
1333
+ }
1334
+ console.error("");
1335
+ }
1336
+ } catch (err) {
1337
+ outputSpinner.stop();
1338
+ console.error("");
1339
+ error(`Failed to list cron schedules: ${err instanceof Error ? err.message : String(err)}`);
1340
+ process.exit(1);
1341
+ }
1342
+ }
1343
+
1344
+ /**
1345
+ * Delete a cron schedule
1346
+ */
1347
+ async function cronDelete(args: string[], options: ServiceOptions): Promise<void> {
1348
+ const expression = args[0];
1349
+
1350
+ if (!expression) {
1351
+ console.error("");
1352
+ error("Cron expression required");
1353
+ info('Usage: jack services cron delete "<expression>"');
1354
+ console.error("");
1355
+ process.exit(1);
1356
+ }
1357
+
1358
+ // Show what will be deleted
1359
+ console.error("");
1360
+ info(`Cron schedule: ${expression}`);
1361
+ console.error("");
1362
+ warn("This will stop all future scheduled runs");
1363
+ console.error("");
1364
+
1365
+ // Confirm deletion
1366
+ const { promptSelect } = await import("../lib/hooks.ts");
1367
+ const choice = await promptSelect(
1368
+ ["Yes, delete", "No, cancel"],
1369
+ `Delete cron schedule '${expression}'?`,
1370
+ );
1371
+
1372
+ if (choice !== 0) {
1373
+ info("Cancelled");
1374
+ return;
1375
+ }
1376
+
1377
+ outputSpinner.start("Deleting cron schedule...");
1378
+ try {
1379
+ const result = await deleteCronSchedule(process.cwd(), expression, {
1380
+ interactive: true,
1381
+ });
1382
+ outputSpinner.stop();
1383
+
1384
+ // Track telemetry
1385
+ track(Events.SERVICE_DELETED, {
1386
+ service_type: "cron",
1387
+ deleted: result.deleted,
1388
+ });
1389
+
1390
+ console.error("");
1391
+ success("Cron schedule deleted");
1392
+ console.error("");
1393
+ } catch (err) {
1394
+ outputSpinner.stop();
1395
+ console.error("");
1396
+ error(`Failed to delete cron schedule: ${err instanceof Error ? err.message : String(err)}`);
1397
+ process.exit(1);
1398
+ }
1399
+ }
1400
+
1401
+ /**
1402
+ * Test/validate a cron expression
1403
+ */
1404
+ async function cronTest(args: string[], options: ServiceOptions): Promise<void> {
1405
+ const expression = args[0];
1406
+
1407
+ if (!expression) {
1408
+ console.error("");
1409
+ error("Cron expression required");
1410
+ info('Usage: jack services cron test "<expression>"');
1411
+ console.error("");
1412
+ console.error("Common patterns:");
1413
+ for (const pattern of COMMON_CRON_PATTERNS) {
1414
+ console.error(` ${pattern.expression.padEnd(15)} ${pattern.description}`);
1415
+ }
1416
+ console.error("");
1417
+ process.exit(1);
1418
+ }
1419
+
1420
+ // Test the expression (no production trigger by default)
1421
+ const result = await testCronExpression(process.cwd(), expression, {
1422
+ interactive: true,
1423
+ });
1424
+
1425
+ if (!result.valid) {
1426
+ console.error("");
1427
+ error(`Invalid cron expression: ${result.error}`);
1428
+ console.error("");
1429
+ console.error("Common patterns:");
1430
+ for (const pattern of COMMON_CRON_PATTERNS) {
1431
+ console.error(` ${pattern.expression.padEnd(15)} ${pattern.description}`);
1432
+ }
1433
+ console.error("");
1434
+ process.exit(1);
1435
+ }
1436
+
1437
+ console.error("");
1438
+ success("Valid cron expression");
1439
+ console.error("");
1440
+ item(`Expression: ${result.expression}`);
1441
+ item(`Schedule: ${result.description}`);
1442
+ console.error("");
1443
+ info("Next 5 runs (UTC):");
1444
+ for (const time of result.nextTimes!) {
1445
+ item(` ${time.toISOString()}`);
1446
+ }
1447
+ console.error("");
1448
+
1449
+ // Check if this is a managed project for optional trigger
1450
+ const link = await readProjectLink(process.cwd());
1451
+ if (link?.deploy_mode === "managed") {
1452
+ const { promptSelect } = await import("../lib/hooks.ts");
1453
+ const choice = await promptSelect(
1454
+ ["Yes, trigger now", "No, skip"],
1455
+ "Trigger scheduled handler on production?",
1456
+ );
1457
+
1458
+ if (choice === 0) {
1459
+ outputSpinner.start("Triggering scheduled handler...");
1460
+ try {
1461
+ const triggerResult = await testCronExpression(process.cwd(), expression, {
1462
+ triggerProduction: true,
1463
+ interactive: true,
1464
+ });
1465
+ outputSpinner.stop();
1466
+
1467
+ if (triggerResult.triggerResult) {
1468
+ console.error("");
1469
+ success(
1470
+ `Triggered! Status: ${triggerResult.triggerResult.status}, Duration: ${triggerResult.triggerResult.durationMs}ms`,
1471
+ );
1472
+ console.error("");
1473
+ }
1474
+ } catch (err) {
1475
+ outputSpinner.stop();
1476
+ console.error("");
1477
+ error(`Failed to trigger: ${err instanceof Error ? err.message : String(err)}`);
1478
+ console.error("");
1479
+ }
1480
+ }
1481
+ }
1482
+ }
@@ -124,9 +124,12 @@ export default async function skills(subcommand?: string, args: string[] = []):
124
124
  case "remove":
125
125
  case "rm":
126
126
  return await removeSkill(args[0]);
127
+ case "upgrade":
128
+ case "update":
129
+ return await upgradeSkill(args[0]);
127
130
  default:
128
131
  error(`Unknown subcommand: ${subcommand}`);
129
- info("Available: run, list, remove");
132
+ info("Available: run, list, remove, upgrade");
130
133
  process.exit(1);
131
134
  }
132
135
  }
@@ -139,6 +142,7 @@ async function showHelp(): Promise<void> {
139
142
  console.log(" run <name> Install (if needed) and launch agent with skill");
140
143
  console.log(" list List installed skills in current project");
141
144
  console.log(" remove <name> Remove a skill from project");
145
+ console.log(" upgrade <name> Re-install skill to get latest version");
142
146
  console.log("");
143
147
  const skills = await fetchAvailableSkills();
144
148
  if (skills.length > 0) {
@@ -333,3 +337,56 @@ async function removeSkill(skillName?: string): Promise<void> {
333
337
  info(`Skill ${skillName} not found in project`);
334
338
  }
335
339
  }
340
+
341
+ async function upgradeSkill(skillName?: string): Promise<void> {
342
+ if (!skillName) {
343
+ error("Missing skill name");
344
+ info("Usage: jack skills upgrade <name>");
345
+ process.exit(1);
346
+ }
347
+
348
+ const projectDir = process.cwd();
349
+ const skillPath = join(projectDir, ".claude/skills", skillName);
350
+
351
+ // Check if installed
352
+ if (!existsSync(skillPath)) {
353
+ error(`Skill ${skillName} not installed`);
354
+ info(`Install it first: jack skills run ${skillName}`);
355
+ process.exit(1);
356
+ }
357
+
358
+ // Remove existing installation
359
+ info(`Removing old version of ${skillName}...`);
360
+ const dirs = [".agents/skills", ".claude/skills", ".codex/skills", ".cursor/skills"];
361
+ for (const dir of dirs) {
362
+ const path = join(projectDir, dir, skillName);
363
+ if (existsSync(path)) {
364
+ await rm(path, { recursive: true, force: true });
365
+ }
366
+ }
367
+
368
+ // Re-install from GitHub
369
+ info(`Installing latest ${skillName}...`);
370
+ const agentFlags = SUPPORTED_AGENTS.flatMap((a) => ["--agent", a]);
371
+ const proc = Bun.spawn(
372
+ ["npx", "skills", "add", SKILLS_REPO, "--skill", skillName, ...agentFlags, "--yes"],
373
+ {
374
+ cwd: projectDir,
375
+ stdout: "pipe",
376
+ stderr: "pipe",
377
+ },
378
+ );
379
+
380
+ const stderr = await new Response(proc.stderr).text();
381
+ await proc.exited;
382
+
383
+ if (proc.exitCode !== 0) {
384
+ error(`Failed to upgrade skill: ${skillName}`);
385
+ if (stderr.trim()) {
386
+ console.error(stderr);
387
+ }
388
+ process.exit(1);
389
+ }
390
+
391
+ success(`Upgraded ${skillName} to latest version`);
392
+ }
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { text } from "@clack/prompts";
5
5
  import {
6
+ applyReferralCode,
6
7
  checkUsernameAvailable,
7
8
  getCurrentUserProfile,
8
9
  registerUser,
@@ -114,6 +115,11 @@ export async function runLoginFlow(options?: LoginFlowOptions): Promise<LoginFlo
114
115
  isNewUser = await promptForUsername(tokens.user.email, tokens.user.first_name);
115
116
  }
116
117
 
118
+ // Prompt for referral code for new users only (one-time, no retry)
119
+ if (isNewUser && process.stdout.isTTY) {
120
+ await promptForReferral();
121
+ }
122
+
117
123
  console.error("");
118
124
  const displayName = tokens.user.first_name || "you";
119
125
  if (isNewUser) {
@@ -294,3 +300,31 @@ function normalizeToUsername(input: string): string {
294
300
  .replace(/^-+|-+$/g, "")
295
301
  .slice(0, 39);
296
302
  }
303
+
304
+ /**
305
+ * Prompt new users for a referral code (one-time, no retry on failure).
306
+ */
307
+ async function promptForReferral(): Promise<void> {
308
+ console.error("");
309
+ const referralInput = await text({
310
+ message: "Were you referred by someone? Enter their username (or press Enter to skip):",
311
+ });
312
+
313
+ if (isCancel(referralInput)) {
314
+ return;
315
+ }
316
+
317
+ const code = referralInput.trim();
318
+ if (!code) {
319
+ return;
320
+ }
321
+
322
+ try {
323
+ const result = await applyReferralCode(code);
324
+ if (result.applied) {
325
+ success("Referral applied! You'll both get a bonus when you upgrade.");
326
+ }
327
+ } catch {
328
+ // Silently continue - referral is not critical
329
+ }
330
+ }
@@ -666,6 +666,35 @@ export async function setUsername(username: string): Promise<SetUsernameResponse
666
666
  return response.json() as Promise<SetUsernameResponse>;
667
667
  }
668
668
 
669
+ export interface ApplyReferralResult {
670
+ applied: boolean;
671
+ reason?: "invalid" | "self_referral" | "already_referred";
672
+ }
673
+
674
+ /**
675
+ * Apply a referral code (username) for the current user.
676
+ * Returns whether the code was applied successfully.
677
+ */
678
+ export async function applyReferralCode(code: string): Promise<ApplyReferralResult> {
679
+ const { authFetch } = await import("./auth/index.ts");
680
+
681
+ const response = await authFetch(`${getControlApiUrl()}/v1/referral/apply`, {
682
+ method: "POST",
683
+ headers: { "Content-Type": "application/json" },
684
+ body: JSON.stringify({ code }),
685
+ });
686
+
687
+ if (response.status === 429) {
688
+ return { applied: false, reason: "invalid" };
689
+ }
690
+
691
+ if (!response.ok) {
692
+ return { applied: false, reason: "invalid" };
693
+ }
694
+
695
+ return response.json() as Promise<ApplyReferralResult>;
696
+ }
697
+
669
698
  /**
670
699
  * Get the current user's profile including username.
671
700
  */
@@ -777,6 +806,36 @@ export interface LogSessionInfo {
777
806
  expires_at: string;
778
807
  }
779
808
 
809
+ // ============================================================================
810
+ // Cron Schedule Types
811
+ // ============================================================================
812
+
813
+ export interface CronScheduleInfo {
814
+ id: string;
815
+ expression: string;
816
+ description: string;
817
+ enabled: boolean;
818
+ next_run_at: string;
819
+ last_run_at: string | null;
820
+ last_run_status: string | null;
821
+ last_run_duration_ms: number | null;
822
+ consecutive_failures: number;
823
+ created_at: string;
824
+ }
825
+
826
+ export interface CreateCronScheduleResponse {
827
+ id: string;
828
+ expression: string;
829
+ description: string;
830
+ next_run_at: string;
831
+ }
832
+
833
+ export interface TriggerCronScheduleResponse {
834
+ triggered: boolean;
835
+ status: string;
836
+ duration_ms: number;
837
+ }
838
+
780
839
  export interface StartLogSessionResponse {
781
840
  success: boolean;
782
841
  session: LogSessionInfo;
@@ -807,3 +866,100 @@ export async function startLogSession(
807
866
 
808
867
  return response.json() as Promise<StartLogSessionResponse>;
809
868
  }
869
+
870
+ // ============================================================================
871
+ // Cron Schedule Operations
872
+ // ============================================================================
873
+
874
+ /**
875
+ * Create a cron schedule for a managed project.
876
+ */
877
+ export async function createCronSchedule(
878
+ projectId: string,
879
+ expression: string,
880
+ ): Promise<CreateCronScheduleResponse> {
881
+ const { authFetch } = await import("./auth/index.ts");
882
+
883
+ const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/crons`, {
884
+ method: "POST",
885
+ headers: { "Content-Type": "application/json" },
886
+ body: JSON.stringify({ expression }),
887
+ });
888
+
889
+ if (!response.ok) {
890
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
891
+ message?: string;
892
+ };
893
+ throw new Error(err.message || `Failed to create cron schedule: ${response.status}`);
894
+ }
895
+
896
+ return response.json() as Promise<CreateCronScheduleResponse>;
897
+ }
898
+
899
+ /**
900
+ * List all cron schedules for a managed project.
901
+ */
902
+ export async function listCronSchedules(projectId: string): Promise<CronScheduleInfo[]> {
903
+ const { authFetch } = await import("./auth/index.ts");
904
+
905
+ const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/crons`);
906
+
907
+ if (!response.ok) {
908
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
909
+ message?: string;
910
+ };
911
+ throw new Error(err.message || `Failed to list cron schedules: ${response.status}`);
912
+ }
913
+
914
+ const data = (await response.json()) as { schedules: CronScheduleInfo[] };
915
+ return data.schedules;
916
+ }
917
+
918
+ /**
919
+ * Delete a cron schedule from a managed project.
920
+ */
921
+ export async function deleteCronSchedule(projectId: string, cronId: string): Promise<void> {
922
+ const { authFetch } = await import("./auth/index.ts");
923
+
924
+ const response = await authFetch(
925
+ `${getControlApiUrl()}/v1/projects/${projectId}/crons/${cronId}`,
926
+ { method: "DELETE" },
927
+ );
928
+
929
+ if (response.status === 404) {
930
+ // Already deleted, treat as success
931
+ return;
932
+ }
933
+
934
+ if (!response.ok) {
935
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
936
+ message?: string;
937
+ };
938
+ throw new Error(err.message || `Failed to delete cron schedule: ${response.status}`);
939
+ }
940
+ }
941
+
942
+ /**
943
+ * Manually trigger a cron schedule on a managed project.
944
+ */
945
+ export async function triggerCronSchedule(
946
+ projectId: string,
947
+ expression: string,
948
+ ): Promise<TriggerCronScheduleResponse> {
949
+ const { authFetch } = await import("./auth/index.ts");
950
+
951
+ const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/crons/trigger`, {
952
+ method: "POST",
953
+ headers: { "Content-Type": "application/json" },
954
+ body: JSON.stringify({ expression }),
955
+ });
956
+
957
+ if (!response.ok) {
958
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
959
+ message?: string;
960
+ };
961
+ throw new Error(err.message || `Failed to trigger cron schedule: ${response.status}`);
962
+ }
963
+
964
+ return response.json() as Promise<TriggerCronScheduleResponse>;
965
+ }
@@ -9,6 +9,7 @@ import { CONFIG_DIR } from "./config.ts";
9
9
  * MCP server configuration structure
10
10
  */
11
11
  export interface McpServerConfig {
12
+ type: "stdio";
12
13
  command: string;
13
14
  args: string[];
14
15
  env?: Record<string, string>;
@@ -48,19 +49,40 @@ export const APP_MCP_CONFIGS: Record<string, AppMcpConfig> = {
48
49
  const JACK_MCP_CONFIG_DIR = join(CONFIG_DIR, "mcp");
49
50
  const JACK_MCP_CONFIG_PATH = join(JACK_MCP_CONFIG_DIR, "config.json");
50
51
 
52
+ /**
53
+ * Find the jack binary path
54
+ * Checks common install locations
55
+ */
56
+ function findJackBinary(): string {
57
+ const bunBin = join(homedir(), ".bun", "bin", "jack");
58
+ const npmBin = join(homedir(), ".npm-global", "bin", "jack");
59
+ const homebrewBin = "/opt/homebrew/bin/jack";
60
+ const usrLocalBin = "/usr/local/bin/jack";
61
+
62
+ // Check in order of priority
63
+ for (const path of [bunBin, npmBin, homebrewBin, usrLocalBin]) {
64
+ if (existsSync(path)) {
65
+ return path;
66
+ }
67
+ }
68
+
69
+ // Fallback to just "jack" and hope PATH works
70
+ return "jack";
71
+ }
72
+
51
73
  /**
52
74
  * Returns the jack MCP server configuration
53
- * Includes PATH with common install locations so Claude Desktop can find jack
75
+ * Uses full path to jack binary for reliability
54
76
  */
55
77
  export function getJackMcpConfig(): McpServerConfig {
56
- // Build PATH with common locations where jack might be installed
57
- // ~/.bun/bin is where `bun link` installs global commands
78
+ // Build PATH with common locations (still needed for child processes)
58
79
  const bunBin = join(homedir(), ".bun", "bin");
59
80
  const npmBin = join(homedir(), ".npm-global", "bin");
60
81
  const defaultPaths = [bunBin, npmBin, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
61
82
 
62
83
  return {
63
- command: "jack",
84
+ type: "stdio",
85
+ command: findJackBinary(),
64
86
  args: ["mcp", "serve"],
65
87
  env: {
66
88
  PATH: defaultPaths.join(":"),
package/src/lib/output.ts CHANGED
@@ -179,7 +179,8 @@ export function box(title: string, lines: string[]): void {
179
179
  const gradient = "░".repeat(innerWidth);
180
180
 
181
181
  // Truncate text if too long for box
182
- const truncate = (text: string) => (text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text);
182
+ const truncate = (text: string) =>
183
+ text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text;
183
184
 
184
185
  // Pad plain text first, then apply colors (ANSI codes break padEnd calculation)
185
186
  const pad = (text: string) => ` ${truncate(text).padEnd(maxLen)} `;
@@ -226,7 +227,8 @@ export function celebrate(title: string, lines: string[]): void {
226
227
  const space = " ".repeat(innerWidth);
227
228
 
228
229
  // Truncate text if too long for box
229
- const truncate = (text: string) => (text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text);
230
+ const truncate = (text: string) =>
231
+ text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text;
230
232
 
231
233
  // Center text based on visual length, then apply colors
232
234
  const center = (text: string, applyBold = false) => {