md4ai 0.10.0 → 0.10.2

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.
@@ -45,6 +45,23 @@ function createSupabaseClient(anonKey, accessToken) {
45
45
  const options = accessToken ? { global: { headers: { Authorization: `Bearer ${accessToken}` } } } : {};
46
46
  return createClient(SUPABASE_URL, anonKey, options);
47
47
  }
48
+ async function createSupabaseClientWithSession(anonKey, accessToken, refreshToken) {
49
+ const client = createClient(SUPABASE_URL, anonKey);
50
+ const { data, error } = await client.auth.setSession({
51
+ access_token: accessToken,
52
+ refresh_token: refreshToken
53
+ });
54
+ if (error || !data.session) {
55
+ throw new Error(error?.message ?? "Failed to establish session");
56
+ }
57
+ return {
58
+ client,
59
+ userId: data.session.user.id,
60
+ newAccessToken: data.session.access_token,
61
+ newRefreshToken: data.session.refresh_token,
62
+ expiresAt: Date.now() + data.session.expires_in * 1e3
63
+ };
64
+ }
48
65
  var init_supabase = __esm({
49
66
  "../packages/shared/dist/supabase.js"() {
50
67
  "use strict";
@@ -192,6 +209,51 @@ async function getAuthenticatedClient() {
192
209
  const supabase = createSupabaseClient(anonKey, creds.accessToken);
193
210
  return { supabase, userId: creds.userId };
194
211
  }
212
+ async function refreshSession() {
213
+ const creds = await loadCredentials();
214
+ if (!creds?.refreshToken)
215
+ return null;
216
+ try {
217
+ const anonKey = getAnonKey();
218
+ const result = await createSupabaseClientWithSession(anonKey, creds.accessToken, creds.refreshToken);
219
+ await saveCredentials({
220
+ ...creds,
221
+ accessToken: result.newAccessToken,
222
+ refreshToken: result.newRefreshToken,
223
+ expiresAt: result.expiresAt
224
+ });
225
+ return { supabase: result.client, userId: result.userId };
226
+ } catch {
227
+ return null;
228
+ }
229
+ }
230
+ async function getLongLivedClient() {
231
+ const creds = await loadCredentials();
232
+ if (!creds?.accessToken || !creds?.refreshToken) {
233
+ console.log(chalk4.yellow("Not logged in."));
234
+ const shouldLogin = await confirm({ message: "Would you like to log in now?" });
235
+ if (!shouldLogin)
236
+ process.exit(0);
237
+ return promptLogin();
238
+ }
239
+ const anonKey = getAnonKey();
240
+ try {
241
+ const result = await createSupabaseClientWithSession(anonKey, creds.accessToken, creds.refreshToken);
242
+ await saveCredentials({
243
+ ...creds,
244
+ accessToken: result.newAccessToken,
245
+ refreshToken: result.newRefreshToken,
246
+ expiresAt: result.expiresAt
247
+ });
248
+ return { supabase: result.client, userId: result.userId };
249
+ } catch {
250
+ console.log(chalk4.yellow("Session expired \u2014 please log in again."));
251
+ const shouldLogin = await confirm({ message: "Would you like to log in now?" });
252
+ if (!shouldLogin)
253
+ process.exit(0);
254
+ return promptLogin();
255
+ }
256
+ }
195
257
  async function promptLogin() {
196
258
  console.log("");
197
259
  const email = await input2({ message: "Email:" });
@@ -1723,7 +1785,7 @@ var CURRENT_VERSION;
1723
1785
  var init_check_update = __esm({
1724
1786
  "dist/check-update.js"() {
1725
1787
  "use strict";
1726
- CURRENT_VERSION = true ? "0.10.0" : "0.0.0-dev";
1788
+ CURRENT_VERSION = true ? "0.10.2" : "0.0.0-dev";
1727
1789
  }
1728
1790
  });
1729
1791
 
@@ -2687,8 +2749,14 @@ function formatUptime(seconds) {
2687
2749
  const m = Math.floor(seconds % 3600 / 60);
2688
2750
  return `${h}h ${m}m`;
2689
2751
  }
2752
+ function isJwtError(error) {
2753
+ if (!error?.message)
2754
+ return false;
2755
+ const msg = error.message.toLowerCase();
2756
+ return msg.includes("jwt expired") || msg.includes("invalid jwt") || msg.includes("invalid token") || msg.includes("not authenticated") || msg.includes("invalid refresh_token");
2757
+ }
2690
2758
  async function mcpWatchCommand() {
2691
- const { supabase, userId } = await getAuthenticatedClient();
2759
+ let { supabase, userId } = await getLongLivedClient();
2692
2760
  const deviceId = await resolveDeviceId(supabase, userId);
2693
2761
  const deviceName = detectDeviceName();
2694
2762
  const myPid = process.pid;
@@ -2740,6 +2808,10 @@ async function mcpWatchCommand() {
2740
2808
  const rows = buildRows(configs, httpResults, cdpStatus);
2741
2809
  const now = (/* @__PURE__ */ new Date()).toISOString();
2742
2810
  const { error: deleteError } = await supabase.from("mcp_server_status").delete().eq("device_id", deviceId);
2811
+ if (deleteError && isJwtError(deleteError)) {
2812
+ await handleJwtExpiry();
2813
+ return;
2814
+ }
2743
2815
  if (deleteError) {
2744
2816
  console.error(chalk20.red(` [debug] Failed to delete old status: ${deleteError.message}`));
2745
2817
  }
@@ -2749,13 +2821,69 @@ async function mcpWatchCommand() {
2749
2821
  ...row,
2750
2822
  checked_at: now
2751
2823
  })));
2824
+ if (insertError && isJwtError(insertError)) {
2825
+ await handleJwtExpiry();
2826
+ return;
2827
+ }
2752
2828
  if (insertError) {
2753
2829
  console.error(chalk20.red(` [debug] Failed to write MCP status: ${insertError.message}`));
2754
2830
  }
2755
2831
  }
2756
- await supabase.from("mcp_watchers").update({ last_heartbeat: now }).eq("device_id", deviceId).eq("pid", myPid);
2832
+ const { error: heartbeatError } = await supabase.from("mcp_watchers").update({ last_heartbeat: now }).eq("device_id", deviceId).eq("pid", myPid);
2833
+ if (heartbeatError && isJwtError(heartbeatError)) {
2834
+ await handleJwtExpiry();
2835
+ return;
2836
+ }
2757
2837
  printTable(rows, deviceName, myPid, cdpStatus);
2758
2838
  }
2839
+ let jwtRefreshAttempted = false;
2840
+ async function handleJwtExpiry() {
2841
+ if (jwtRefreshAttempted) {
2842
+ printSessionExpired();
2843
+ await forceShutdown();
2844
+ return;
2845
+ }
2846
+ jwtRefreshAttempted = true;
2847
+ console.log(chalk20.yellow("\n Session token expired \u2014 attempting to refresh..."));
2848
+ const refreshed = await refreshSession();
2849
+ if (refreshed) {
2850
+ supabase = refreshed.supabase;
2851
+ console.log(chalk20.green(" Session refreshed successfully. Resuming monitoring.\n"));
2852
+ jwtRefreshAttempted = false;
2853
+ await supabase.from("mcp_watchers").upsert({
2854
+ device_id: deviceId,
2855
+ pid: myPid,
2856
+ tty: myTty,
2857
+ cli_version: CURRENT_VERSION,
2858
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
2859
+ last_heartbeat: (/* @__PURE__ */ new Date()).toISOString()
2860
+ }, { onConflict: "device_id,pid" });
2861
+ } else {
2862
+ printSessionExpired();
2863
+ await forceShutdown();
2864
+ }
2865
+ }
2866
+ function printSessionExpired() {
2867
+ process.stdout.write("\x1B[3J\x1B[2J\x1B[H");
2868
+ console.log("");
2869
+ console.log(chalk20.bgRed.white.bold(" \u2717 SESSION EXPIRED \u2014 this watcher is no longer sending data to the dashboard \u2717 "));
2870
+ console.log("");
2871
+ console.log(chalk20.yellow(" Your authentication token has expired and could not be refreshed."));
2872
+ console.log(chalk20.yellow(' The dashboard will show "No watchers" because this process can no longer update it.'));
2873
+ console.log("");
2874
+ console.log(chalk20.white(" To fix this:"));
2875
+ console.log(chalk20.cyan(" 1. Run: md4ai login"));
2876
+ console.log(chalk20.cyan(" 2. Then restart the watcher: md4ai mcp-watch"));
2877
+ console.log("");
2878
+ }
2879
+ let interval;
2880
+ let envInterval;
2881
+ async function forceShutdown() {
2882
+ clearInterval(interval);
2883
+ clearInterval(envInterval);
2884
+ process.stdout.write(`\x1B]0;MCP mon (EXPIRED)\x07`);
2885
+ process.exit(1);
2886
+ }
2759
2887
  await cycle();
2760
2888
  let lastEnvHash = "";
2761
2889
  async function envCycle() {
@@ -2775,7 +2903,7 @@ async function mcpWatchCommand() {
2775
2903
  await supabase.from("claude_folders").update({ env_manifest_json: envManifest }).eq("id", state.lastFolderId);
2776
2904
  }
2777
2905
  await envCycle();
2778
- const envInterval = setInterval(envCycle, ENV_POLL_INTERVAL_MS);
2906
+ envInterval = setInterval(envCycle, ENV_POLL_INTERVAL_MS);
2779
2907
  async function rescanCycle() {
2780
2908
  await checkPendingRescans(supabase, deviceId, deviceName);
2781
2909
  }
@@ -2783,7 +2911,7 @@ async function mcpWatchCommand() {
2783
2911
  await cycle();
2784
2912
  await rescanCycle();
2785
2913
  }
2786
- const interval = setInterval(fullCycle, POLL_INTERVAL_MS);
2914
+ interval = setInterval(fullCycle, POLL_INTERVAL_MS);
2787
2915
  const shutdown = async () => {
2788
2916
  clearInterval(interval);
2789
2917
  clearInterval(envInterval);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md4ai",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,15 +33,15 @@
33
33
  "node": ">=22"
34
34
  },
35
35
  "dependencies": {
36
- "@inquirer/prompts": "^7.0.0",
36
+ "@inquirer/prompts": "^8.3.0",
37
37
  "@supabase/supabase-js": "^2.98.0",
38
- "chalk": "^5.4.0",
39
- "commander": "^14.0.0",
38
+ "chalk": "^5.6.2",
39
+ "commander": "^14.0.3",
40
40
  "jszip": "^3.10.1"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@md4ai/shared": "workspace:*",
44
- "@types/node": "^22.0.0",
44
+ "@types/node": "^22.15.31",
45
45
  "esbuild": "^0.27.3",
46
46
  "typescript": "^5.7.0"
47
47
  }