@getjack/jack 0.1.31 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "description": "Ship before you forget why you started. The vibecoder's deployment CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -99,6 +99,7 @@ async function resolveDatabaseInfo(projectName: string): Promise<ResolvedDatabas
99
99
 
100
100
  interface ServiceOptions {
101
101
  project?: string;
102
+ json?: boolean;
102
103
  }
103
104
 
104
105
  export default async function services(
@@ -209,17 +210,23 @@ async function resolveProjectName(options: ServiceOptions): Promise<string> {
209
210
  * Show database information
210
211
  */
211
212
  async function dbInfo(options: ServiceOptions): Promise<void> {
213
+ const jsonOutput = options.json ?? false;
212
214
  const projectName = await resolveProjectName(options);
213
215
  const projectDir = process.cwd();
214
216
  const link = await readProjectLink(projectDir);
215
217
 
216
218
  // For managed projects, use control plane API (no wrangler dependency)
217
219
  if (link?.deploy_mode === "managed") {
218
- outputSpinner.start("Fetching database info...");
220
+ if (!jsonOutput) outputSpinner.start("Fetching database info...");
219
221
  try {
220
222
  const { getManagedDatabaseInfo } = await import("../lib/control-plane.ts");
221
223
  const dbInfo = await getManagedDatabaseInfo(link.project_id);
222
- outputSpinner.stop();
224
+ if (!jsonOutput) outputSpinner.stop();
225
+
226
+ if (jsonOutput) {
227
+ console.log(JSON.stringify({ name: dbInfo.name, id: dbInfo.id, sizeBytes: dbInfo.sizeBytes, numTables: dbInfo.numTables, source: "managed" }));
228
+ return;
229
+ }
223
230
 
224
231
  console.error("");
225
232
  success(`Database: ${dbInfo.name}`);
@@ -231,7 +238,14 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
231
238
  console.error("");
232
239
  return;
233
240
  } catch (err) {
234
- outputSpinner.stop();
241
+ if (!jsonOutput) outputSpinner.stop();
242
+ if (jsonOutput) {
243
+ const msg = err instanceof Error && err.message.includes("No database found")
244
+ ? "No database configured. Run 'jack services db create' to create one."
245
+ : `Failed to fetch database info: ${err instanceof Error ? err.message : String(err)}`;
246
+ console.log(JSON.stringify({ success: false, error: msg }));
247
+ process.exit(1);
248
+ }
235
249
  console.error("");
236
250
  if (err instanceof Error && err.message.includes("No database found")) {
237
251
  error("No database found for this project");
@@ -248,6 +262,10 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
248
262
  const dbInfo = await resolveDatabaseInfo(projectName);
249
263
 
250
264
  if (!dbInfo) {
265
+ if (jsonOutput) {
266
+ console.log(JSON.stringify({ success: false, error: "No database found for this project" }));
267
+ return;
268
+ }
251
269
  console.error("");
252
270
  error("No database found for this project");
253
271
  info("Create one with: jack services db create");
@@ -256,11 +274,15 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
256
274
  }
257
275
 
258
276
  // Fetch detailed database info via wrangler
259
- outputSpinner.start("Fetching database info...");
277
+ if (!jsonOutput) outputSpinner.start("Fetching database info...");
260
278
  const wranglerDbInfo = await getWranglerDatabaseInfo(dbInfo.name);
261
- outputSpinner.stop();
279
+ if (!jsonOutput) outputSpinner.stop();
262
280
 
263
281
  if (!wranglerDbInfo) {
282
+ if (jsonOutput) {
283
+ console.log(JSON.stringify({ success: false, error: "Database not found" }));
284
+ process.exit(1);
285
+ }
264
286
  console.error("");
265
287
  error("Database not found");
266
288
  info("It may have been deleted");
@@ -268,6 +290,11 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
268
290
  process.exit(1);
269
291
  }
270
292
 
293
+ if (jsonOutput) {
294
+ console.log(JSON.stringify({ name: wranglerDbInfo.name, id: dbInfo.id || wranglerDbInfo.id, sizeBytes: wranglerDbInfo.sizeBytes, numTables: wranglerDbInfo.numTables, source: "byo" }));
295
+ return;
296
+ }
297
+
271
298
  // Display info
272
299
  console.error("");
273
300
  success(`Database: ${wranglerDbInfo.name}`);
@@ -624,10 +651,16 @@ async function dbCreate(args: string[], options: ServiceOptions): Promise<void>
624
651
  * List all databases in the project
625
652
  */
626
653
  async function dbList(options: ServiceOptions): Promise<void> {
627
- outputSpinner.start("Fetching databases...");
654
+ const jsonOutput = options.json ?? false;
655
+ if (!jsonOutput) outputSpinner.start("Fetching databases...");
628
656
  try {
629
657
  const databases = await listDatabases(process.cwd());
630
- outputSpinner.stop();
658
+ if (!jsonOutput) outputSpinner.stop();
659
+
660
+ if (jsonOutput) {
661
+ console.log(JSON.stringify(databases));
662
+ return;
663
+ }
631
664
 
632
665
  if (databases.length === 0) {
633
666
  console.error("");
@@ -654,7 +687,11 @@ async function dbList(options: ServiceOptions): Promise<void> {
654
687
  console.error("");
655
688
  }
656
689
  } catch (err) {
657
- outputSpinner.stop();
690
+ if (!jsonOutput) outputSpinner.stop();
691
+ if (jsonOutput) {
692
+ console.log(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
693
+ process.exit(1);
694
+ }
658
695
  console.error("");
659
696
  error(`Failed to list databases: ${err instanceof Error ? err.message : String(err)}`);
660
697
  process.exit(1);
@@ -729,7 +766,8 @@ function parseExecuteArgs(args: string[]): ExecuteArgs {
729
766
  /**
730
767
  * Execute SQL against the database
731
768
  */
732
- async function dbExecute(args: string[], _options: ServiceOptions): Promise<void> {
769
+ async function dbExecute(args: string[], options: ServiceOptions): Promise<void> {
770
+ const jsonOutput = options.json ?? false;
733
771
  const execArgs = parseExecuteArgs(args);
734
772
 
735
773
  // Validate input
@@ -766,7 +804,7 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
766
804
  const projectDir = process.cwd();
767
805
 
768
806
  try {
769
- outputSpinner.start("Executing SQL...");
807
+ if (!jsonOutput) outputSpinner.start("Executing SQL...");
770
808
 
771
809
  let result;
772
810
  if (execArgs.filePath) {
@@ -823,7 +861,7 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
823
861
  }
824
862
 
825
863
  // NOW execute with confirmation
826
- outputSpinner.start("Executing SQL...");
864
+ if (!jsonOutput) outputSpinner.start("Executing SQL...");
827
865
  if (execArgs.filePath) {
828
866
  result = await executeSqlFile({
829
867
  projectDir,
@@ -857,12 +895,37 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
857
895
  error_type: "execution_failed",
858
896
  });
859
897
 
898
+ if (jsonOutput) {
899
+ console.log(JSON.stringify({ success: false, error: result.error || "SQL execution failed" }));
900
+ process.exit(1);
901
+ }
902
+
860
903
  console.error("");
861
904
  error(result.error || "SQL execution failed");
862
905
  console.error("");
863
906
  process.exit(1);
864
907
  }
865
908
 
909
+ // Track telemetry
910
+ track(Events.SQL_EXECUTED, {
911
+ success: true,
912
+ risk_level: result.risk,
913
+ statement_count: result.statements.length,
914
+ from_file: !!execArgs.filePath,
915
+ });
916
+
917
+ // JSON output mode — structured result for agents
918
+ if (jsonOutput) {
919
+ console.log(JSON.stringify({
920
+ success: true,
921
+ results: result.results ?? [],
922
+ meta: result.meta,
923
+ risk: result.risk,
924
+ warning: result.warning,
925
+ }));
926
+ return;
927
+ }
928
+
866
929
  // Show results
867
930
  console.error("");
868
931
  success(`SQL executed (${getRiskDescription(result.risk)})`);
@@ -886,14 +949,6 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
886
949
  console.log(JSON.stringify(result.results, null, 2));
887
950
  }
888
951
  console.error("");
889
-
890
- // Track telemetry
891
- track(Events.SQL_EXECUTED, {
892
- success: true,
893
- risk_level: result.risk,
894
- statement_count: result.statements.length,
895
- from_file: !!execArgs.filePath,
896
- });
897
952
  } catch (err) {
898
953
  outputSpinner.stop();
899
954
 
@@ -904,6 +959,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
904
959
  risk_level: err.risk,
905
960
  });
906
961
 
962
+ if (jsonOutput) {
963
+ console.log(JSON.stringify({ success: false, error: err.message, suggestion: "Add --write flag" }));
964
+ process.exit(1);
965
+ }
966
+
907
967
  console.error("");
908
968
  error(err.message);
909
969
  info("Add the --write flag to allow data modification:");
@@ -919,6 +979,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
919
979
  risk_level: "destructive",
920
980
  });
921
981
 
982
+ if (jsonOutput) {
983
+ console.log(JSON.stringify({ success: false, error: err.message, suggestion: "Destructive operations require confirmation via CLI" }));
984
+ process.exit(1);
985
+ }
986
+
922
987
  console.error("");
923
988
  error(err.message);
924
989
  info("Destructive operations require confirmation via CLI.");
@@ -931,6 +996,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
931
996
  error_type: "unknown",
932
997
  });
933
998
 
999
+ if (jsonOutput) {
1000
+ console.log(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }));
1001
+ process.exit(1);
1002
+ }
1003
+
934
1004
  console.error("");
935
1005
  error(`SQL execution failed: ${err instanceof Error ? err.message : String(err)}`);
936
1006
  console.error("");
@@ -3,14 +3,15 @@ import { createReporter, output } from "../lib/output.ts";
3
3
  import { deployProject } from "../lib/project-operations.ts";
4
4
 
5
5
  export default async function ship(
6
- options: { managed?: boolean; byo?: boolean; dryRun?: boolean } = {},
6
+ options: { managed?: boolean; byo?: boolean; dryRun?: boolean; json?: boolean } = {},
7
7
  ): Promise<void> {
8
8
  const isCi = process.env.CI === "true" || process.env.CI === "1";
9
+ const jsonOutput = options.json ?? false;
9
10
  try {
10
11
  const result = await deployProject({
11
12
  projectPath: process.cwd(),
12
- reporter: createReporter(),
13
- interactive: !isCi,
13
+ reporter: jsonOutput ? undefined : createReporter(),
14
+ interactive: !isCi && !jsonOutput,
14
15
  includeSecrets: !options.dryRun,
15
16
  includeSync: !options.dryRun,
16
17
  managed: options.managed,
@@ -18,11 +19,35 @@ export default async function ship(
18
19
  dryRun: options.dryRun,
19
20
  });
20
21
 
22
+ if (jsonOutput) {
23
+ console.log(
24
+ JSON.stringify({
25
+ success: true,
26
+ projectName: result.projectName,
27
+ url: result.workerUrl,
28
+ deployMode: result.deployMode,
29
+ }),
30
+ );
31
+ return;
32
+ }
33
+
21
34
  if (!result.workerUrl && result.deployOutput) {
22
35
  console.error(result.deployOutput);
23
36
  }
24
37
  } catch (error) {
25
38
  const details = getErrorDetails(error);
39
+
40
+ if (jsonOutput) {
41
+ console.log(
42
+ JSON.stringify({
43
+ success: false,
44
+ error: details.message,
45
+ suggestion: details.suggestion,
46
+ }),
47
+ );
48
+ process.exit(details.meta?.exitCode ?? 1);
49
+ }
50
+
26
51
  if (!details.meta?.reported) {
27
52
  output.error(details.message);
28
53
  }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * jack tokens - Manage API tokens for headless authentication
3
+ *
4
+ * Tokens are account-level (not project-scoped).
5
+ * Set JACK_API_TOKEN in your environment for CI/CD and automated pipelines.
6
+ */
7
+
8
+ import { error, info, success } from "../lib/output.ts";
9
+ import {
10
+ type TokenInfo,
11
+ createApiToken,
12
+ listApiTokens,
13
+ revokeApiToken,
14
+ } from "../lib/services/token-operations.ts";
15
+ import { Events, track } from "../lib/telemetry.ts";
16
+
17
+ export default async function tokens(
18
+ subcommand?: string,
19
+ args: string[] = [],
20
+ flags: Record<string, unknown> = {},
21
+ ): Promise<void> {
22
+ if (!subcommand) {
23
+ return showHelp();
24
+ }
25
+
26
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
27
+ return showHelp();
28
+ }
29
+
30
+ switch (subcommand) {
31
+ case "create":
32
+ case "new":
33
+ return await createToken(args, flags);
34
+ case "list":
35
+ case "ls":
36
+ return await listTokens();
37
+ case "revoke":
38
+ case "rm":
39
+ case "delete":
40
+ return await revokeToken(args);
41
+ default:
42
+ error(`Unknown subcommand: ${subcommand}`);
43
+ info("Available: create, list, revoke");
44
+ process.exit(1);
45
+ }
46
+ }
47
+
48
+ function showHelp(): void {
49
+ console.error("");
50
+ info("jack tokens - Manage API tokens for headless authentication");
51
+ console.error("");
52
+ console.error("Commands:");
53
+ console.error(" create [name] Create a new API token");
54
+ console.error(" list List active tokens");
55
+ console.error(" revoke <id> Revoke a token");
56
+ console.error("");
57
+ console.error("Usage:");
58
+ console.error(" Set JACK_API_TOKEN in your environment for headless auth.");
59
+ console.error(" Tokens work in CI/CD, Docker, and automated pipelines.");
60
+ console.error("");
61
+ }
62
+
63
+ async function createToken(args: string[], flags: Record<string, unknown> = {}): Promise<void> {
64
+ // Accept name from --name flag or first positional arg
65
+ let name = "CLI Token";
66
+ if (flags.name && typeof flags.name === "string") {
67
+ name = flags.name;
68
+ } else if (args[0] && !args[0].startsWith("-")) {
69
+ name = args[0];
70
+ }
71
+
72
+ const data = await createApiToken(name);
73
+
74
+ track(Events.TOKEN_CREATED);
75
+
76
+ success("Token created");
77
+ console.error("");
78
+ console.error(` ${data.token}`);
79
+ console.error("");
80
+ console.error(" Save this token -- it will not be shown again.");
81
+ console.error("");
82
+ console.error(" Usage:");
83
+ console.error(" export JACK_API_TOKEN=<token>");
84
+ console.error(" jack ship");
85
+ console.error("");
86
+ }
87
+
88
+ async function listTokens(): Promise<void> {
89
+ const tokenList = await listApiTokens();
90
+
91
+ if (tokenList.length === 0) {
92
+ info("No active tokens");
93
+ return;
94
+ }
95
+
96
+ console.error("");
97
+ for (const t of tokenList) {
98
+ const lastUsed = t.last_used_at ? `last used ${t.last_used_at}` : "never used";
99
+ console.error(` ${t.id} ${t.name} (${lastUsed})`);
100
+ }
101
+ console.error("");
102
+ }
103
+
104
+ async function revokeToken(args: string[]): Promise<void> {
105
+ const tokenId = args[0];
106
+
107
+ if (!tokenId) {
108
+ error("Missing token ID");
109
+ info("Usage: jack tokens revoke <token-id>");
110
+ info("Run 'jack tokens list' to see token IDs");
111
+ process.exit(1);
112
+ }
113
+
114
+ await revokeApiToken(tokenId);
115
+
116
+ track(Events.TOKEN_REVOKED);
117
+
118
+ success(`Token revoked: ${tokenId}`);
119
+ }
@@ -19,13 +19,17 @@ export default async function whoami(): Promise<void> {
19
19
  item(`Name: ${creds.user.first_name}${creds.user.last_name ? ` ${creds.user.last_name}` : ""}`);
20
20
  }
21
21
 
22
- const expiresIn = creds.expires_at - Math.floor(Date.now() / 1000);
23
- if (expiresIn > 0) {
24
- const hours = Math.floor(expiresIn / 3600);
25
- const minutes = Math.floor((expiresIn % 3600) / 60);
26
- item(`Token expires: ${hours}h ${minutes}m`);
22
+ if (process.env.JACK_API_TOKEN) {
23
+ item("Auth: API token");
27
24
  } else {
28
- item("Token: expired (will refresh on next request)");
25
+ const expiresIn = creds.expires_at - Math.floor(Date.now() / 1000);
26
+ if (expiresIn > 0) {
27
+ const hours = Math.floor(expiresIn / 3600);
28
+ const minutes = Math.floor((expiresIn % 3600) / 60);
29
+ item(`Token expires: ${hours}h ${minutes}m`);
30
+ } else {
31
+ item("Token: expired (will refresh on next request)");
32
+ }
29
33
  }
30
34
  console.error("");
31
35
  }
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ const cli = meow(
35
35
  login Sign in
36
36
  logout Sign out
37
37
  whoami Show current user
38
+ tokens Manage API tokens
38
39
  update Update jack to latest version
39
40
 
40
41
  Project Management
@@ -181,6 +182,9 @@ const cli = meow(
181
182
  sort: {
182
183
  type: "string",
183
184
  },
185
+ name: {
186
+ type: "string",
187
+ },
184
188
  },
185
189
  },
186
190
  );
@@ -208,6 +212,7 @@ const [command, ...args] = cli.input;
208
212
  os: process.platform,
209
213
  arch: process.arch,
210
214
  node_version: process.version,
215
+ auth_method: process.env.JACK_API_TOKEN ? "token" : "oauth",
211
216
  });
212
217
 
213
218
  // Update lastIdentifyDate
@@ -220,6 +225,7 @@ const [command, ...args] = cli.input;
220
225
  os: process.platform,
221
226
  arch: process.arch,
222
227
  node_version: process.version,
228
+ auth_method: process.env.JACK_API_TOKEN ? "token" : "oauth",
223
229
  });
224
230
  }
225
231
  })();
@@ -285,6 +291,7 @@ try {
285
291
  managed: cli.flags.managed,
286
292
  byo: cli.flags.byo,
287
293
  dryRun: cli.flags.dryRun,
294
+ json: cli.flags.json,
288
295
  });
289
296
  break;
290
297
  }
@@ -389,6 +396,7 @@ try {
389
396
 
390
397
  await withTelemetry("services", services, { subcommand })(args[0], serviceArgs, {
391
398
  project: cli.flags.project,
399
+ json: cli.flags.json,
392
400
  });
393
401
  break;
394
402
  }
@@ -399,6 +407,15 @@ try {
399
407
  });
400
408
  break;
401
409
  }
410
+ case "tokens": {
411
+ const { default: tokens } = await import("./commands/tokens.ts");
412
+ await withTelemetry("tokens", tokens, { subcommand: args[0] })(
413
+ args[0],
414
+ args.slice(1),
415
+ cli.flags,
416
+ );
417
+ break;
418
+ }
402
419
  case "domain": {
403
420
  const { default: domain } = await import("./commands/domain.ts");
404
421
  await withTelemetry("domain", domain, { subcommand: args[0] })(args[0], args.slice(1));
@@ -73,6 +73,16 @@ export async function refreshToken(refreshTokenValue: string): Promise<TokenResp
73
73
  }
74
74
 
75
75
  export async function getValidAccessToken(): Promise<string | null> {
76
+ // Priority 1: API token from environment
77
+ const apiToken = process.env.JACK_API_TOKEN;
78
+ if (apiToken) {
79
+ if (!apiToken.startsWith("jkt_")) {
80
+ console.error("Warning: JACK_API_TOKEN should start with 'jkt_'");
81
+ }
82
+ return apiToken;
83
+ }
84
+
85
+ // Priority 2: Stored OAuth credentials
76
86
  const creds = await getCredentials();
77
87
  if (!creds) {
78
88
  return null;
@@ -102,7 +112,7 @@ export async function getValidAccessToken(): Promise<string | null> {
102
112
  export async function authFetch(url: string, options: RequestInit = {}): Promise<Response> {
103
113
  const token = await getValidAccessToken();
104
114
  if (!token) {
105
- throw new Error("Not authenticated. Run 'jack login' first.");
115
+ throw new Error("Not authenticated. Run 'jack login' or set JACK_API_TOKEN.");
106
116
  }
107
117
 
108
118
  return fetch(url, {
@@ -13,7 +13,7 @@ export async function requireAuth(): Promise<string> {
13
13
  throw new JackError(
14
14
  JackErrorCode.AUTH_FAILED,
15
15
  "Not logged in",
16
- "Run 'jack login' to sign in to jack cloud",
16
+ "Run 'jack login' to sign in, or set JACK_API_TOKEN for headless use",
17
17
  );
18
18
  }
19
19
 
@@ -51,6 +51,9 @@ export type AuthState = "logged-in" | "not-logged-in" | "session-expired";
51
51
  * - "session-expired": had credentials but refresh failed
52
52
  */
53
53
  export async function getAuthState(): Promise<AuthState> {
54
+ // API token always counts as logged in
55
+ if (process.env.JACK_API_TOKEN) return "logged-in";
56
+
54
57
  const creds = await getCredentials();
55
58
  if (!creds) return "not-logged-in";
56
59
 
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Token operations service layer for jack cloud
3
+ *
4
+ * Provides shared API token management functions for both CLI and MCP.
5
+ * Returns pure data - no console.log or process.exit.
6
+ */
7
+
8
+ import { authFetch } from "../auth/index.ts";
9
+ import { getControlApiUrl } from "../control-plane.ts";
10
+
11
+ // ============================================================================
12
+ // Types
13
+ // ============================================================================
14
+
15
+ export interface CreateTokenResult {
16
+ token: string;
17
+ id: string;
18
+ name: string;
19
+ created_at: string;
20
+ expires_at: string | null;
21
+ }
22
+
23
+ export interface TokenInfo {
24
+ id: string;
25
+ name: string;
26
+ id_prefix: string;
27
+ created_at: string;
28
+ last_used_at: string | null;
29
+ expires_at: string | null;
30
+ }
31
+
32
+ // ============================================================================
33
+ // Service Functions
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Create a new API token for headless authentication.
38
+ */
39
+ export async function createApiToken(
40
+ name: string,
41
+ expiresInDays?: number,
42
+ ): Promise<CreateTokenResult> {
43
+ const response = await authFetch(`${getControlApiUrl()}/v1/tokens`, {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify({ name, expires_in_days: expiresInDays }),
47
+ });
48
+
49
+ if (!response.ok) {
50
+ const err = (await response.json().catch(() => ({}))) as { message?: string };
51
+ throw new Error(err.message || `Failed to create token: ${response.status}`);
52
+ }
53
+
54
+ return response.json() as Promise<CreateTokenResult>;
55
+ }
56
+
57
+ /**
58
+ * List all active API tokens for the current user.
59
+ */
60
+ export async function listApiTokens(): Promise<TokenInfo[]> {
61
+ const response = await authFetch(`${getControlApiUrl()}/v1/tokens`);
62
+
63
+ if (!response.ok) {
64
+ const err = (await response.json().catch(() => ({}))) as { message?: string };
65
+ throw new Error(err.message || `Failed to list tokens: ${response.status}`);
66
+ }
67
+
68
+ const data = (await response.json()) as { tokens: TokenInfo[] };
69
+ return data.tokens;
70
+ }
71
+
72
+ /**
73
+ * Revoke an API token by ID.
74
+ */
75
+ export async function revokeApiToken(tokenId: string): Promise<void> {
76
+ const response = await authFetch(`${getControlApiUrl()}/v1/tokens/${tokenId}`, {
77
+ method: "DELETE",
78
+ });
79
+
80
+ if (!response.ok) {
81
+ const err = (await response.json().catch(() => ({}))) as { message?: string };
82
+ throw new Error(err.message || `Failed to revoke token: ${response.status}`);
83
+ }
84
+ }
@@ -44,6 +44,9 @@ export const Events = {
44
44
  BYO_DEPLOY_STARTED: "byo_deploy_started",
45
45
  BYO_DEPLOY_COMPLETED: "byo_deploy_completed",
46
46
  BYO_DEPLOY_FAILED: "byo_deploy_failed",
47
+ // Token management events
48
+ TOKEN_CREATED: "token_created",
49
+ TOKEN_REVOKED: "token_revoked",
47
50
  } as const;
48
51
 
49
52
  type EventName = (typeof Events)[keyof typeof Events];
@@ -127,6 +130,7 @@ export interface UserProperties {
127
130
  is_tty?: boolean;
128
131
  locale?: string;
129
132
  config_style?: "byoc" | "jack-cloud";
133
+ auth_method?: "oauth" | "token";
130
134
  }
131
135
 
132
136
  // Detect environment properties (for user profile - stable properties)
@@ -31,6 +31,13 @@ export function registerResources(
31
31
  description: "Semantic information about jack's capabilities for AI agents",
32
32
  mimeType: "application/json",
33
33
  },
34
+ {
35
+ uri: "agents://workflows",
36
+ name: "Workflow Recipes",
37
+ description:
38
+ "Multi-step workflow templates for common tasks like creating APIs with databases, debugging production issues, and setting up cron jobs",
39
+ mimeType: "text/markdown",
40
+ },
34
41
  ],
35
42
  };
36
43
  });
@@ -160,6 +167,172 @@ Check for JACK.md in the project root for project-specific instructions.
160
167
  };
161
168
  }
162
169
 
170
+ if (uri === "agents://workflows") {
171
+ const workflows = `# Jack Workflow Recipes
172
+
173
+ Multi-step workflows for common tasks. Each workflow can be executed by an agent team or a single agent working sequentially.
174
+
175
+ ---
176
+
177
+ ## 1. Create API with Database
178
+
179
+ Goal: Scaffold a new API, add a database, create tables, and verify.
180
+
181
+ \`\`\`
182
+ Step 1: Create project
183
+ jack new my-api --template api
184
+
185
+ Step 2: Add database
186
+ mcp__jack__create_database (or: jack services db create)
187
+
188
+ Step 3: Create schema
189
+ mcp__jack__execute_sql
190
+ sql: "CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP)"
191
+ allow_write: true
192
+
193
+ Step 4: Edit src/index.ts — add routes that use c.env.DB
194
+
195
+ Step 5: Deploy
196
+ mcp__jack__deploy_project (or: jack ship)
197
+
198
+ Step 6: Verify
199
+ curl https://<slug>.runjack.xyz/api/items
200
+ \`\`\`
201
+
202
+ ---
203
+
204
+ ## 2. Debug Production Issue
205
+
206
+ Goal: Identify and fix a bug reported in production.
207
+
208
+ \`\`\`
209
+ Step 1: Check current status
210
+ mcp__jack__get_project_status (or: jack info)
211
+
212
+ Step 2: Collect recent logs
213
+ mcp__jack__tail_logs with max_events: 100, duration_ms: 5000
214
+
215
+ Step 3: Inspect database state if relevant
216
+ mcp__jack__execute_sql
217
+ sql: "SELECT * FROM <table> WHERE <condition>"
218
+
219
+ Step 4: Read and fix the source code
220
+
221
+ Step 5: Deploy fix
222
+ mcp__jack__deploy_project (or: jack ship)
223
+
224
+ Step 6: Verify fix via logs
225
+ mcp__jack__tail_logs with max_events: 20, duration_ms: 3000
226
+ \`\`\`
227
+
228
+ ---
229
+
230
+ ## 3. Add Scheduled Task (Cron)
231
+
232
+ Goal: Run code on a schedule (cleanup, sync, reports).
233
+
234
+ \`\`\`
235
+ Step 1: Add scheduled handler to src/index.ts
236
+ export default {
237
+ async fetch(request, env) { ... },
238
+ async scheduled(event, env, ctx) {
239
+ // your cron logic here
240
+ },
241
+ };
242
+
243
+ Or with Hono, add a POST /__scheduled route.
244
+
245
+ Step 2: Deploy the handler
246
+ mcp__jack__deploy_project (or: jack ship)
247
+
248
+ Step 3: Create cron schedule
249
+ mcp__jack__create_cron with expression: "0 * * * *"
250
+ (or: jack services cron create "0 * * * *")
251
+
252
+ Step 4: Verify schedule
253
+ mcp__jack__test_cron with expression: "0 * * * *"
254
+ Shows next 5 scheduled times.
255
+
256
+ Step 5: Monitor via logs
257
+ mcp__jack__tail_logs
258
+ \`\`\`
259
+
260
+ ---
261
+
262
+ ## 4. Add File Storage
263
+
264
+ Goal: Enable file uploads and downloads via R2.
265
+
266
+ \`\`\`
267
+ Step 1: Create storage bucket
268
+ mcp__jack__create_storage_bucket (or: jack services storage create)
269
+
270
+ Step 2: Deploy to activate binding
271
+ mcp__jack__deploy_project (or: jack ship)
272
+
273
+ Step 3: Add upload/download routes to src/index.ts
274
+ Upload: c.env.BUCKET.put(key, body)
275
+ Download: c.env.BUCKET.get(key)
276
+ List: c.env.BUCKET.list()
277
+
278
+ Step 4: Deploy routes
279
+ mcp__jack__deploy_project (or: jack ship)
280
+ \`\`\`
281
+
282
+ ---
283
+
284
+ ## 5. Add Semantic Search (Vectorize + AI)
285
+
286
+ Goal: Enable vector similarity search using embeddings.
287
+
288
+ \`\`\`
289
+ Step 1: Create vector index
290
+ mcp__jack__create_vectorize_index (or: jack services vectorize create)
291
+ Default: 768 dimensions, cosine metric (matches bge-base-en-v1.5)
292
+
293
+ Step 2: Deploy to activate binding
294
+ mcp__jack__deploy_project (or: jack ship)
295
+
296
+ Step 3: Add indexing route
297
+ Generate embedding: c.env.AI.run("@cf/baai/bge-base-en-v1.5", { text: [input] })
298
+ Insert: c.env.VECTORIZE_INDEX.insert([{ id, values, metadata }])
299
+
300
+ Step 4: Add search route
301
+ Generate query embedding, then:
302
+ c.env.VECTORIZE_INDEX.query(embedding, { topK: 5, returnMetadata: "all" })
303
+
304
+ Step 5: Deploy and test
305
+ mcp__jack__deploy_project (or: jack ship)
306
+ \`\`\`
307
+
308
+ ---
309
+
310
+ ## Parallel Task Patterns (Agent Teams)
311
+
312
+ For Opus 4.6 Agent Teams, these tasks can run in parallel:
313
+
314
+ **Independent (run simultaneously):**
315
+ - Creating database + creating storage bucket
316
+ - Reading logs + checking project status
317
+ - Multiple SQL queries on different tables
318
+
319
+ **Sequential (must wait for previous):**
320
+ - Create project → then add services
321
+ - Create database → then create tables → then deploy
322
+ - Edit code → then deploy → then verify
323
+ `;
324
+
325
+ return {
326
+ contents: [
327
+ {
328
+ uri,
329
+ mimeType: "text/markdown",
330
+ text: workflows,
331
+ },
332
+ ],
333
+ };
334
+ }
335
+
163
336
  throw new Error(`Unknown resource URI: ${uri}`);
164
337
  });
165
338
  }