md4ai 0.11.0 → 0.13.0

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 (2) hide show
  1. package/dist/index.bundled.js +360 -104
  2. package/package.json +1 -1
@@ -73,7 +73,7 @@ var CURRENT_VERSION;
73
73
  var init_check_update = __esm({
74
74
  "dist/check-update.js"() {
75
75
  "use strict";
76
- CURRENT_VERSION = true ? "0.11.0" : "0.0.0-dev";
76
+ CURRENT_VERSION = true ? "0.13.0" : "0.0.0-dev";
77
77
  }
78
78
  });
79
79
 
@@ -1656,21 +1656,40 @@ var init_marketplace_scanner = __esm({
1656
1656
 
1657
1657
  // dist/doppler/auth.js
1658
1658
  async function resolveDopplerToken() {
1659
+ const { join: join17 } = await import("node:path");
1660
+ const { homedir: homedir10 } = await import("node:os");
1661
+ const credPath = join17(homedir10(), ".md4ai", "credentials.json");
1659
1662
  const creds = await loadCredentials();
1660
1663
  if (creds?.dopplerToken) {
1661
- const { join: join17 } = await import("node:path");
1662
- const { homedir: homedir10 } = await import("node:os");
1663
- return {
1664
- token: creds.dopplerToken,
1665
- sourcePath: join17(homedir10(), ".md4ai", "credentials.json")
1666
- };
1664
+ return { token: creds.dopplerToken, sourcePath: credPath };
1665
+ }
1666
+ if (!creds?.accessToken || !creds?.userId)
1667
+ return null;
1668
+ try {
1669
+ let supabase = createSupabaseClient(getAnonKey(), creds.accessToken);
1670
+ let userId = creds.userId;
1671
+ if (Date.now() > creds.expiresAt) {
1672
+ const refreshed = await refreshSession();
1673
+ if (!refreshed)
1674
+ return null;
1675
+ supabase = refreshed.supabase;
1676
+ userId = refreshed.userId;
1677
+ }
1678
+ const { data } = await supabase.from("user_secrets").select("doppler_token").eq("user_id", userId).maybeSingle();
1679
+ if (!data?.doppler_token)
1680
+ return null;
1681
+ await mergeCredentials({ dopplerToken: data.doppler_token });
1682
+ return { token: data.doppler_token, sourcePath: "Supabase (cached locally)" };
1683
+ } catch {
1684
+ return null;
1667
1685
  }
1668
- return null;
1669
1686
  }
1670
1687
  var init_auth3 = __esm({
1671
1688
  "dist/doppler/auth.js"() {
1672
1689
  "use strict";
1673
1690
  init_config();
1691
+ init_auth();
1692
+ init_dist();
1674
1693
  }
1675
1694
  });
1676
1695
 
@@ -1713,6 +1732,10 @@ async function validateDopplerToken(token) {
1713
1732
  throw err;
1714
1733
  }
1715
1734
  }
1735
+ async function fetchDopplerProjects(token) {
1736
+ const data = await dopplerFetch("https://api.doppler.com/v3/projects", token);
1737
+ return data.projects.map((p) => ({ slug: p.slug, name: p.name }));
1738
+ }
1716
1739
  var DopplerApiError, MAX_RETRIES;
1717
1740
  var init_api = __esm({
1718
1741
  "dist/doppler/api.js"() {
@@ -1733,9 +1756,10 @@ var init_api = __esm({
1733
1756
 
1734
1757
  // dist/scanner/doppler-scanner.js
1735
1758
  import { readFile as readFile9 } from "node:fs/promises";
1736
- import { join as join11 } from "node:path";
1759
+ import { join as join11, basename } from "node:path";
1737
1760
  import { existsSync as existsSync6 } from "node:fs";
1738
1761
  import chalk10 from "chalk";
1762
+ import { select as select3 } from "@inquirer/prompts";
1739
1763
  async function parseDopplerYaml(projectRoot) {
1740
1764
  const yamlPath = join11(projectRoot, "doppler.yaml");
1741
1765
  if (!existsSync6(yamlPath))
@@ -1751,6 +1775,7 @@ async function parseDopplerYaml(projectRoot) {
1751
1775
  async function scanDoppler(projectRoot, folderId) {
1752
1776
  const tokenResult = await resolveDopplerToken();
1753
1777
  if (!tokenResult) {
1778
+ console.log(chalk10.dim(' Doppler: no token configured. Run "md4ai doppler connect" to set up.'));
1754
1779
  return null;
1755
1780
  }
1756
1781
  let projectSlug = await parseDopplerYaml(projectRoot);
@@ -1762,7 +1787,40 @@ async function scanDoppler(projectRoot, folderId) {
1762
1787
  matchedVia = "manual";
1763
1788
  }
1764
1789
  if (!projectSlug) {
1765
- return null;
1790
+ if (!folderId || !process.stdin.isTTY) {
1791
+ console.log(chalk10.dim(' Doppler: token configured but no project linked. Run "md4ai doppler set-project <slug>" to link one.'));
1792
+ return null;
1793
+ }
1794
+ const projects = await fetchDopplerProjects(tokenResult.token);
1795
+ if (projects.length === 0) {
1796
+ console.log(chalk10.yellow(" Doppler: no projects accessible with this token."));
1797
+ return null;
1798
+ }
1799
+ const projectName = basename(projectRoot);
1800
+ if (projects.length === 1) {
1801
+ projectSlug = projects[0].slug;
1802
+ console.log(chalk10.green(` Doppler: using "${projectSlug}" (only accessible project).`));
1803
+ } else {
1804
+ const autoMatch = projects.find((p) => p.slug.toLowerCase() === projectName.toLowerCase());
1805
+ if (autoMatch) {
1806
+ projectSlug = autoMatch.slug;
1807
+ console.log(chalk10.green(` Doppler: auto-matched project "${projectSlug}" from project name.`));
1808
+ } else {
1809
+ projectSlug = await select3({
1810
+ message: `Which Doppler project holds secrets for "${projectName}"?`,
1811
+ choices: projects.map((p) => ({
1812
+ name: `${p.slug}${p.name !== p.slug ? ` (${p.name})` : ""}`,
1813
+ value: p.slug
1814
+ }))
1815
+ });
1816
+ }
1817
+ }
1818
+ const state = await loadState();
1819
+ const dopplerProjects = state.dopplerProjects ?? {};
1820
+ dopplerProjects[folderId] = projectSlug;
1821
+ await saveState({ dopplerProjects });
1822
+ matchedVia = "manual";
1823
+ console.log(chalk10.dim(` Doppler: saved "${projectSlug}" for this project. To change: md4ai doppler set-project <slug>`));
1766
1824
  }
1767
1825
  console.log(chalk10.dim(` Doppler: checking project "${projectSlug}" (via ${matchedVia})...`));
1768
1826
  try {
@@ -2136,7 +2194,7 @@ var init_push_toolings = __esm({
2136
2194
 
2137
2195
  // dist/commands/push-health-results.js
2138
2196
  import chalk11 from "chalk";
2139
- async function pushHealthResults(supabase, folderId, manifest) {
2197
+ async function pushHealthResults(supabase, folderId, manifest, deviceId) {
2140
2198
  const { data: checks } = await supabase.from("env_health_checks").select("id, check_type, check_config").eq("folder_id", folderId).eq("enabled", true);
2141
2199
  if (!checks?.length)
2142
2200
  return;
@@ -2154,7 +2212,8 @@ async function pushHealthResults(supabase, folderId, manifest) {
2154
2212
  check_id: chk.id,
2155
2213
  status: localStatus === "present" ? "pass" : "fail",
2156
2214
  message: localStatus === "present" ? "Set" : "Missing",
2157
- ran_at: ranAt
2215
+ ran_at: ranAt,
2216
+ device_id: deviceId ?? null
2158
2217
  });
2159
2218
  }
2160
2219
  } else if (chk.check_type === "file_exists") {
@@ -2162,7 +2221,8 @@ async function pushHealthResults(supabase, folderId, manifest) {
2162
2221
  check_id: chk.id,
2163
2222
  status: "pass",
2164
2223
  message: "Found",
2165
- ran_at: ranAt
2224
+ ran_at: ranAt,
2225
+ device_id: deviceId ?? null
2166
2226
  });
2167
2227
  } else if (chk.check_type === "gh_secret") {
2168
2228
  const variable = config2.secret;
@@ -2172,7 +2232,8 @@ async function pushHealthResults(supabase, folderId, manifest) {
2172
2232
  check_id: chk.id,
2173
2233
  status: ghStatus === "present" ? "pass" : ghStatus === "missing" ? "fail" : "warn",
2174
2234
  message: ghStatus === "present" ? "Exists" : ghStatus === "missing" ? "Missing" : "Unknown",
2175
- ran_at: ranAt
2235
+ ran_at: ranAt,
2236
+ device_id: deviceId ?? null
2176
2237
  });
2177
2238
  } else if (chk.check_type === "vercel_env") {
2178
2239
  const variable = config2.variable;
@@ -2187,14 +2248,16 @@ async function pushHealthResults(supabase, folderId, manifest) {
2187
2248
  check_id: chk.id,
2188
2249
  status: hasTarget ? "pass" : "warn",
2189
2250
  message: hasTarget ? `Present (${target})` : `Present but not in ${target}`,
2190
- ran_at: ranAt
2251
+ ran_at: ranAt,
2252
+ device_id: deviceId ?? null
2191
2253
  });
2192
2254
  } else {
2193
2255
  results.push({
2194
2256
  check_id: chk.id,
2195
2257
  status: "fail",
2196
2258
  message: `Not found in ${projectName}`,
2197
- ran_at: ranAt
2259
+ ran_at: ranAt,
2260
+ device_id: deviceId ?? null
2198
2261
  });
2199
2262
  }
2200
2263
  }
@@ -2274,11 +2337,11 @@ var map_exports = {};
2274
2337
  __export(map_exports, {
2275
2338
  mapCommand: () => mapCommand
2276
2339
  });
2277
- import { resolve as resolve4, basename } from "node:path";
2340
+ import { resolve as resolve4, basename as basename2 } from "node:path";
2278
2341
  import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
2279
2342
  import { existsSync as existsSync8 } from "node:fs";
2280
2343
  import chalk12 from "chalk";
2281
- import { select as select3, input as input5, confirm as confirm2 } from "@inquirer/prompts";
2344
+ import { select as select4, input as input5, confirm as confirm2 } from "@inquirer/prompts";
2282
2345
  async function mapCommand(path, options) {
2283
2346
  await checkForUpdate();
2284
2347
  const projectRoot = resolve4(path ?? process.cwd());
@@ -2288,7 +2351,16 @@ async function mapCommand(path, options) {
2288
2351
  }
2289
2352
  console.log(chalk12.blue(`Scanning: ${projectRoot}
2290
2353
  `));
2291
- const result = await scanProject(projectRoot);
2354
+ let earlyFolderId;
2355
+ if (!options.offline) {
2356
+ try {
2357
+ const { supabase: earlyDb } = await getAuthenticatedClient();
2358
+ const { data: dp } = await earlyDb.from("device_paths").select("folder_id").eq("path", projectRoot).maybeSingle();
2359
+ earlyFolderId = dp?.folder_id ?? void 0;
2360
+ } catch {
2361
+ }
2362
+ }
2363
+ const result = await scanProject(projectRoot, earlyFolderId);
2292
2364
  console.log(` Files found: ${result.graph.nodes.length}`);
2293
2365
  console.log(` References: ${result.graph.edges.length}`);
2294
2366
  console.log(` Broken refs: ${result.brokenRefs.length}`);
@@ -2297,7 +2369,7 @@ async function mapCommand(path, options) {
2297
2369
  console.log(` Skills: ${result.skills.length}`);
2298
2370
  console.log(` Toolings: ${result.toolings.length}`);
2299
2371
  console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
2300
- console.log(` Doppler: ${result.doppler ? `${result.doppler.configs.length} config(s)` : "not configured"}`);
2372
+ console.log(` Doppler: ${result.doppler ? `${result.doppler.configs.length} config(s) \u2014 ${result.doppler.project}` : "see above"}`);
2301
2373
  console.log(` Plugins: ${result.marketplacePlugins.length} (${result.marketplacePlugins.reduce((n, p) => n + p.skills.length, 0)} skills)`);
2302
2374
  console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
2303
2375
  if (result.brokenRefs.length > 0) {
@@ -2321,10 +2393,12 @@ async function mapCommand(path, options) {
2321
2393
  Local preview: ${htmlPath}`));
2322
2394
  if (!options.offline) {
2323
2395
  try {
2324
- const { supabase } = await getAuthenticatedClient();
2396
+ const { supabase, userId } = await getAuthenticatedClient();
2325
2397
  const { data: devicePaths } = await supabase.from("device_paths").select("folder_id, device_name").eq("path", projectRoot);
2326
2398
  if (devicePaths?.length) {
2327
2399
  const { folder_id, device_name } = devicePaths[0];
2400
+ const { data: deviceRow } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", device_name).single();
2401
+ const deviceId = deviceRow?.id;
2328
2402
  const { data: proposedFiles } = await supabase.from("folder_files").select("id, file_path, proposed_at").eq("folder_id", folder_id).eq("proposed_for_deletion", true);
2329
2403
  if (proposedFiles?.length && process.stdin.isTTY) {
2330
2404
  const { checkbox } = await import("@inquirer/prompts");
@@ -2388,39 +2462,60 @@ ${proposedFiles.length} file(s) proposed for deletion:
2388
2462
  if (result.doppler) {
2389
2463
  updatePayload.doppler_json = result.doppler;
2390
2464
  }
2391
- const { error } = await supabase.from("claude_folders").update(updatePayload).eq("id", folder_id);
2392
- if (error) {
2393
- console.error(chalk12.yellow(`Sync warning: ${error.message}`));
2394
- } else {
2395
- await pushToolings(supabase, folder_id, result.toolings);
2396
- const manifestForHealth = result.envManifest ?? storedManifest;
2397
- if (manifestForHealth) {
2398
- await pushHealthResults(supabase, folder_id, manifestForHealth);
2465
+ try {
2466
+ const { error } = await supabase.from("claude_folders").update(updatePayload).eq("id", folder_id);
2467
+ if (error) {
2468
+ console.log(chalk12.dim(`Debug: claude_folders update skipped (${error.message})`));
2399
2469
  }
2400
- await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
2401
- await saveState({
2402
- lastFolderId: folder_id,
2403
- lastDeviceName: device_name,
2404
- lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
2405
- });
2406
- console.log(chalk12.green("Synced to Supabase."));
2407
- console.log(chalk12.cyan(`
2470
+ } catch (err) {
2471
+ console.log(chalk12.dim(`Debug: claude_folders update skipped (${err instanceof Error ? err.message : String(err)})`));
2472
+ }
2473
+ if (deviceId) {
2474
+ await supabase.from("device_scans").upsert({
2475
+ folder_id,
2476
+ device_id: deviceId,
2477
+ user_id: userId,
2478
+ graph_json: result.graph,
2479
+ orphans_json: result.orphans,
2480
+ skills_table_json: result.skills,
2481
+ stale_files_json: result.staleFiles,
2482
+ broken_refs_json: result.brokenRefs,
2483
+ env_manifest_json: result.envManifest,
2484
+ doppler_json: result.doppler,
2485
+ marketplace_plugins_json: result.marketplacePlugins,
2486
+ data_hash: result.dataHash,
2487
+ scanned_at: result.scannedAt
2488
+ }, { onConflict: "folder_id,device_id" });
2489
+ }
2490
+ await pushToolings(supabase, folder_id, result.toolings);
2491
+ const manifestForHealth = result.envManifest ?? storedManifest;
2492
+ if (manifestForHealth) {
2493
+ await pushHealthResults(supabase, folder_id, manifestForHealth, deviceId);
2494
+ }
2495
+ await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
2496
+ await saveState({
2497
+ lastFolderId: folder_id,
2498
+ lastDeviceName: device_name,
2499
+ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString()
2500
+ });
2501
+ console.log(chalk12.green("Synced to Supabase."));
2502
+ console.log(chalk12.cyan(`
2408
2503
  https://www.md4ai.com/project/${folder_id}
2409
2504
  `));
2410
- const graphPaths = result.graph.nodes.map((n) => n.filePath);
2411
- const configFiles = await readClaudeConfigFiles(projectRoot, graphPaths);
2412
- if (configFiles.length > 0) {
2413
- for (const file of configFiles) {
2414
- await supabase.from("folder_files").upsert({
2415
- folder_id,
2416
- file_path: file.filePath,
2417
- content: file.content,
2418
- size_bytes: file.sizeBytes,
2419
- last_modified: file.lastModified
2420
- }, { onConflict: "folder_id,file_path" });
2421
- }
2422
- console.log(chalk12.green(` Uploaded ${configFiles.length} config file(s).`));
2505
+ const graphPaths = result.graph.nodes.map((n) => n.filePath);
2506
+ const configFiles = await readClaudeConfigFiles(projectRoot, graphPaths);
2507
+ if (configFiles.length > 0) {
2508
+ for (const file of configFiles) {
2509
+ await supabase.from("folder_files").upsert({
2510
+ folder_id,
2511
+ file_path: file.filePath,
2512
+ content: file.content,
2513
+ size_bytes: file.sizeBytes,
2514
+ last_modified: file.lastModified,
2515
+ device_id: deviceId
2516
+ }, { onConflict: "folder_id,file_path,device_id" });
2423
2517
  }
2518
+ console.log(chalk12.green(` Uploaded ${configFiles.length} config file(s).`));
2424
2519
  }
2425
2520
  } else {
2426
2521
  console.log(chalk12.yellow("\nThis folder is not linked to a project on your dashboard."));
@@ -2432,13 +2527,13 @@ ${proposedFiles.length} file(s) proposed for deletion:
2432
2527
  if (!shouldLink) {
2433
2528
  console.log(chalk12.dim("Skipped \u2014 local preview still generated."));
2434
2529
  } else {
2435
- const { supabase: sb, userId } = await getAuthenticatedClient();
2530
+ const { supabase: sb, userId: userId2 } = await getAuthenticatedClient();
2436
2531
  const { data: folders } = await sb.from("claude_folders").select("id, name").order("name");
2437
2532
  const choices = [
2438
2533
  { name: "+ Create a new project", value: "__new__" },
2439
2534
  ...(folders ?? []).map((f) => ({ name: f.name, value: f.id }))
2440
2535
  ];
2441
- const chosen = await select3({
2536
+ const chosen = await select4({
2442
2537
  message: "Link to which project?",
2443
2538
  choices
2444
2539
  });
@@ -2446,9 +2541,9 @@ ${proposedFiles.length} file(s) proposed for deletion:
2446
2541
  if (chosen === "__new__") {
2447
2542
  const projectName = await input5({
2448
2543
  message: "Project name:",
2449
- default: basename(projectRoot)
2544
+ default: basename2(projectRoot)
2450
2545
  });
2451
- const { data: newFolder, error: createErr } = await sb.from("claude_folders").insert({ user_id: userId, name: projectName }).select("id").single();
2546
+ const { data: newFolder, error: createErr } = await sb.from("claude_folders").insert({ user_id: userId2, name: projectName }).select("id").single();
2452
2547
  if (createErr || !newFolder) {
2453
2548
  console.error(chalk12.red(`Failed to create project: ${createErr?.message}`));
2454
2549
  return;
@@ -2461,33 +2556,59 @@ ${proposedFiles.length} file(s) proposed for deletion:
2461
2556
  const deviceName = detectDeviceName();
2462
2557
  const osType = detectOs2();
2463
2558
  await sb.from("devices").upsert({
2464
- user_id: userId,
2559
+ user_id: userId2,
2465
2560
  device_name: deviceName,
2466
2561
  os_type: osType
2467
2562
  }, { onConflict: "user_id,device_name" });
2563
+ const { data: inlineDeviceRow } = await sb.from("devices").select("id").eq("user_id", userId2).eq("device_name", deviceName).single();
2564
+ const inlineDeviceId = inlineDeviceRow?.id;
2468
2565
  await sb.from("device_paths").upsert({
2469
- user_id: userId,
2566
+ user_id: userId2,
2470
2567
  folder_id: folderId,
2471
2568
  device_name: deviceName,
2472
2569
  os_type: osType,
2473
2570
  path: projectRoot,
2474
2571
  last_synced: (/* @__PURE__ */ new Date()).toISOString()
2475
2572
  }, { onConflict: "folder_id,device_name" });
2476
- await sb.from("claude_folders").update({
2477
- graph_json: result.graph,
2478
- orphans_json: result.orphans,
2479
- broken_refs_json: result.brokenRefs,
2480
- skills_table_json: result.skills,
2481
- stale_files_json: result.staleFiles,
2482
- env_manifest_json: result.envManifest,
2483
- doppler_json: result.doppler,
2484
- marketplace_plugins_json: result.marketplacePlugins,
2485
- last_scanned: result.scannedAt,
2486
- data_hash: result.dataHash
2487
- }).eq("id", folderId);
2573
+ try {
2574
+ const { error: inlineFolderErr } = await sb.from("claude_folders").update({
2575
+ graph_json: result.graph,
2576
+ orphans_json: result.orphans,
2577
+ broken_refs_json: result.brokenRefs,
2578
+ skills_table_json: result.skills,
2579
+ stale_files_json: result.staleFiles,
2580
+ env_manifest_json: result.envManifest,
2581
+ doppler_json: result.doppler,
2582
+ marketplace_plugins_json: result.marketplacePlugins,
2583
+ last_scanned: result.scannedAt,
2584
+ data_hash: result.dataHash
2585
+ }).eq("id", folderId);
2586
+ if (inlineFolderErr) {
2587
+ console.log(chalk12.dim(`Debug: claude_folders update skipped (${inlineFolderErr.message})`));
2588
+ }
2589
+ } catch (err) {
2590
+ console.log(chalk12.dim(`Debug: claude_folders update skipped (${err instanceof Error ? err.message : String(err)})`));
2591
+ }
2592
+ if (inlineDeviceId) {
2593
+ await sb.from("device_scans").upsert({
2594
+ folder_id: folderId,
2595
+ device_id: inlineDeviceId,
2596
+ user_id: userId2,
2597
+ graph_json: result.graph,
2598
+ orphans_json: result.orphans,
2599
+ skills_table_json: result.skills,
2600
+ stale_files_json: result.staleFiles,
2601
+ broken_refs_json: result.brokenRefs,
2602
+ env_manifest_json: result.envManifest,
2603
+ doppler_json: result.doppler,
2604
+ marketplace_plugins_json: result.marketplacePlugins,
2605
+ data_hash: result.dataHash,
2606
+ scanned_at: result.scannedAt
2607
+ }, { onConflict: "folder_id,device_id" });
2608
+ }
2488
2609
  await pushToolings(sb, folderId, result.toolings);
2489
2610
  if (result.envManifest) {
2490
- await pushHealthResults(sb, folderId, result.envManifest);
2611
+ await pushHealthResults(sb, folderId, result.envManifest, inlineDeviceId);
2491
2612
  }
2492
2613
  const graphPaths2 = result.graph.nodes.map((n) => n.filePath);
2493
2614
  const configFiles = await readClaudeConfigFiles(projectRoot, graphPaths2);
@@ -2497,8 +2618,9 @@ ${proposedFiles.length} file(s) proposed for deletion:
2497
2618
  file_path: file.filePath,
2498
2619
  content: file.content,
2499
2620
  size_bytes: file.sizeBytes,
2500
- last_modified: file.lastModified
2501
- }, { onConflict: "folder_id,file_path" });
2621
+ last_modified: file.lastModified,
2622
+ device_id: inlineDeviceId
2623
+ }, { onConflict: "folder_id,file_path,device_id" });
2502
2624
  }
2503
2625
  await saveState({
2504
2626
  lastFolderId: folderId,
@@ -2543,7 +2665,7 @@ function isValidProjectPath(p) {
2543
2665
  return resolved.startsWith("/") && !resolved.includes("..") && existsSync11(resolved);
2544
2666
  }
2545
2667
  async function syncCommand(options) {
2546
- const { supabase } = await getAuthenticatedClient();
2668
+ const { supabase, userId } = await getAuthenticatedClient();
2547
2669
  if (options.all) {
2548
2670
  const { data: devices, error } = await supabase.from("device_paths").select("folder_id, device_name, path");
2549
2671
  if (error || !devices?.length) {
@@ -2562,16 +2684,40 @@ async function syncCommand(options) {
2562
2684
  }
2563
2685
  try {
2564
2686
  const result = await scanProject(device.path);
2565
- await supabase.from("claude_folders").update({
2566
- graph_json: result.graph,
2567
- orphans_json: result.orphans,
2568
- broken_refs_json: result.brokenRefs,
2569
- skills_table_json: result.skills,
2570
- stale_files_json: result.staleFiles,
2571
- env_manifest_json: result.envManifest,
2572
- last_scanned: result.scannedAt,
2573
- data_hash: result.dataHash
2574
- }).eq("id", device.folder_id);
2687
+ const { data: allDeviceRow } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", device.device_name).single();
2688
+ const allDeviceId = allDeviceRow?.id;
2689
+ try {
2690
+ const { error: allFolderErr } = await supabase.from("claude_folders").update({
2691
+ graph_json: result.graph,
2692
+ orphans_json: result.orphans,
2693
+ broken_refs_json: result.brokenRefs,
2694
+ skills_table_json: result.skills,
2695
+ stale_files_json: result.staleFiles,
2696
+ env_manifest_json: result.envManifest,
2697
+ last_scanned: result.scannedAt,
2698
+ data_hash: result.dataHash
2699
+ }).eq("id", device.folder_id);
2700
+ if (allFolderErr) {
2701
+ console.log(chalk15.dim(` Debug: claude_folders update skipped (${allFolderErr.message})`));
2702
+ }
2703
+ } catch (folderErr) {
2704
+ console.log(chalk15.dim(` Debug: claude_folders update skipped (${folderErr instanceof Error ? folderErr.message : String(folderErr)})`));
2705
+ }
2706
+ if (allDeviceId) {
2707
+ await supabase.from("device_scans").upsert({
2708
+ folder_id: device.folder_id,
2709
+ device_id: allDeviceId,
2710
+ user_id: userId,
2711
+ graph_json: result.graph,
2712
+ orphans_json: result.orphans,
2713
+ skills_table_json: result.skills,
2714
+ stale_files_json: result.staleFiles,
2715
+ broken_refs_json: result.brokenRefs,
2716
+ env_manifest_json: result.envManifest,
2717
+ data_hash: result.dataHash,
2718
+ scanned_at: result.scannedAt
2719
+ }, { onConflict: "folder_id,device_id" });
2720
+ }
2575
2721
  await pushToolings(supabase, device.folder_id, result.toolings);
2576
2722
  await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
2577
2723
  console.log(chalk15.green(` Done: ${device.device_name}`));
@@ -2602,15 +2748,38 @@ async function syncCommand(options) {
2602
2748
  console.log(chalk15.yellow(` ${proposedSingle.length} file(s) proposed for deletion \u2014 run \`md4ai scan\` to review.`));
2603
2749
  }
2604
2750
  const result = await scanProject(device.path);
2605
- await supabase.from("claude_folders").update({
2606
- graph_json: result.graph,
2607
- orphans_json: result.orphans,
2608
- broken_refs_json: result.brokenRefs,
2609
- skills_table_json: result.skills,
2610
- stale_files_json: result.staleFiles,
2611
- last_scanned: result.scannedAt,
2612
- data_hash: result.dataHash
2613
- }).eq("id", device.folder_id);
2751
+ const { data: singleDeviceRow } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", device.device_name).single();
2752
+ const singleDeviceId = singleDeviceRow?.id;
2753
+ try {
2754
+ const { error: singleFolderErr } = await supabase.from("claude_folders").update({
2755
+ graph_json: result.graph,
2756
+ orphans_json: result.orphans,
2757
+ broken_refs_json: result.brokenRefs,
2758
+ skills_table_json: result.skills,
2759
+ stale_files_json: result.staleFiles,
2760
+ last_scanned: result.scannedAt,
2761
+ data_hash: result.dataHash
2762
+ }).eq("id", device.folder_id);
2763
+ if (singleFolderErr) {
2764
+ console.log(chalk15.dim(`Debug: claude_folders update skipped (${singleFolderErr.message})`));
2765
+ }
2766
+ } catch (err) {
2767
+ console.log(chalk15.dim(`Debug: claude_folders update skipped (${err instanceof Error ? err.message : String(err)})`));
2768
+ }
2769
+ if (singleDeviceId) {
2770
+ await supabase.from("device_scans").upsert({
2771
+ folder_id: device.folder_id,
2772
+ device_id: singleDeviceId,
2773
+ user_id: userId,
2774
+ graph_json: result.graph,
2775
+ orphans_json: result.orphans,
2776
+ skills_table_json: result.skills,
2777
+ stale_files_json: result.staleFiles,
2778
+ broken_refs_json: result.brokenRefs,
2779
+ data_hash: result.dataHash,
2780
+ scanned_at: result.scannedAt
2781
+ }, { onConflict: "folder_id,device_id" });
2782
+ }
2614
2783
  await pushToolings(supabase, device.folder_id, result.toolings);
2615
2784
  await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
2616
2785
  await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
@@ -3757,6 +3926,8 @@ Linking "${folder.name}" to this device...
3757
3926
  device_name: deviceName,
3758
3927
  os_type: osType
3759
3928
  }, { onConflict: "user_id,device_name" });
3929
+ const { data: deviceRow } = await supabase.from("devices").select("id").eq("user_id", userId).eq("device_name", deviceName).single();
3930
+ const deviceId = deviceRow.id;
3760
3931
  const { data: existing } = await supabase.from("device_paths").select("id").eq("folder_id", folder.id).eq("device_name", deviceName).maybeSingle();
3761
3932
  if (existing) {
3762
3933
  await supabase.from("device_paths").update({ path: cwd, os_type: osType, last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id);
@@ -3782,23 +3953,42 @@ Linking "${folder.name}" to this device...
3782
3953
  console.log(` Skills: ${result.skills.length}`);
3783
3954
  console.log(` Toolings: ${result.toolings.length}`);
3784
3955
  console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
3785
- console.log(` Doppler: ${result.doppler ? `${result.doppler.configs.length} config(s)` : "not configured"}`);
3956
+ console.log(` Doppler: ${result.doppler ? `${result.doppler.configs.length} config(s) \u2014 ${result.doppler.project}` : "see above"}`);
3786
3957
  console.log(` Plugins: ${result.marketplacePlugins.length} (${result.marketplacePlugins.reduce((n, p) => n + p.skills.length, 0)} skills)`);
3787
- const { error: scanErr } = await supabase.from("claude_folders").update({
3958
+ try {
3959
+ const { error: scanErr } = await supabase.from("claude_folders").update({
3960
+ graph_json: result.graph,
3961
+ orphans_json: result.orphans,
3962
+ broken_refs_json: result.brokenRefs,
3963
+ skills_table_json: result.skills,
3964
+ stale_files_json: result.staleFiles,
3965
+ env_manifest_json: result.envManifest,
3966
+ doppler_json: result.doppler,
3967
+ marketplace_plugins_json: result.marketplacePlugins,
3968
+ last_scanned: result.scannedAt,
3969
+ data_hash: result.dataHash
3970
+ }).eq("id", folder.id);
3971
+ if (scanErr) {
3972
+ console.log(chalk16.dim(`Debug: claude_folders update skipped (${scanErr.message})`));
3973
+ }
3974
+ } catch (err) {
3975
+ console.log(chalk16.dim(`Debug: claude_folders update skipped (${err instanceof Error ? err.message : String(err)})`));
3976
+ }
3977
+ await supabase.from("device_scans").upsert({
3978
+ folder_id: folder.id,
3979
+ device_id: deviceId,
3980
+ user_id: userId,
3788
3981
  graph_json: result.graph,
3789
3982
  orphans_json: result.orphans,
3790
- broken_refs_json: result.brokenRefs,
3791
3983
  skills_table_json: result.skills,
3792
3984
  stale_files_json: result.staleFiles,
3985
+ broken_refs_json: result.brokenRefs,
3793
3986
  env_manifest_json: result.envManifest,
3794
3987
  doppler_json: result.doppler,
3795
3988
  marketplace_plugins_json: result.marketplacePlugins,
3796
- last_scanned: result.scannedAt,
3797
- data_hash: result.dataHash
3798
- }).eq("id", folder.id);
3799
- if (scanErr) {
3800
- console.error(chalk16.yellow(`Scan upload warning: ${scanErr.message}`));
3801
- }
3989
+ data_hash: result.dataHash,
3990
+ scanned_at: result.scannedAt
3991
+ }, { onConflict: "folder_id,device_id" });
3802
3992
  await pushToolings(supabase, folder.id, result.toolings);
3803
3993
  const graphPaths = result.graph.nodes.map((n) => n.filePath);
3804
3994
  const configFiles = await readClaudeConfigFiles(cwd, graphPaths);
@@ -3809,8 +3999,9 @@ Linking "${folder.name}" to this device...
3809
3999
  file_path: file.filePath,
3810
4000
  content: file.content,
3811
4001
  size_bytes: file.sizeBytes,
3812
- last_modified: file.lastModified
3813
- }, { onConflict: "folder_id,file_path" });
4002
+ last_modified: file.lastModified,
4003
+ device_id: deviceId
4004
+ }, { onConflict: "folder_id,file_path,device_id" });
3814
4005
  }
3815
4006
  console.log(chalk16.green(` Uploaded ${configFiles.length} config file(s).`));
3816
4007
  }
@@ -4581,6 +4772,8 @@ async function configSetCommand(key, value) {
4581
4772
  // dist/commands/doppler.js
4582
4773
  init_config();
4583
4774
  init_api();
4775
+ init_auth();
4776
+ init_dist();
4584
4777
  import chalk26 from "chalk";
4585
4778
  import { input as input7 } from "@inquirer/prompts";
4586
4779
  async function dopplerConnectCommand() {
@@ -4599,7 +4792,51 @@ async function dopplerConnectCommand() {
4599
4792
  await mergeCredentials({ dopplerToken: token });
4600
4793
  console.log(chalk26.green(`Token validated \u2014 ${projectCount} project${projectCount !== 1 ? "s" : ""} accessible.`));
4601
4794
  console.log(chalk26.green("Saved to ~/.md4ai/credentials.json"));
4602
- console.log(chalk26.dim("Doppler will be checked automatically on your next scan."));
4795
+ const creds = await loadCredentials();
4796
+ if (!creds?.accessToken || !creds?.userId) {
4797
+ console.log("");
4798
+ console.log(chalk26.yellow("\u26A0 Not logged in to MD4AI \u2014 token saved locally only."));
4799
+ console.log(chalk26.yellow(" The web dashboard cannot show live Doppler status until the token is synced."));
4800
+ console.log(chalk26.yellow(' To fix: run "md4ai login", then "md4ai doppler connect" again.'));
4801
+ } else {
4802
+ let supabase = createSupabaseClient(getAnonKey(), creds.accessToken);
4803
+ let userId = creds.userId;
4804
+ if (Date.now() > creds.expiresAt) {
4805
+ const refreshed = await refreshSession();
4806
+ if (refreshed) {
4807
+ supabase = refreshed.supabase;
4808
+ userId = refreshed.userId;
4809
+ } else {
4810
+ console.log("");
4811
+ console.log(chalk26.yellow("\u26A0 Session expired \u2014 token saved locally only."));
4812
+ console.log(chalk26.yellow(' To sync to the web dashboard: run "md4ai login", then "md4ai doppler connect" again.'));
4813
+ }
4814
+ }
4815
+ if (supabase) {
4816
+ try {
4817
+ const { error } = await supabase.from("user_secrets").upsert({ user_id: userId, doppler_token: token, updated_at: (/* @__PURE__ */ new Date()).toISOString() }, { onConflict: "user_id" });
4818
+ if (error) {
4819
+ console.log(chalk26.yellow(`\u26A0 Could not sync token to web dashboard: ${error.message}`));
4820
+ } else {
4821
+ const { data: verify } = await supabase.from("user_secrets").select("id").eq("user_id", userId).maybeSingle();
4822
+ if (verify) {
4823
+ console.log(chalk26.green("Token synced to web dashboard."));
4824
+ } else {
4825
+ console.log(chalk26.yellow("\u26A0 Token sync could not be verified \u2014 check the web dashboard."));
4826
+ }
4827
+ }
4828
+ } catch (err) {
4829
+ console.log(chalk26.yellow(`\u26A0 Could not sync token to web dashboard: ${err instanceof Error ? err.message : "unknown error"}`));
4830
+ }
4831
+ }
4832
+ }
4833
+ console.log("");
4834
+ console.log(chalk26.cyan("Next step: link a Doppler project to your Claude project."));
4835
+ console.log(chalk26.dim(" The scanner looks for a doppler.yaml in the project root, e.g.:"));
4836
+ console.log(chalk26.dim(" project: my-doppler-project"));
4837
+ console.log(chalk26.dim(" Or use a manual override:"));
4838
+ console.log(chalk26.dim(" md4ai doppler set-project <slug>"));
4839
+ console.log(chalk26.dim(' Then run "md4ai scan" to fetch secret names from Doppler.'));
4603
4840
  }
4604
4841
  async function dopplerDisconnectCommand() {
4605
4842
  const creds = await loadCredentials();
@@ -4614,7 +4851,26 @@ async function dopplerDisconnectCommand() {
4614
4851
  const credPath = join17(homedir10(), ".md4ai", "credentials.json");
4615
4852
  await writeFile6(credPath, JSON.stringify(rest, null, 2), "utf-8");
4616
4853
  await chmod2(credPath, 384);
4617
- console.log(chalk26.green("Doppler token removed."));
4854
+ try {
4855
+ const currentCreds = await loadCredentials();
4856
+ if (currentCreds?.accessToken && currentCreds?.userId) {
4857
+ let supabase = createSupabaseClient(getAnonKey(), currentCreds.accessToken);
4858
+ let userId = currentCreds.userId;
4859
+ if (Date.now() > currentCreds.expiresAt) {
4860
+ const refreshed = await refreshSession();
4861
+ if (refreshed) {
4862
+ supabase = refreshed.supabase;
4863
+ userId = refreshed.userId;
4864
+ }
4865
+ }
4866
+ await supabase.from("user_secrets").delete().eq("user_id", userId);
4867
+ console.log(chalk26.green("Doppler token removed from local and web dashboard."));
4868
+ } else {
4869
+ console.log(chalk26.green("Doppler token removed locally."));
4870
+ }
4871
+ } catch {
4872
+ console.log(chalk26.green("Doppler token removed locally."));
4873
+ }
4618
4874
  }
4619
4875
  async function dopplerSetProjectCommand(slug) {
4620
4876
  const state = await loadState();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md4ai",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
5
5
  "type": "module",
6
6
  "bin": {