@trops/dash-core 0.1.350 → 0.1.352

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.
@@ -26707,1465 +26707,1820 @@ function parsePackageId(id) {
26707
26707
  var packageId = { toPackageId: toPackageId$1, parsePackageId };
26708
26708
 
26709
26709
  /**
26710
- * registryController.js
26710
+ * registryAuthController.js
26711
26711
  *
26712
- * Manages fetching, caching, and searching the remote widget registry index.
26713
- * Runs in the Electron main process.
26712
+ * Manages authentication with the Dash registry service.
26713
+ * Uses OAuth device code flow for desktop app authentication.
26714
26714
  *
26715
- * Responsibilities:
26716
- * - Fetch and cache the remote registry-index.json with 5-min TTL
26717
- * - Search/filter across both packages and individual widgets
26718
- * - Support two-level browsing: packages (bundles) and widgets within packages
26715
+ * Flow:
26716
+ * 1. App calls initiateDeviceFlow() gets device code + verification URL
26717
+ * 2. User opens verification URL in browser, signs in, enters code
26718
+ * 3. App polls pollForToken() until authorized
26719
+ * 4. Token stored securely via electron-store (encrypted)
26719
26720
  */
26720
26721
 
26721
- const path$a = require$$1$2;
26722
- const fs$6 = require$$0$2;
26723
- const { toPackageId } = packageId;
26724
-
26725
- // Default registry API base URL
26726
- const DEFAULT_REGISTRY_API_URL = "https://main.d919rwhuzp7rj.amplifyapp.com";
26727
-
26728
- // Cache TTL: 5 minutes
26729
- const CACHE_TTL_MS = 5 * 60 * 1000;
26730
-
26731
- let cachedIndex = null;
26732
- let cacheTimestamp = 0;
26722
+ const REGISTRY_BASE_URL$1 =
26723
+ process.env.DASH_REGISTRY_API_URL ||
26724
+ "https://main.d919rwhuzp7rj.amplifyapp.com";
26733
26725
 
26734
- /**
26735
- * Get the local test registry path for dev mode
26736
- */
26737
- function getTestRegistryPath() {
26738
- return path$a.join(__dirname, "..", "registry", "test-registry-index.json");
26726
+ // Lazy-load electron-store to avoid issues when not installed
26727
+ let store$3 = null;
26728
+ function getStore$1() {
26729
+ if (!store$3) {
26730
+ const Store = require$$1$1;
26731
+ store$3 = new Store({
26732
+ name: "dash-registry-auth",
26733
+ encryptionKey: "dash-registry-v1",
26734
+ });
26735
+ }
26736
+ return store$3;
26739
26737
  }
26740
26738
 
26741
26739
  /**
26742
- * Check if running in development mode
26740
+ * Initiate the OAuth device code flow.
26741
+ * Returns the device code, user code, and verification URL.
26742
+ *
26743
+ * @returns {Promise<Object>} { deviceCode, userCode, verificationUrl, verificationUrlComplete, expiresIn, interval }
26743
26744
  */
26744
- function isDev() {
26745
- return (
26746
- process.defaultApp ||
26747
- process.env.NODE_ENV === "development" ||
26748
- process.env.NODE_ENV === "dev"
26749
- );
26745
+ async function initiateDeviceFlow$1() {
26746
+ const response = await fetch(`${REGISTRY_BASE_URL$1}/api/auth/device`, {
26747
+ method: "POST",
26748
+ headers: { "Content-Type": "application/json" },
26749
+ });
26750
+
26751
+ if (!response.ok) {
26752
+ throw new Error(`Device flow initiation failed: ${response.status}`);
26753
+ }
26754
+
26755
+ const data = await response.json();
26756
+
26757
+ return {
26758
+ deviceCode: data.device_code,
26759
+ userCode: data.user_code,
26760
+ verificationUrl: data.verification_uri,
26761
+ verificationUrlComplete: data.verification_uri_complete,
26762
+ expiresIn: data.expires_in,
26763
+ interval: data.interval,
26764
+ };
26750
26765
  }
26751
26766
 
26752
26767
  /**
26753
- * Fetch the registry index from remote URL or local file (dev mode)
26754
- * Caches the result for CACHE_TTL_MS milliseconds.
26768
+ * Poll the registry for token after user completes browser auth.
26755
26769
  *
26756
- * @param {boolean} forceRefresh - Bypass cache and fetch fresh data
26757
- * @returns {Promise<Object>} The registry index
26770
+ * @param {string} deviceCode - The device code from initiateDeviceFlow()
26771
+ * @returns {Promise<Object>} { status: 'pending' | 'authorized' | 'expired', token?, userId? }
26758
26772
  */
26759
- async function fetchRegistryIndex(forceRefresh = false) {
26760
- const now = Date.now();
26773
+ async function pollForToken$1(deviceCode) {
26774
+ const response = await fetch(
26775
+ `${REGISTRY_BASE_URL$1}/api/auth/device?device_code=${encodeURIComponent(deviceCode)}`,
26776
+ );
26761
26777
 
26762
- // Return cached data if still valid
26763
- if (!forceRefresh && cachedIndex && now - cacheTimestamp < CACHE_TTL_MS) {
26764
- console.log("[RegistryController] Returning cached registry index");
26765
- return cachedIndex;
26778
+ if (response.status === 428) {
26779
+ return { status: "pending" };
26766
26780
  }
26767
26781
 
26768
- try {
26769
- let indexData;
26770
-
26771
- if (isDev()) {
26772
- // In dev mode, try local test file first
26773
- const testPath = getTestRegistryPath();
26774
- if (fs$6.existsSync(testPath)) {
26775
- console.log(
26776
- "[RegistryController] Loading test registry from:",
26777
- testPath,
26778
- );
26779
- const raw = fs$6.readFileSync(testPath, "utf8");
26780
- indexData = JSON.parse(raw);
26781
- } else {
26782
- // Fall back to API (supports DASH_REGISTRY_URL as full-URL override)
26783
- const registryUrl =
26784
- process.env.DASH_REGISTRY_URL ||
26785
- `${process.env.DASH_REGISTRY_API_URL || DEFAULT_REGISTRY_API_URL}/api/packages`;
26786
- console.log(
26787
- "[RegistryController] Fetching registry from:",
26788
- registryUrl,
26789
- );
26790
- const response = await fetch(registryUrl);
26791
- if (!response.ok) {
26792
- throw new Error(
26793
- `Failed to fetch registry: ${response.status} ${response.statusText}`,
26794
- );
26795
- }
26796
- indexData = await response.json();
26797
- }
26798
- } else {
26799
- // In production, fetch from API
26800
- const registryUrl =
26801
- process.env.DASH_REGISTRY_URL ||
26802
- `${process.env.DASH_REGISTRY_API_URL || DEFAULT_REGISTRY_API_URL}/api/packages`;
26803
- console.log("[RegistryController] Fetching registry from:", registryUrl);
26804
-
26805
- const response = await fetch(registryUrl);
26806
- if (!response.ok) {
26807
- throw new Error(
26808
- `Failed to fetch registry: ${response.status} ${response.statusText}`,
26809
- );
26810
- }
26811
- indexData = await response.json();
26782
+ if (response.status === 400) {
26783
+ const data = await response.json();
26784
+ if (data.error === "expired_token") {
26785
+ return { status: "expired" };
26812
26786
  }
26787
+ return { status: "pending" };
26788
+ }
26813
26789
 
26814
- // Normalize: ensure `version` exists on each package (API uses `latestVersion`)
26815
- if (indexData.packages) {
26816
- indexData.packages = indexData.packages.map((pkg) => ({
26817
- ...pkg,
26818
- version: pkg.version || pkg.latestVersion || "0.0.0",
26819
- }));
26820
- }
26790
+ if (response.ok) {
26791
+ const data = await response.json();
26821
26792
 
26822
- // Cache the result
26823
- cachedIndex = indexData;
26824
- cacheTimestamp = now;
26793
+ // Store the token securely
26794
+ const s = getStore$1();
26795
+ s.set("accessToken", data.access_token);
26796
+ s.set("userId", data.user_id);
26797
+ s.set("tokenType", data.token_type);
26798
+ s.set("authenticatedAt", new Date().toISOString());
26825
26799
 
26826
- console.log(
26827
- `[RegistryController] Loaded ${indexData.packages?.length || 0} packages`,
26828
- );
26829
- return indexData;
26830
- } catch (error) {
26831
- console.error("[RegistryController] Error fetching registry:", error);
26800
+ return {
26801
+ status: "authorized",
26802
+ token: data.access_token,
26803
+ userId: data.user_id,
26804
+ };
26805
+ }
26832
26806
 
26833
- // Return stale cache if available
26834
- if (cachedIndex) {
26835
- console.log(
26836
- "[RegistryController] Returning stale cache after fetch error",
26837
- );
26838
- return cachedIndex;
26839
- }
26807
+ throw new Error(`Unexpected response: ${response.status}`);
26808
+ }
26840
26809
 
26841
- throw error;
26810
+ /**
26811
+ * Get the stored auth token.
26812
+ *
26813
+ * @returns {Object|null} { token, userId, authenticatedAt } or null if not authenticated
26814
+ */
26815
+ function getStoredToken$4() {
26816
+ try {
26817
+ const s = getStore$1();
26818
+ const token = s.get("accessToken");
26819
+ if (!token) return null;
26820
+
26821
+ return {
26822
+ token,
26823
+ userId: s.get("userId"),
26824
+ authenticatedAt: s.get("authenticatedAt"),
26825
+ };
26826
+ } catch {
26827
+ return null;
26842
26828
  }
26843
26829
  }
26844
26830
 
26845
26831
  /**
26846
- * Search the registry across packages and individual widgets
26832
+ * Check if the user is authenticated with the registry.
26847
26833
  *
26848
- * @param {string} query - Search query string
26849
- * @param {Object} filters - Optional filters
26850
- * @param {string} filters.category - Filter by category
26851
- * @param {string} filters.author - Filter by author
26852
- * @param {string} filters.tag - Filter by tag
26853
- * @param {string} filters.type - Filter by package type ("widget" or "dashboard")
26854
- * @param {string[]} filters.compatibleWidgets - Only return dashboards whose required widgets are all in this list
26855
- * @param {string[]} filters.appCapabilities - Only return packages whose required API providers are all in this list
26856
- * @returns {Promise<Object>} { packages: [...], totalWidgets: number }
26834
+ * @returns {Object} { authenticated: boolean, userId?: string }
26857
26835
  */
26858
- async function searchRegistry$1(query = "", filters = {}) {
26859
- const index = await fetchRegistryIndex();
26860
- let packages = index.packages || [];
26861
-
26862
- // Apply type filter — packages without an explicit type default to "widget"
26863
- if (filters.type) {
26864
- const typeLower = filters.type.toLowerCase();
26865
- packages = packages.filter(
26866
- (pkg) => (pkg.type || "widget").toLowerCase() === typeLower,
26867
- );
26836
+ function getAuthStatus$1() {
26837
+ const stored = getStoredToken$4();
26838
+ if (!stored) {
26839
+ return { authenticated: false };
26868
26840
  }
26869
26841
 
26870
- // Apply search query
26871
- if (query) {
26872
- const q = query.toLowerCase();
26873
- packages = packages.filter((pkg) => {
26874
- // Match against package-level fields
26875
- const packageMatch =
26876
- (pkg.name || "").toLowerCase().includes(q) ||
26877
- (pkg.displayName || "").toLowerCase().includes(q) ||
26878
- (pkg.description || "").toLowerCase().includes(q) ||
26879
- (pkg.author || "").toLowerCase().includes(q) ||
26880
- (pkg.tags || []).some((t) => t.toLowerCase().includes(q));
26842
+ return {
26843
+ authenticated: true,
26844
+ userId: stored.userId,
26845
+ authenticatedAt: stored.authenticatedAt,
26846
+ };
26847
+ }
26881
26848
 
26882
- // Match against individual widgets within the package
26883
- const widgetMatch = (pkg.widgets || []).some(
26884
- (w) =>
26885
- (w.name || "").toLowerCase().includes(q) ||
26886
- (w.displayName || "").toLowerCase().includes(q) ||
26887
- (w.description || "").toLowerCase().includes(q),
26888
- );
26849
+ /**
26850
+ * Get the user's registry profile.
26851
+ *
26852
+ * @returns {Promise<Object|null>} User profile or null
26853
+ */
26854
+ async function getRegistryProfile$2() {
26855
+ const stored = getStoredToken$4();
26856
+ if (!stored) return null;
26889
26857
 
26890
- return packageMatch || widgetMatch;
26858
+ try {
26859
+ const response = await fetch(`${REGISTRY_BASE_URL$1}/api/auth/me`, {
26860
+ headers: {
26861
+ Authorization: `Bearer ${stored.token}`,
26862
+ },
26891
26863
  });
26892
- }
26893
26864
 
26894
- // Apply category filter (supports single string or comma-separated or array)
26895
- if (filters.category) {
26896
- const cats = Array.isArray(filters.category)
26897
- ? filters.category
26898
- : filters.category.split(",").map((c) => c.trim().toLowerCase());
26899
- packages = packages.filter((pkg) =>
26900
- cats.includes((pkg.category || "").toLowerCase()),
26901
- );
26902
- }
26903
-
26904
- // Apply author filter
26905
- if (filters.author) {
26906
- packages = packages.filter(
26907
- (pkg) =>
26908
- (pkg.author || "").toLowerCase() === filters.author.toLowerCase(),
26909
- );
26910
- }
26911
-
26912
- // Apply tag filter (supports single string or comma-separated or array)
26913
- if (filters.tag) {
26914
- const tags = Array.isArray(filters.tag)
26915
- ? filters.tag
26916
- : filters.tag.split(",").map((t) => t.trim().toLowerCase());
26917
- packages = packages.filter((pkg) =>
26918
- (pkg.tags || []).some((t) => tags.includes(t.toLowerCase())),
26919
- );
26920
- }
26865
+ if (response.status === 401) {
26866
+ // Token expired or invalid — clear stored credentials
26867
+ clearToken$2();
26868
+ return null;
26869
+ }
26870
+ if (!response.ok) return null;
26921
26871
 
26922
- // Apply compatibility filter — only dashboards whose required widgets
26923
- // are all present in the user's installed widget list
26924
- if (filters.compatibleWidgets && filters.compatibleWidgets.length) {
26925
- const installedSet = new Set(
26926
- filters.compatibleWidgets.map((w) => w.toLowerCase()),
26927
- );
26928
- packages = packages.filter((pkg) => {
26929
- const requiredWidgets = (pkg.widgets || []).filter(
26930
- (w) => w.required !== false,
26931
- );
26932
- return requiredWidgets.every(
26933
- (w) =>
26934
- installedSet.has((w.package || "").toLowerCase()) ||
26935
- installedSet.has((w.name || "").toLowerCase()),
26936
- );
26937
- });
26872
+ const data = await response.json();
26873
+ return data.user || null;
26874
+ } catch {
26875
+ return null;
26938
26876
  }
26877
+ }
26939
26878
 
26940
- // Apply API capability filter — only return packages whose required
26941
- // "api" providers are all present in the app's capability set
26942
- if (filters.appCapabilities && filters.appCapabilities.length) {
26943
- const capSet = new Set(filters.appCapabilities.map((c) => c.toLowerCase()));
26944
- packages = packages.filter((pkg) => {
26945
- // Collect all "api" provider requirements from package-level and widget-level providers
26946
- const apiProviders = [];
26947
-
26948
- // Package-level providers
26949
- for (const p of pkg.providers || []) {
26950
- if (p.providerClass === "api" && p.required !== false) {
26951
- apiProviders.push(p.type);
26952
- }
26953
- }
26954
-
26955
- // Widget-level providers
26956
- for (const w of pkg.widgets || []) {
26957
- for (const p of w.providers || []) {
26958
- if (p.providerClass === "api" && p.required !== false) {
26959
- apiProviders.push(p.type);
26960
- }
26961
- }
26962
- }
26963
-
26964
- // Package is compatible if all required API namespaces are present
26965
- return apiProviders.every((api) => capSet.has(api.toLowerCase()));
26966
- });
26879
+ /**
26880
+ * Clear stored auth token (logout).
26881
+ */
26882
+ function clearToken$2() {
26883
+ try {
26884
+ const s = getStore$1();
26885
+ s.clear();
26886
+ console.log("[RegistryAuthController] Token cleared");
26887
+ } catch (err) {
26888
+ console.error("[RegistryAuthController] Error clearing token:", err);
26967
26889
  }
26968
-
26969
- // Count total widgets across matched packages
26970
- const totalWidgets = packages.reduce(
26971
- (sum, pkg) => sum + (pkg.widgets || []).length,
26972
- 0,
26973
- );
26974
-
26975
- return { packages, totalWidgets };
26976
26890
  }
26977
26891
 
26978
26892
  /**
26979
- * Get a specific package by name.
26980
- *
26981
- * Handles multiple naming formats:
26982
- * - bare name: "ocean-depth"
26983
- * - scoped name: "john/ocean-depth" or "@john/ocean-depth"
26984
- * - displayName: "Ocean Depth"
26893
+ * Update the authenticated user's registry profile.
26985
26894
  *
26986
- * @param {string} packageName - Name of the package (any format)
26987
- * @returns {Promise<Object|null>} Package data or null if not found
26895
+ * @param {Object} updates - Fields to update (e.g. { displayName })
26896
+ * @returns {Promise<Object|null>} Updated user or null on 401
26988
26897
  */
26989
- async function getPackage$1(packageName) {
26990
- if (!packageName) return null;
26991
-
26992
- const index = await fetchRegistryIndex();
26993
- const packages = index.packages || [];
26994
-
26995
- // 1. Exact match on name
26996
- let pkg = packages.find((p) => p.name === packageName);
26997
- if (pkg) return pkg;
26998
-
26999
- // 2. If input contains "/", split into scope + name and match both fields
27000
- if (packageName.includes("/")) {
27001
- const parts = packageName.split("/");
27002
- const inputScope = parts[0].replace(/^@/, "");
27003
- const inputName = parts.slice(1).join("/");
27004
- pkg = packages.find(
27005
- (p) =>
27006
- p.name === inputName &&
27007
- (p.scope || "").replace(/^@/, "") === inputScope,
27008
- );
27009
- if (pkg) return pkg;
27010
- }
26898
+ async function updateRegistryProfile$1(updates) {
26899
+ const stored = getStoredToken$4();
26900
+ if (!stored) return null;
27011
26901
 
27012
- // 3. Match by displayName (case-insensitive)
27013
- const lower = packageName.toLowerCase();
27014
- pkg = packages.find((p) => (p.displayName || "").toLowerCase() === lower);
27015
- if (pkg) return pkg;
26902
+ try {
26903
+ const response = await fetch(`${REGISTRY_BASE_URL$1}/api/auth/me`, {
26904
+ method: "PATCH",
26905
+ headers: {
26906
+ Authorization: `Bearer ${stored.token}`,
26907
+ "Content-Type": "application/json",
26908
+ },
26909
+ body: JSON.stringify(updates),
26910
+ });
27016
26911
 
27017
- // 4. Try bare-name match against scoped registry entries
27018
- // (registry might store "scope/name" in p.name while caller sends just "name")
27019
- pkg = packages.find((p) => {
27020
- if (p.name && p.name.includes("/")) {
27021
- const bareName = p.name.split("/").pop();
27022
- return bareName === packageName;
26912
+ if (response.status === 401) {
26913
+ clearToken$2();
26914
+ return null;
27023
26915
  }
27024
- return false;
27025
- });
26916
+ if (!response.ok) return null;
27026
26917
 
27027
- return pkg || null;
26918
+ const data = await response.json();
26919
+ return data.user || null;
26920
+ } catch {
26921
+ return null;
26922
+ }
27028
26923
  }
27029
26924
 
27030
26925
  /**
27031
- * Check for updates to installed widgets
26926
+ * Get the authenticated user's published packages.
27032
26927
  *
27033
- * @param {Array<Object>} installedWidgets - Array of { name, version } objects
27034
- * @returns {Promise<Array<Object>>} Widgets with available updates
26928
+ * @returns {Promise<Object|null>} { packages: [...] } or null
27035
26929
  */
27036
- async function checkUpdates(installedWidgets = []) {
27037
- const index = await fetchRegistryIndex();
27038
- const updates = [];
26930
+ async function getRegistryPackages$1() {
26931
+ const stored = getStoredToken$4();
26932
+ if (!stored) return null;
27039
26933
 
27040
- for (const installed of installedWidgets) {
27041
- const installedId = installed.packageId || installed.name;
27042
- const pkg = (index.packages || []).find((p) => {
27043
- // Match by scoped ID (e.g. "@trops/slack" === "@trops/slack")
27044
- const registryId = toPackageId(p.scope, p.name);
27045
- if (registryId === installedId) return true;
27046
- // Fallback: bare-name match for pre-migration entries
27047
- if (p.name === installedId) return true;
27048
- return false;
26934
+ try {
26935
+ const response = await fetch(`${REGISTRY_BASE_URL$1}/api/auth/me/packages`, {
26936
+ headers: {
26937
+ Authorization: `Bearer ${stored.token}`,
26938
+ },
27049
26939
  });
27050
- if (pkg && pkg.version !== installed.version) {
27051
- updates.push({
27052
- name: installed.name,
27053
- currentVersion: installed.version,
27054
- latestVersion: pkg.version,
27055
- downloadUrl: pkg.downloadUrl,
27056
- changelog: pkg.changelog || null,
27057
- });
26940
+
26941
+ if (response.status === 401) {
26942
+ clearToken$2();
26943
+ return null;
27058
26944
  }
27059
- }
26945
+ if (!response.ok) return null;
27060
26946
 
27061
- return updates;
26947
+ return await response.json();
26948
+ } catch {
26949
+ return null;
26950
+ }
27062
26951
  }
27063
26952
 
27064
26953
  /**
27065
- * Search the registry for dashboard packages only.
27066
- * Convenience wrapper around searchRegistry with type: "dashboard".
26954
+ * Update a published package's metadata.
27067
26955
  *
27068
- * @param {string} query - Search query string
27069
- * @param {Object} filters - Optional filters (category, author, tag, compatibleWidgets)
27070
- * @returns {Promise<Object>} { packages: [...], totalWidgets: number }
26956
+ * @param {string} scope - Package scope (e.g. "@trops")
26957
+ * @param {string} name - Package name
26958
+ * @param {Object} updates - Fields to update (displayName, description, category, tags, visibility)
26959
+ * @returns {Promise<Object|null>} Updated package or null
27071
26960
  */
27072
- async function searchDashboards(query = "", filters = {}) {
27073
- return searchRegistry$1(query, { ...filters, type: "dashboard" });
26961
+ async function updateRegistryPackage$1(scope, name, updates) {
26962
+ const stored = getStoredToken$4();
26963
+ if (!stored) return null;
26964
+
26965
+ try {
26966
+ const response = await fetch(
26967
+ `${REGISTRY_BASE_URL$1}/api/packages/${encodeURIComponent(scope)}/${encodeURIComponent(name)}`,
26968
+ {
26969
+ method: "PATCH",
26970
+ headers: {
26971
+ Authorization: `Bearer ${stored.token}`,
26972
+ "Content-Type": "application/json",
26973
+ },
26974
+ body: JSON.stringify(updates),
26975
+ },
26976
+ );
26977
+
26978
+ if (response.status === 401) {
26979
+ clearToken$2();
26980
+ return null;
26981
+ }
26982
+ if (!response.ok) return null;
26983
+
26984
+ return await response.json();
26985
+ } catch {
26986
+ return null;
26987
+ }
27074
26988
  }
27075
26989
 
27076
26990
  /**
27077
- * Search the registry for theme packages only.
27078
- * Convenience wrapper around searchRegistry with type: "theme".
26991
+ * Delete a published package from the registry.
27079
26992
  *
27080
- * @param {string} query - Search query string
27081
- * @param {Object} filters - Optional filters (category, author, tag)
27082
- * @returns {Promise<Object>} { packages: [...], totalWidgets: number }
26993
+ * @param {string} scope - Package scope (e.g. "@trops")
26994
+ * @param {string} name - Package name
26995
+ * @returns {Promise<Object|null>} Response or null
27083
26996
  */
27084
- async function searchThemes(query = "", filters = {}) {
27085
- return searchRegistry$1(query, { ...filters, type: "theme" });
27086
- }
26997
+ async function deleteRegistryPackage(scope, name) {
26998
+ const stored = getStoredToken$4();
26999
+ if (!stored) return null;
27087
27000
 
27088
- var registryController$3 = {
27089
- fetchRegistryIndex,
27090
- searchRegistry: searchRegistry$1,
27091
- searchDashboards,
27092
- searchThemes,
27093
- getPackage: getPackage$1,
27094
- checkUpdates,
27001
+ try {
27002
+ const response = await fetch(
27003
+ `${REGISTRY_BASE_URL$1}/api/packages/${encodeURIComponent(scope)}/${encodeURIComponent(name)}`,
27004
+ {
27005
+ method: "DELETE",
27006
+ headers: {
27007
+ Authorization: `Bearer ${stored.token}`,
27008
+ },
27009
+ },
27010
+ );
27011
+
27012
+ if (response.status === 401) {
27013
+ clearToken$2();
27014
+ return null;
27015
+ }
27016
+ if (!response.ok) return null;
27017
+
27018
+ return await response.json();
27019
+ } catch {
27020
+ return null;
27021
+ }
27022
+ }
27023
+
27024
+ var registryAuthController$2 = {
27025
+ initiateDeviceFlow: initiateDeviceFlow$1,
27026
+ pollForToken: pollForToken$1,
27027
+ getStoredToken: getStoredToken$4,
27028
+ getAuthStatus: getAuthStatus$1,
27029
+ getRegistryProfile: getRegistryProfile$2,
27030
+ updateRegistryProfile: updateRegistryProfile$1,
27031
+ getRegistryPackages: getRegistryPackages$1,
27032
+ updateRegistryPackage: updateRegistryPackage$1,
27033
+ deleteRegistryPackage,
27034
+ clearToken: clearToken$2,
27095
27035
  };
27096
27036
 
27097
- var fs$5 = require$$0$2;
27098
- var JSONStream = require$$4;
27099
- const algoliasearch$1 = require$$2$2;
27100
- const path$9 = require$$3$3;
27101
- const { ensureDirectoryExistence, checkDirectory } = file;
27037
+ /**
27038
+ * registryController.js
27039
+ *
27040
+ * Manages fetching, caching, and searching the remote widget registry index.
27041
+ * Runs in the Electron main process.
27042
+ *
27043
+ * Responsibilities:
27044
+ * - Fetch and cache the remote registry-index.json with 5-min TTL
27045
+ * - Search/filter across both packages and individual widgets
27046
+ * - Support two-level browsing: packages (bundles) and widgets within packages
27047
+ */
27102
27048
 
27103
- let AlgoliaIndex$1 = class AlgoliaIndex {
27104
- /**
27105
- * @var client the algoliasearch client
27106
- */
27107
- client = null;
27049
+ const path$a = require$$1$2;
27050
+ const fs$6 = require$$0$2;
27051
+ const { toPackageId } = packageId;
27052
+ const { getStoredToken: getStoredToken$3 } = registryAuthController$2;
27108
27053
 
27109
- /**
27110
- * @var index the algoliasearch initiated index
27111
- */
27112
- index = null;
27054
+ /**
27055
+ * Build request headers for the registry. When the user is signed in, we
27056
+ * include the Bearer token so the server returns private packages that
27057
+ * the user owns or has been granted entitlements for. Anonymous fetches
27058
+ * still work and simply return only public packages.
27059
+ */
27060
+ function buildAuthHeaders() {
27061
+ const stored = getStoredToken$3();
27062
+ return stored?.token ? { Authorization: `Bearer ${stored.token}` } : {};
27063
+ }
27113
27064
 
27114
- constructor(appId = "", apiKey = "", indexName = "") {
27115
- if (appId !== "" && apiKey !== "" && indexName !== "") {
27116
- this.client = algoliasearch$1(appId, apiKey);
27117
- this.index = this.client.initIndex(indexName);
27118
- }
27119
- }
27065
+ // Default registry API base URL
27066
+ const DEFAULT_REGISTRY_API_URL = "https://main.d919rwhuzp7rj.amplifyapp.com";
27120
27067
 
27121
- createBatchesFromJSONFile = (
27122
- filepath,
27123
- batchFilepath = "/data/batch",
27124
- batchSize,
27125
- callback = null,
27126
- ) => {
27127
- return new Promise((resolve, reject) => {
27128
- // instantiate the JSON parser that will be used by the readStream
27129
- var parser = JSONStream.parse("*");
27068
+ // Cache TTL: 5 minutes
27069
+ const CACHE_TTL_MS = 5 * 60 * 1000;
27130
27070
 
27131
- // count how many items have been added to a single batch
27132
- var countForBatch = 0;
27071
+ // Cache is keyed by userId so anonymous + authenticated results don't mix.
27072
+ // When a user signs in, their cache entry is empty and gets populated with
27073
+ // their owned/entitled private packages alongside the public set.
27074
+ const caches = new Map(); // userId | "anon" -> { data, timestamp }
27133
27075
 
27134
- // counter for the number of batches (used as filename)
27135
- var batchNumber = 1;
27076
+ function getCacheKey() {
27077
+ const stored = getStoredToken$3();
27078
+ return stored?.userId || "anon";
27079
+ }
27136
27080
 
27137
- // create the readStream to parse the large file (json)
27138
- var readStream = fs$5.createReadStream(filepath).pipe(parser);
27081
+ /**
27082
+ * Get the local test registry path for dev mode
27083
+ */
27084
+ function getTestRegistryPath() {
27085
+ return path$a.join(__dirname, "..", "registry", "test-registry-index.json");
27086
+ }
27139
27087
 
27140
- var batch = [];
27088
+ /**
27089
+ * Check if running in development mode
27090
+ */
27091
+ function isDev() {
27092
+ return (
27093
+ process.defaultApp ||
27094
+ process.env.NODE_ENV === "development" ||
27095
+ process.env.NODE_ENV === "dev"
27096
+ );
27097
+ }
27141
27098
 
27142
- // lets first remove the batch folder
27143
- this.clearDirectory(batchFilepath)
27144
- .then(() => {
27145
- // when we receive data...
27146
- readStream.on("data", function (data) {
27147
- try {
27148
- // if we have reached the limit for the batch...
27149
- // lets write to the batch file
27150
- if (countForBatch === batchSize) {
27151
- // write to the batch file
27152
- var writeStream = fs$5.createWriteStream(
27153
- batchFilepath + "/batch_" + batchNumber + ".json",
27154
- );
27155
- writeStream.write(JSON.stringify(batch));
27156
- writeStream.close();
27099
+ /**
27100
+ * Fetch the registry index from remote URL or local file (dev mode)
27101
+ * Caches the result for CACHE_TTL_MS milliseconds.
27102
+ *
27103
+ * @param {boolean} forceRefresh - Bypass cache and fetch fresh data
27104
+ * @returns {Promise<Object>} The registry index
27105
+ */
27106
+ async function fetchRegistryIndex(forceRefresh = false) {
27107
+ const now = Date.now();
27108
+ const cacheKey = getCacheKey();
27109
+ const cached = caches.get(cacheKey);
27157
27110
 
27158
- // adjust counts and reset batch array
27159
- countForBatch = 0;
27160
- // bump the batch number
27161
- batchNumber++;
27162
- // reset the batch json
27163
- batch = [];
27164
- // callback function to pass batchnumber (or anything later on)
27165
- callback &&
27166
- typeof callback === "function" &&
27167
- callback(batchNumber);
27168
- } else {
27169
- try {
27170
- // push the JSON data into the batch array to be written later
27171
- batch.push(data);
27172
- countForBatch++;
27173
- } catch (e) {
27174
- reject(e);
27175
- }
27176
- }
27177
- } catch (e) {
27178
- reject(e);
27179
- }
27180
- });
27111
+ // Return cached data if still valid
27112
+ if (!forceRefresh && cached && now - cached.timestamp < CACHE_TTL_MS) {
27113
+ console.log(
27114
+ `[RegistryController] Returning cached registry index (key=${cacheKey})`,
27115
+ );
27116
+ return cached.data;
27117
+ }
27181
27118
 
27182
- readStream.on("error", function (e) {
27183
- console.log("batch on error ", e);
27184
- reject(e);
27185
- });
27119
+ try {
27120
+ let indexData;
27186
27121
 
27187
- readStream.on("close", function () {
27188
- console.log("batch on close ");
27189
- resolve("batches completed ", batchNumber);
27190
- });
27191
- })
27192
- .catch((e) => {
27193
- console.log("catch batch ", e.message);
27194
- reject(e);
27122
+ if (isDev()) {
27123
+ // In dev mode, try local test file first
27124
+ const testPath = getTestRegistryPath();
27125
+ if (fs$6.existsSync(testPath)) {
27126
+ console.log(
27127
+ "[RegistryController] Loading test registry from:",
27128
+ testPath,
27129
+ );
27130
+ const raw = fs$6.readFileSync(testPath, "utf8");
27131
+ indexData = JSON.parse(raw);
27132
+ } else {
27133
+ // Fall back to API (supports DASH_REGISTRY_URL as full-URL override)
27134
+ const registryUrl =
27135
+ process.env.DASH_REGISTRY_URL ||
27136
+ `${process.env.DASH_REGISTRY_API_URL || DEFAULT_REGISTRY_API_URL}/api/packages`;
27137
+ console.log(
27138
+ "[RegistryController] Fetching registry from:",
27139
+ registryUrl,
27140
+ );
27141
+ const response = await fetch(registryUrl, {
27142
+ headers: buildAuthHeaders(),
27195
27143
  });
27196
- });
27197
- };
27144
+ if (!response.ok) {
27145
+ throw new Error(
27146
+ `Failed to fetch registry: ${response.status} ${response.statusText}`,
27147
+ );
27148
+ }
27149
+ indexData = await response.json();
27150
+ }
27151
+ } else {
27152
+ // In production, fetch from API
27153
+ const registryUrl =
27154
+ process.env.DASH_REGISTRY_URL ||
27155
+ `${process.env.DASH_REGISTRY_API_URL || DEFAULT_REGISTRY_API_URL}/api/packages`;
27156
+ console.log("[RegistryController] Fetching registry from:", registryUrl);
27198
27157
 
27199
- clearDirectory = (directoryPath) => {
27200
- return new Promise((resolve, reject) => {
27201
- try {
27202
- checkDirectory(directoryPath);
27203
- fs$5.readdir(directoryPath, (err, files) => {
27204
- if (err) reject(err);
27205
- if (files) {
27206
- files.forEach((file) => {
27207
- fs$5.unlinkSync(path$9.join(directoryPath, file));
27208
- });
27209
- resolve();
27210
- }
27211
- });
27212
- } catch (e) {
27213
- console.log("clear dir error ", e.message);
27214
- reject(e);
27158
+ const response = await fetch(registryUrl, {
27159
+ headers: buildAuthHeaders(),
27160
+ });
27161
+ if (!response.ok) {
27162
+ throw new Error(
27163
+ `Failed to fetch registry: ${response.status} ${response.statusText}`,
27164
+ );
27215
27165
  }
27216
- });
27217
- };
27166
+ indexData = await response.json();
27167
+ }
27218
27168
 
27219
- async partialUpdateObjectsFromDirectorySync(
27220
- batchFilepath,
27221
- createIfNotExists = false,
27222
- callback = null,
27223
- ) {
27224
- try {
27225
- // read the directory...
27226
- const files = await fs$5.readdirSync(batchFilepath);
27227
- let results = [];
27228
- for (const fileIndex in files) {
27229
- // for each file lets read the file and then push to algolia
27230
- const pathToBatch = path$9.join(batchFilepath, files[fileIndex]);
27231
- const fileContents = await this.readFile(pathToBatch);
27232
- if (fileContents) {
27233
- if ("data" in fileContents && "filepath" in fileContents) {
27234
- // now we can update the index with the partial update
27235
- const updateResult = await this.partialUpdateObjects(
27236
- fileContents.data,
27237
- fileContents.filepath,
27238
- createIfNotExists,
27239
- callback,
27240
- );
27241
- results.push({ file: files[fileIndex] });
27242
- } else {
27243
- console.log("missed ", files[fileIndex]);
27244
- }
27245
- }
27246
- }
27247
- return Promise.resolve(results);
27248
- } catch (e) {
27249
- return Promise.reject(e);
27169
+ // Normalize: ensure `version` exists on each package (API uses `latestVersion`)
27170
+ if (indexData.packages) {
27171
+ indexData.packages = indexData.packages.map((pkg) => ({
27172
+ ...pkg,
27173
+ version: pkg.version || pkg.latestVersion || "0.0.0",
27174
+ }));
27175
+ }
27176
+
27177
+ // Cache the result
27178
+ caches.set(cacheKey, { data: indexData, timestamp: now });
27179
+
27180
+ console.log(
27181
+ `[RegistryController] Loaded ${indexData.packages?.length || 0} packages (key=${cacheKey})`,
27182
+ );
27183
+ return indexData;
27184
+ } catch (error) {
27185
+ console.error("[RegistryController] Error fetching registry:", error);
27186
+
27187
+ // Return stale cache if available
27188
+ const stale = caches.get(cacheKey);
27189
+ if (stale) {
27190
+ console.log(
27191
+ "[RegistryController] Returning stale cache after fetch error",
27192
+ );
27193
+ return stale.data;
27250
27194
  }
27195
+
27196
+ throw error;
27251
27197
  }
27198
+ }
27252
27199
 
27253
- async readFile(filepath) {
27254
- return await new Promise((resolve, reject) => {
27255
- fs$5.readFile(filepath, "utf8", (err, data) => {
27256
- if (err) {
27257
- reject(err);
27258
- }
27259
- resolve({ data, filepath });
27260
- });
27261
- });
27200
+ /**
27201
+ * Search the registry across packages and individual widgets
27202
+ *
27203
+ * @param {string} query - Search query string
27204
+ * @param {Object} filters - Optional filters
27205
+ * @param {string} filters.category - Filter by category
27206
+ * @param {string} filters.author - Filter by author
27207
+ * @param {string} filters.tag - Filter by tag
27208
+ * @param {string} filters.type - Filter by package type ("widget" or "dashboard")
27209
+ * @param {string[]} filters.compatibleWidgets - Only return dashboards whose required widgets are all in this list
27210
+ * @param {string[]} filters.appCapabilities - Only return packages whose required API providers are all in this list
27211
+ * @returns {Promise<Object>} { packages: [...], totalWidgets: number }
27212
+ */
27213
+ async function searchRegistry$1(query = "", filters = {}) {
27214
+ const index = await fetchRegistryIndex();
27215
+ let packages = index.packages || [];
27216
+
27217
+ // Apply type filter — packages without an explicit type default to "widget"
27218
+ if (filters.type) {
27219
+ const typeLower = filters.type.toLowerCase();
27220
+ packages = packages.filter(
27221
+ (pkg) => (pkg.type || "widget").toLowerCase() === typeLower,
27222
+ );
27262
27223
  }
27263
27224
 
27264
- browseObjects = (query = "", callback = null) => {
27265
- return new Promise((resolve, reject) => {
27266
- try {
27267
- if (this.index !== null) {
27268
- // call algolia to update the objects
27269
- this.index
27270
- .browseObjects({
27271
- query,
27272
- batch: (hits) => {
27273
- if (callback && typeof callback === "function") {
27274
- callback(hits);
27275
- }
27276
- },
27277
- })
27278
- .then(() => {
27279
- resolve({ success: true });
27280
- })
27281
- .catch((e) => reject(e));
27282
- } else {
27283
- reject("No index for client");
27284
- }
27285
- } catch (e) {
27286
- console.log("browse objects ", e.message);
27287
- reject(e);
27288
- }
27225
+ // Apply search query
27226
+ if (query) {
27227
+ const q = query.toLowerCase();
27228
+ packages = packages.filter((pkg) => {
27229
+ // Match against package-level fields
27230
+ const packageMatch =
27231
+ (pkg.name || "").toLowerCase().includes(q) ||
27232
+ (pkg.displayName || "").toLowerCase().includes(q) ||
27233
+ (pkg.description || "").toLowerCase().includes(q) ||
27234
+ (pkg.author || "").toLowerCase().includes(q) ||
27235
+ (pkg.tags || []).some((t) => t.toLowerCase().includes(q));
27236
+
27237
+ // Match against individual widgets within the package
27238
+ const widgetMatch = (pkg.widgets || []).some(
27239
+ (w) =>
27240
+ (w.name || "").toLowerCase().includes(q) ||
27241
+ (w.displayName || "").toLowerCase().includes(q) ||
27242
+ (w.description || "").toLowerCase().includes(q),
27243
+ );
27244
+
27245
+ return packageMatch || widgetMatch;
27289
27246
  });
27290
- };
27247
+ }
27291
27248
 
27292
- async partialUpdateObjects(
27293
- objects,
27294
- file,
27295
- createIfNotExists = false,
27296
- callback = null,
27297
- ) {
27298
- return new Promise((resolve, reject) => {
27299
- try {
27300
- if (objects) {
27301
- const batch = JSON.parse(objects);
27249
+ // Apply category filter (supports single string or comma-separated or array)
27250
+ if (filters.category) {
27251
+ const cats = Array.isArray(filters.category)
27252
+ ? filters.category
27253
+ : filters.category.split(",").map((c) => c.trim().toLowerCase());
27254
+ packages = packages.filter((pkg) =>
27255
+ cats.includes((pkg.category || "").toLowerCase()),
27256
+ );
27257
+ }
27302
27258
 
27303
- // callback function to pass batchnumber (or anything later on)
27304
- if (callback && typeof callback === "function") {
27305
- callback("indexing objects ", file, batch.length);
27306
- }
27259
+ // Apply author filter
27260
+ if (filters.author) {
27261
+ packages = packages.filter(
27262
+ (pkg) =>
27263
+ (pkg.author || "").toLowerCase() === filters.author.toLowerCase(),
27264
+ );
27265
+ }
27307
27266
 
27308
- if (this.index !== null) {
27309
- // call algolia to update the objects
27310
- this.index
27311
- .partialUpdateObjects(batch, {
27312
- createIfNotExists: createIfNotExists,
27313
- })
27314
- .then(({ objectIDs }) => {
27315
- resolve({
27316
- success: true,
27317
- batchComplete: batch.length,
27318
- objectIDs,
27319
- });
27320
- })
27321
- .catch((e) => {
27322
- console.log("Error partialUpdateObjects", e.message);
27323
- reject(e);
27324
- });
27325
- } else {
27326
- reject("No index for client");
27327
- }
27328
- }
27329
- } catch (e) {
27330
- console.log("partial update objects ", e.message);
27331
- reject(e);
27332
- }
27267
+ // Apply tag filter (supports single string or comma-separated or array)
27268
+ if (filters.tag) {
27269
+ const tags = Array.isArray(filters.tag)
27270
+ ? filters.tag
27271
+ : filters.tag.split(",").map((t) => t.trim().toLowerCase());
27272
+ packages = packages.filter((pkg) =>
27273
+ (pkg.tags || []).some((t) => tags.includes(t.toLowerCase())),
27274
+ );
27275
+ }
27276
+
27277
+ // Apply compatibility filter — only dashboards whose required widgets
27278
+ // are all present in the user's installed widget list
27279
+ if (filters.compatibleWidgets && filters.compatibleWidgets.length) {
27280
+ const installedSet = new Set(
27281
+ filters.compatibleWidgets.map((w) => w.toLowerCase()),
27282
+ );
27283
+ packages = packages.filter((pkg) => {
27284
+ const requiredWidgets = (pkg.widgets || []).filter(
27285
+ (w) => w.required !== false,
27286
+ );
27287
+ return requiredWidgets.every(
27288
+ (w) =>
27289
+ installedSet.has((w.package || "").toLowerCase()) ||
27290
+ installedSet.has((w.name || "").toLowerCase()),
27291
+ );
27333
27292
  });
27334
27293
  }
27335
27294
 
27336
- search = (query = "", options = {}) => {
27337
- return new Promise((resolve, reject) => {
27338
- try {
27339
- if (this.index !== null) {
27340
- this.index
27341
- .search(query, options)
27342
- .then((result) => resolve(result))
27343
- .catch((e) => reject(e));
27344
- } else {
27345
- reject("No index for client");
27295
+ // Apply API capability filter only return packages whose required
27296
+ // "api" providers are all present in the app's capability set
27297
+ if (filters.appCapabilities && filters.appCapabilities.length) {
27298
+ const capSet = new Set(filters.appCapabilities.map((c) => c.toLowerCase()));
27299
+ packages = packages.filter((pkg) => {
27300
+ // Collect all "api" provider requirements from package-level and widget-level providers
27301
+ const apiProviders = [];
27302
+
27303
+ // Package-level providers
27304
+ for (const p of pkg.providers || []) {
27305
+ if (p.providerClass === "api" && p.required !== false) {
27306
+ apiProviders.push(p.type);
27346
27307
  }
27347
- } catch (e) {
27348
- reject(e);
27349
27308
  }
27309
+
27310
+ // Widget-level providers
27311
+ for (const w of pkg.widgets || []) {
27312
+ for (const p of w.providers || []) {
27313
+ if (p.providerClass === "api" && p.required !== false) {
27314
+ apiProviders.push(p.type);
27315
+ }
27316
+ }
27317
+ }
27318
+
27319
+ // Package is compatible if all required API namespaces are present
27320
+ return apiProviders.every((api) => capSet.has(api.toLowerCase()));
27350
27321
  });
27351
- };
27322
+ }
27352
27323
 
27353
- saveObjects = (objects, file, callback = null) => {
27354
- return new Promise((resolve, reject) => {
27355
- try {
27356
- if (objects) {
27357
- const batch = JSON.parse(objects);
27324
+ // Count total widgets across matched packages
27325
+ const totalWidgets = packages.reduce(
27326
+ (sum, pkg) => sum + (pkg.widgets || []).length,
27327
+ 0,
27328
+ );
27358
27329
 
27359
- // callback function to pass batchnumber (or anything later on)
27360
- if (callback && typeof callback === "function") {
27361
- callback("saving objects ", file);
27362
- }
27330
+ return { packages, totalWidgets };
27331
+ }
27363
27332
 
27364
- if (this.index !== null) {
27365
- // call algolia to update the objects
27366
- this.index
27367
- .saveObjects(batch, {
27368
- autoGenerateObjectIDIfNotExist: true,
27369
- })
27370
- .then(({ objectIDs }) => {
27371
- resolve({
27372
- success: true,
27373
- batchComplete: batch.length,
27374
- file,
27375
- objectIDs,
27376
- });
27377
- })
27378
- .catch((e) => reject(e));
27379
- } else {
27380
- reject("No index for client");
27381
- }
27382
- }
27383
- } catch (e) {
27384
- console.log("save objects error", e.message);
27385
- reject(e);
27386
- }
27333
+ /**
27334
+ * Get a specific package by name.
27335
+ *
27336
+ * Handles multiple naming formats:
27337
+ * - bare name: "ocean-depth"
27338
+ * - scoped name: "john/ocean-depth" or "@john/ocean-depth"
27339
+ * - displayName: "Ocean Depth"
27340
+ *
27341
+ * @param {string} packageName - Name of the package (any format)
27342
+ * @returns {Promise<Object|null>} Package data or null if not found
27343
+ */
27344
+ async function getPackage$1(packageName) {
27345
+ if (!packageName) return null;
27346
+
27347
+ const index = await fetchRegistryIndex();
27348
+ const packages = index.packages || [];
27349
+
27350
+ // 1. Exact match on name
27351
+ let pkg = packages.find((p) => p.name === packageName);
27352
+ if (pkg) return pkg;
27353
+
27354
+ // 2. If input contains "/", split into scope + name and match both fields
27355
+ if (packageName.includes("/")) {
27356
+ const parts = packageName.split("/");
27357
+ const inputScope = parts[0].replace(/^@/, "");
27358
+ const inputName = parts.slice(1).join("/");
27359
+ pkg = packages.find(
27360
+ (p) =>
27361
+ p.name === inputName &&
27362
+ (p.scope || "").replace(/^@/, "") === inputScope,
27363
+ );
27364
+ if (pkg) return pkg;
27365
+ }
27366
+
27367
+ // 3. Match by displayName (case-insensitive)
27368
+ const lower = packageName.toLowerCase();
27369
+ pkg = packages.find((p) => (p.displayName || "").toLowerCase() === lower);
27370
+ if (pkg) return pkg;
27371
+
27372
+ // 4. Try bare-name match against scoped registry entries
27373
+ // (registry might store "scope/name" in p.name while caller sends just "name")
27374
+ pkg = packages.find((p) => {
27375
+ if (p.name && p.name.includes("/")) {
27376
+ const bareName = p.name.split("/").pop();
27377
+ return bareName === packageName;
27378
+ }
27379
+ return false;
27380
+ });
27381
+
27382
+ return pkg || null;
27383
+ }
27384
+
27385
+ /**
27386
+ * Check for updates to installed widgets
27387
+ *
27388
+ * @param {Array<Object>} installedWidgets - Array of { name, version } objects
27389
+ * @returns {Promise<Array<Object>>} Widgets with available updates
27390
+ */
27391
+ async function checkUpdates(installedWidgets = []) {
27392
+ const index = await fetchRegistryIndex();
27393
+ const updates = [];
27394
+
27395
+ for (const installed of installedWidgets) {
27396
+ const installedId = installed.packageId || installed.name;
27397
+ const pkg = (index.packages || []).find((p) => {
27398
+ // Match by scoped ID (e.g. "@trops/slack" === "@trops/slack")
27399
+ const registryId = toPackageId(p.scope, p.name);
27400
+ if (registryId === installedId) return true;
27401
+ // Fallback: bare-name match for pre-migration entries
27402
+ if (p.name === installedId) return true;
27403
+ return false;
27387
27404
  });
27388
- };
27389
- };
27405
+ if (pkg && pkg.version !== installed.version) {
27406
+ updates.push({
27407
+ name: installed.name,
27408
+ currentVersion: installed.version,
27409
+ latestVersion: pkg.version,
27410
+ downloadUrl: pkg.downloadUrl,
27411
+ changelog: pkg.changelog || null,
27412
+ });
27413
+ }
27414
+ }
27390
27415
 
27391
- var algolia = AlgoliaIndex$1;
27416
+ return updates;
27417
+ }
27392
27418
 
27393
27419
  /**
27394
- * algoliaController.js
27420
+ * Search the registry for dashboard packages only.
27421
+ * Convenience wrapper around searchRegistry with type: "dashboard".
27395
27422
  *
27396
- * This is a sample controller that is called from the electron.js file
27423
+ * @param {string} query - Search query string
27424
+ * @param {Object} filters - Optional filters (category, author, tag, compatibleWidgets)
27425
+ * @returns {Promise<Object>} { packages: [...], totalWidgets: number }
27426
+ */
27427
+ async function searchDashboards(query = "", filters = {}) {
27428
+ return searchRegistry$1(query, { ...filters, type: "dashboard" });
27429
+ }
27430
+
27431
+ /**
27432
+ * Search the registry for theme packages only.
27433
+ * Convenience wrapper around searchRegistry with type: "theme".
27397
27434
  *
27398
- * The electron.js file contains listeners from the renderer that will call
27399
- * the controller methods as seen below.
27435
+ * @param {string} query - Search query string
27436
+ * @param {Object} filters - Optional filters (category, author, tag)
27437
+ * @returns {Promise<Object>} { packages: [...], totalWidgets: number }
27400
27438
  */
27439
+ async function searchThemes(query = "", filters = {}) {
27440
+ return searchRegistry$1(query, { ...filters, type: "theme" });
27441
+ }
27401
27442
 
27402
- const algoliasearch = require$$2$2;
27403
- const events$3 = events$8;
27404
- const AlgoliaIndex = algolia;
27405
- var fs$4 = require$$0$2;
27443
+ var registryController$3 = {
27444
+ fetchRegistryIndex,
27445
+ searchRegistry: searchRegistry$1,
27446
+ searchDashboards,
27447
+ searchThemes,
27448
+ getPackage: getPackage$1,
27449
+ checkUpdates,
27450
+ };
27406
27451
 
27407
- const algoliaController$1 = {
27452
+ var fs$5 = require$$0$2;
27453
+ var JSONStream = require$$4;
27454
+ const algoliasearch$1 = require$$2$2;
27455
+ const path$9 = require$$3$3;
27456
+ const { ensureDirectoryExistence, checkDirectory } = file;
27457
+
27458
+ let AlgoliaIndex$1 = class AlgoliaIndex {
27408
27459
  /**
27409
- * loadPagesForApplication
27410
- * Load the pages for the application <userdata>/appId/pages.json
27411
- * - filter out the indices that are "rule" indices
27412
- *
27413
- * @param {BrowserWindow} win the main window
27414
- * @param {string} appId the application id from Algolia
27460
+ * @var client the algoliasearch client
27415
27461
  */
27416
- listIndices: (win, application) => {
27417
- try {
27418
- const searchClient = algoliasearch(
27419
- application["appId"],
27420
- application["key"],
27421
- );
27422
- searchClient
27423
- .listIndices()
27424
- .then(({ items }) => {
27425
- const filtered = items.filter(
27426
- (item) => item.name.substring(0, 7) !== "sitehub",
27427
- );
27428
- win.webContents.send(events$3.ALGOLIA_LIST_INDICES_COMPLETE, filtered);
27429
- })
27430
- .catch((e) => {
27431
- win.webContents.send(events$3.ALGOLIA_LIST_INDICES_ERROR, {
27432
- error: e.message,
27433
- });
27434
- });
27435
- } catch (e) {
27436
- win.webContents.send(events$3.ALGOLIA_LIST_INDICES_ERROR, {
27437
- error: e.message,
27438
- });
27439
- }
27440
- },
27441
-
27442
- getAnalyticsForQuery: (win, application, indexName, query) => {
27443
- try {
27444
- const baseUrl = "https://analytics.us.algolia.com";
27445
- const headers = {
27446
- "X-Algolia-Application-Id": application["appId"],
27447
- "X-Algolia-API-Key": application["key"],
27448
- };
27449
- const url = `${baseUrl}/2/hits?search=${encodeURIComponent(
27450
- query,
27451
- )}&clickAnalytics=true&index=${indexName}`;
27452
- axios
27453
- .get(url, {
27454
- headers: headers,
27455
- })
27456
- .then((resp) => {
27457
- if (resp.status === 200) {
27458
- win.webContents.send(events$3.ALGOLIA_ANALYTICS_FOR_QUERY_COMPLETE, {
27459
- result: resp.data,
27460
- indexName: indexName,
27461
- query: query,
27462
- });
27463
- } else {
27464
- win.webContents.send(events$3.ALGOLIA_ANALYTICS_FOR_QUERY_ERROR, {
27465
- error: true,
27466
- message: "Failed request",
27467
- });
27468
- }
27469
- })
27470
- .catch((e) => {
27471
- win.webContents.send(events$3.ALGOLIA_ANALYTICS_FOR_QUERY_ERROR, {
27472
- error: true,
27473
- message: e.message,
27474
- });
27475
- });
27476
- } catch (e) {
27477
- win.webContents.send(events$3.ALGOLIA_ANALYTICS_FOR_QUERY_ERROR, {
27478
- error: true,
27479
- message: e.message,
27480
- });
27481
- }
27482
- },
27462
+ client = null;
27483
27463
 
27484
27464
  /**
27485
- * browseObjectsToFile
27486
- * Lets try and browse an index and pull down the hits and save as a file
27487
- *
27488
- * @param {*} win
27489
- * @param {*} appId
27490
- * @param {*} apiKey
27491
- * @param {*} indexName
27492
- * @param {*} toFilename
27493
- * @param {*} query
27465
+ * @var index the algoliasearch initiated index
27494
27466
  */
27495
- browseObjectsToFile: (
27496
- win,
27497
- appId,
27498
- apiKey,
27499
- indexName,
27500
- toFilename,
27501
- query = "",
27502
- ) => {
27503
- try {
27504
- if (
27505
- toFilename !== "" &&
27506
- apiKey !== "" &&
27507
- indexName !== "" &&
27508
- appId !== ""
27509
- ) {
27510
- // init the Algolia Index helper
27511
- const a = new AlgoliaIndex(appId, apiKey, indexName);
27512
- // create the write stream to store the hits
27513
- const writeStream = fs$4.createWriteStream(toFilename);
27514
- writeStream.write("[");
27467
+ index = null;
27515
27468
 
27516
- let sep = "";
27469
+ constructor(appId = "", apiKey = "", indexName = "") {
27470
+ if (appId !== "" && apiKey !== "" && indexName !== "") {
27471
+ this.client = algoliasearch$1(appId, apiKey);
27472
+ this.index = this.client.initIndex(indexName);
27473
+ }
27474
+ }
27517
27475
 
27518
- // call the algolia browseObjects helper method
27519
- a.browseObjects(query, (hits) => {
27520
- win.webContents.send(events$3.ALGOLIA_BROWSE_OBJECTS_UPDATE, hits);
27476
+ createBatchesFromJSONFile = (
27477
+ filepath,
27478
+ batchFilepath = "/data/batch",
27479
+ batchSize,
27480
+ callback = null,
27481
+ ) => {
27482
+ return new Promise((resolve, reject) => {
27483
+ // instantiate the JSON parser that will be used by the readStream
27484
+ var parser = JSONStream.parse("*");
27521
27485
 
27522
- let count = 0;
27523
- // write to the file
27524
- hits.forEach((hit) => {
27525
- writeStream.write(sep + JSON.stringify(hit));
27526
- count++;
27527
- sep = ",\n";
27486
+ // count how many items have been added to a single batch
27487
+ var countForBatch = 0;
27488
+
27489
+ // counter for the number of batches (used as filename)
27490
+ var batchNumber = 1;
27491
+
27492
+ // create the readStream to parse the large file (json)
27493
+ var readStream = fs$5.createReadStream(filepath).pipe(parser);
27494
+
27495
+ var batch = [];
27496
+
27497
+ // lets first remove the batch folder
27498
+ this.clearDirectory(batchFilepath)
27499
+ .then(() => {
27500
+ // when we receive data...
27501
+ readStream.on("data", function (data) {
27502
+ try {
27503
+ // if we have reached the limit for the batch...
27504
+ // lets write to the batch file
27505
+ if (countForBatch === batchSize) {
27506
+ // write to the batch file
27507
+ var writeStream = fs$5.createWriteStream(
27508
+ batchFilepath + "/batch_" + batchNumber + ".json",
27509
+ );
27510
+ writeStream.write(JSON.stringify(batch));
27511
+ writeStream.close();
27512
+
27513
+ // adjust counts and reset batch array
27514
+ countForBatch = 0;
27515
+ // bump the batch number
27516
+ batchNumber++;
27517
+ // reset the batch json
27518
+ batch = [];
27519
+ // callback function to pass batchnumber (or anything later on)
27520
+ callback &&
27521
+ typeof callback === "function" &&
27522
+ callback(batchNumber);
27523
+ } else {
27524
+ try {
27525
+ // push the JSON data into the batch array to be written later
27526
+ batch.push(data);
27527
+ countForBatch++;
27528
+ } catch (e) {
27529
+ reject(e);
27530
+ }
27531
+ }
27532
+ } catch (e) {
27533
+ reject(e);
27534
+ }
27528
27535
  });
27529
- })
27530
- .then((result) => {
27531
- writeStream.write("]");
27532
- win.webContents.send(
27533
- events$3.ALGOLIA_BROWSE_OBJECTS_COMPLETE,
27534
- result,
27535
- );
27536
- })
27537
- .catch((e) => {
27538
- win.webContents.send(events$3.ALGOLIA_BROWSE_OBJECTS_ERROR, e);
27536
+
27537
+ readStream.on("error", function (e) {
27538
+ console.log("batch on error ", e);
27539
+ reject(e);
27539
27540
  });
27540
- } else {
27541
- win.webContents.send(
27542
- events$3.ALGOLIA_BROWSE_OBJECTS_ERROR,
27543
- new Error("Missing parameters"),
27544
- );
27545
- }
27546
- } catch (e) {
27547
- win.webContents.send(events$3.ALGOLIA_BROWSE_OBJECTS_ERROR, {
27548
- error: e.message,
27549
- });
27550
- }
27551
- },
27552
27541
 
27553
- async partialUpdateObjectsFromDirectory(
27554
- win,
27555
- appId,
27556
- apiKey,
27557
- indexName,
27558
- dir,
27559
- createIfNotExists = false,
27560
- ) {
27561
- try {
27562
- const a = new AlgoliaIndex(appId, apiKey, indexName);
27563
- // now we can make the call to the utility and we are passing in the createIfNotExists FALSE by default
27564
- a.partialUpdateObjectsFromDirectorySync(
27565
- dir,
27566
- createIfNotExists,
27567
- (data) => {
27568
- win.webContents.send(
27569
- events$3.ALGOLIA_PARTIAL_UPDATE_OBJECTS_UPDATE,
27570
- data,
27571
- );
27572
- },
27573
- )
27574
- .then((result) => {
27575
- win.webContents.send(
27576
- events$3.ALGOLIA_PARTIAL_UPDATE_OBJECTS_COMPLETE,
27577
- result,
27578
- );
27542
+ readStream.on("close", function () {
27543
+ console.log("batch on close ");
27544
+ resolve("batches completed ", batchNumber);
27545
+ });
27579
27546
  })
27580
27547
  .catch((e) => {
27581
- win.webContents.send(events$3.ALGOLIA_PARTIAL_UPDATE_OBJECTS_ERROR, e);
27548
+ console.log("catch batch ", e.message);
27549
+ reject(e);
27582
27550
  });
27583
- } catch (e) {
27584
- win.webContents.send(events$3.ALGOLIA_PARTIAL_UPDATE_OBJECTS_ERROR, {
27585
- error: e.message,
27586
- });
27587
- }
27588
- },
27551
+ });
27552
+ };
27589
27553
 
27590
- /**
27591
- * createBatchesFromFile
27592
- * @param {*} win
27593
- * @param {*} filepath
27594
- * @param {*} batchFilepath
27595
- * @param {*} batchSize
27596
- * @param {*} callback
27597
- */
27598
- createBatchesFromFile: (
27599
- win,
27600
- filepath,
27601
- batchFilepath = "/data/batch",
27602
- batchSize = 500,
27603
- ) => {
27604
- try {
27605
- const a = new AlgoliaIndex();
27606
- a.createBatchesFromJSONFile(
27607
- filepath,
27608
- batchFilepath,
27609
- batchSize,
27610
- (data) => {
27611
- win.webContents.send(events$3.ALGOLIA_CREATE_BATCH_UPDATE, data);
27612
- },
27613
- )
27614
- .then((result) => {
27615
- win.webContents.send(events$3.ALGOLIA_CREATE_BATCH_COMPLETE, result);
27616
- })
27617
- .catch((e) => {
27618
- win.webContents.send(events$3.ALGOLIA_CREATE_BATCH_ERROR, e);
27554
+ clearDirectory = (directoryPath) => {
27555
+ return new Promise((resolve, reject) => {
27556
+ try {
27557
+ checkDirectory(directoryPath);
27558
+ fs$5.readdir(directoryPath, (err, files) => {
27559
+ if (err) reject(err);
27560
+ if (files) {
27561
+ files.forEach((file) => {
27562
+ fs$5.unlinkSync(path$9.join(directoryPath, file));
27563
+ });
27564
+ resolve();
27565
+ }
27619
27566
  });
27620
- } catch (e) {
27621
- win.webContents.send(events$3.ALGOLIA_CREATE_BATCH_ERROR, {
27622
- error: e.message,
27623
- });
27624
- }
27625
- },
27626
- /**
27627
- * search
27628
- * Search an index and return results in-memory (no file export).
27629
- * Returns the result directly for use with ipcMain.handle / ipcRenderer.invoke.
27630
- */
27631
- search: async (win, appId, apiKey, indexName, query = "", options = {}) => {
27567
+ } catch (e) {
27568
+ console.log("clear dir error ", e.message);
27569
+ reject(e);
27570
+ }
27571
+ });
27572
+ };
27573
+
27574
+ async partialUpdateObjectsFromDirectorySync(
27575
+ batchFilepath,
27576
+ createIfNotExists = false,
27577
+ callback = null,
27578
+ ) {
27632
27579
  try {
27633
- const a = new AlgoliaIndex(appId, apiKey, indexName);
27634
- return await a.search(query, options);
27580
+ // read the directory...
27581
+ const files = await fs$5.readdirSync(batchFilepath);
27582
+ let results = [];
27583
+ for (const fileIndex in files) {
27584
+ // for each file lets read the file and then push to algolia
27585
+ const pathToBatch = path$9.join(batchFilepath, files[fileIndex]);
27586
+ const fileContents = await this.readFile(pathToBatch);
27587
+ if (fileContents) {
27588
+ if ("data" in fileContents && "filepath" in fileContents) {
27589
+ // now we can update the index with the partial update
27590
+ const updateResult = await this.partialUpdateObjects(
27591
+ fileContents.data,
27592
+ fileContents.filepath,
27593
+ createIfNotExists,
27594
+ callback,
27595
+ );
27596
+ results.push({ file: files[fileIndex] });
27597
+ } else {
27598
+ console.log("missed ", files[fileIndex]);
27599
+ }
27600
+ }
27601
+ }
27602
+ return Promise.resolve(results);
27635
27603
  } catch (e) {
27636
- return { error: true, message: e.message || String(e) };
27604
+ return Promise.reject(e);
27637
27605
  }
27638
- },
27639
- };
27640
-
27641
- var algoliaController_1 = algoliaController$1;
27642
-
27643
- const OpenAI = require$$0$7;
27644
- const events$2 = events$8;
27606
+ }
27645
27607
 
27646
- const openaiController$1 = {
27647
- async describeImage(win, imageUrl, apiKey, prompt = "What's in this image?") {
27648
- try {
27649
- const openai = new OpenAI({
27650
- apiKey: apiKey,
27608
+ async readFile(filepath) {
27609
+ return await new Promise((resolve, reject) => {
27610
+ fs$5.readFile(filepath, "utf8", (err, data) => {
27611
+ if (err) {
27612
+ reject(err);
27613
+ }
27614
+ resolve({ data, filepath });
27651
27615
  });
27652
- const response = await openai.chat.completions.create({
27653
- model: "gpt-4-vision-preview",
27654
- messages: [
27655
- {
27656
- role: "user",
27657
- content: [
27658
- { type: "text", text: prompt },
27659
- {
27660
- type: "image_url",
27661
- image_url: imageUrl,
27616
+ });
27617
+ }
27618
+
27619
+ browseObjects = (query = "", callback = null) => {
27620
+ return new Promise((resolve, reject) => {
27621
+ try {
27622
+ if (this.index !== null) {
27623
+ // call algolia to update the objects
27624
+ this.index
27625
+ .browseObjects({
27626
+ query,
27627
+ batch: (hits) => {
27628
+ if (callback && typeof callback === "function") {
27629
+ callback(hits);
27630
+ }
27662
27631
  },
27663
- ],
27664
- },
27665
- ],
27666
- });
27632
+ })
27633
+ .then(() => {
27634
+ resolve({ success: true });
27635
+ })
27636
+ .catch((e) => reject(e));
27637
+ } else {
27638
+ reject("No index for client");
27639
+ }
27640
+ } catch (e) {
27641
+ console.log("browse objects ", e.message);
27642
+ reject(e);
27643
+ }
27644
+ });
27645
+ };
27667
27646
 
27668
- win.webContents.send(events$2.OPENAI_DESCRIBE_IMAGE_COMPLETE, {
27669
- succes: true,
27670
- imageUrl,
27671
- response,
27672
- });
27673
- } catch (e) {
27674
- win.webContents.send(events$2.OPENAI_DESCRIBE_IMAGE_ERROR, {
27675
- succes: true,
27676
- error: e.message,
27677
- });
27678
- }
27679
- },
27680
- };
27647
+ async partialUpdateObjects(
27648
+ objects,
27649
+ file,
27650
+ createIfNotExists = false,
27651
+ callback = null,
27652
+ ) {
27653
+ return new Promise((resolve, reject) => {
27654
+ try {
27655
+ if (objects) {
27656
+ const batch = JSON.parse(objects);
27681
27657
 
27682
- var openaiController_1 = openaiController$1;
27658
+ // callback function to pass batchnumber (or anything later on)
27659
+ if (callback && typeof callback === "function") {
27660
+ callback("indexing objects ", file, batch.length);
27661
+ }
27683
27662
 
27684
- const { app: app$4 } = require$$0$1;
27685
- const path$8 = require$$1$2;
27686
- const { writeFileSync } = require$$0$2;
27687
- const { getFileContents: getFileContents$2 } = file;
27663
+ if (this.index !== null) {
27664
+ // call algolia to update the objects
27665
+ this.index
27666
+ .partialUpdateObjects(batch, {
27667
+ createIfNotExists: createIfNotExists,
27668
+ })
27669
+ .then(({ objectIDs }) => {
27670
+ resolve({
27671
+ success: true,
27672
+ batchComplete: batch.length,
27673
+ objectIDs,
27674
+ });
27675
+ })
27676
+ .catch((e) => {
27677
+ console.log("Error partialUpdateObjects", e.message);
27678
+ reject(e);
27679
+ });
27680
+ } else {
27681
+ reject("No index for client");
27682
+ }
27683
+ }
27684
+ } catch (e) {
27685
+ console.log("partial update objects ", e.message);
27686
+ reject(e);
27687
+ }
27688
+ });
27689
+ }
27688
27690
 
27689
- const configFilename$1 = "menuItems.json";
27690
- const appName$2 = "Dashboard";
27691
+ search = (query = "", options = {}) => {
27692
+ return new Promise((resolve, reject) => {
27693
+ try {
27694
+ if (this.index !== null) {
27695
+ this.index
27696
+ .search(query, options)
27697
+ .then((result) => resolve(result))
27698
+ .catch((e) => reject(e));
27699
+ } else {
27700
+ reject("No index for client");
27701
+ }
27702
+ } catch (e) {
27703
+ reject(e);
27704
+ }
27705
+ });
27706
+ };
27691
27707
 
27692
- const menuItemsController$1 = {
27693
- saveMenuItemForApplication: (win, appId, menuItem) => {
27694
- try {
27695
- // filename to the pages file (live pages)
27696
- const filename = path$8.join(
27697
- app$4.getPath("userData"),
27698
- appName$2,
27699
- appId,
27700
- configFilename$1,
27701
- );
27702
- const menuItemsArray = getFileContents$2(filename);
27708
+ saveObjects = (objects, file, callback = null) => {
27709
+ return new Promise((resolve, reject) => {
27710
+ try {
27711
+ if (objects) {
27712
+ const batch = JSON.parse(objects);
27703
27713
 
27704
- menuItemsArray.filter((mi) => mi !== null);
27714
+ // callback function to pass batchnumber (or anything later on)
27715
+ if (callback && typeof callback === "function") {
27716
+ callback("saving objects ", file);
27717
+ }
27705
27718
 
27706
- // add the menuItems object to the file
27707
- menuItemsArray.push(menuItem);
27719
+ if (this.index !== null) {
27720
+ // call algolia to update the objects
27721
+ this.index
27722
+ .saveObjects(batch, {
27723
+ autoGenerateObjectIDIfNotExist: true,
27724
+ })
27725
+ .then(({ objectIDs }) => {
27726
+ resolve({
27727
+ success: true,
27728
+ batchComplete: batch.length,
27729
+ file,
27730
+ objectIDs,
27731
+ });
27732
+ })
27733
+ .catch((e) => reject(e));
27734
+ } else {
27735
+ reject("No index for client");
27736
+ }
27737
+ }
27738
+ } catch (e) {
27739
+ console.log("save objects error", e.message);
27740
+ reject(e);
27741
+ }
27742
+ });
27743
+ };
27744
+ };
27708
27745
 
27709
- // write the new pages configuration back to the file
27710
- writeFileSync(filename, JSON.stringify(menuItemsArray, null, 2));
27746
+ var algolia = AlgoliaIndex$1;
27711
27747
 
27712
- console.log("[menuItemsController] Menu item saved successfully");
27748
+ /**
27749
+ * algoliaController.js
27750
+ *
27751
+ * This is a sample controller that is called from the electron.js file
27752
+ *
27753
+ * The electron.js file contains listeners from the renderer that will call
27754
+ * the controller methods as seen below.
27755
+ */
27713
27756
 
27714
- // Return the data for ipcMain.handle() - modern promise-based approach
27715
- return {
27716
- menuItems: menuItemsArray,
27717
- success: true,
27718
- };
27719
- } catch (e) {
27720
- console.error("[menuItemsController] Error saving menu item:", e);
27721
- // Return error object with empty menu items array
27722
- return {
27723
- error: true,
27724
- message: e.message,
27725
- menuItems: [],
27726
- };
27727
- }
27728
- },
27757
+ const algoliasearch = require$$2$2;
27758
+ const events$3 = events$8;
27759
+ const AlgoliaIndex = algolia;
27760
+ var fs$4 = require$$0$2;
27729
27761
 
27730
- listMenuItemsForApplication: (win, appId) => {
27762
+ const algoliaController$1 = {
27763
+ /**
27764
+ * loadPagesForApplication
27765
+ * Load the pages for the application <userdata>/appId/pages.json
27766
+ * - filter out the indices that are "rule" indices
27767
+ *
27768
+ * @param {BrowserWindow} win the main window
27769
+ * @param {string} appId the application id from Algolia
27770
+ */
27771
+ listIndices: (win, application) => {
27731
27772
  try {
27732
- const filename = path$8.join(
27733
- app$4.getPath("userData"),
27734
- appName$2,
27735
- appId,
27736
- configFilename$1,
27773
+ const searchClient = algoliasearch(
27774
+ application["appId"],
27775
+ application["key"],
27737
27776
  );
27738
- const menuItemsArray = getFileContents$2(filename);
27739
- const filtered = menuItemsArray.filter((mi) => mi !== null);
27740
- // Return the data for ipcMain.handle() - modern promise-based approach
27741
- return {
27742
- menuItems: filtered,
27743
- };
27777
+ searchClient
27778
+ .listIndices()
27779
+ .then(({ items }) => {
27780
+ const filtered = items.filter(
27781
+ (item) => item.name.substring(0, 7) !== "sitehub",
27782
+ );
27783
+ win.webContents.send(events$3.ALGOLIA_LIST_INDICES_COMPLETE, filtered);
27784
+ })
27785
+ .catch((e) => {
27786
+ win.webContents.send(events$3.ALGOLIA_LIST_INDICES_ERROR, {
27787
+ error: e.message,
27788
+ });
27789
+ });
27744
27790
  } catch (e) {
27745
- console.error("[menuItemsController] Error listing menu items:", e);
27746
- // Return error object with empty menu items array
27747
- return {
27748
- error: true,
27749
- message: e.message,
27750
- menuItems: [],
27751
- };
27791
+ win.webContents.send(events$3.ALGOLIA_LIST_INDICES_ERROR, {
27792
+ error: e.message,
27793
+ });
27752
27794
  }
27753
27795
  },
27754
- };
27755
-
27756
- var menuItemsController_1 = menuItemsController$1;
27757
27796
 
27758
- const path$7 = require$$1$2;
27759
- const { app: app$3 } = require$$0$1;
27760
-
27761
- const pluginController$1 = {
27762
- install: (win, packageName, filepath) => {
27797
+ getAnalyticsForQuery: (win, application, indexName, query) => {
27763
27798
  try {
27764
- const rootPath = path$7.join(
27765
- app$3.getPath("userData"),
27766
- "plugins",
27767
- packageName,
27768
- );
27799
+ const baseUrl = "https://analytics.us.algolia.com";
27800
+ const headers = {
27801
+ "X-Algolia-Application-Id": application["appId"],
27802
+ "X-Algolia-API-Key": application["key"],
27803
+ };
27804
+ const url = `${baseUrl}/2/hits?search=${encodeURIComponent(
27805
+ query,
27806
+ )}&clickAnalytics=true&index=${indexName}`;
27807
+ axios
27808
+ .get(url, {
27809
+ headers: headers,
27810
+ })
27811
+ .then((resp) => {
27812
+ if (resp.status === 200) {
27813
+ win.webContents.send(events$3.ALGOLIA_ANALYTICS_FOR_QUERY_COMPLETE, {
27814
+ result: resp.data,
27815
+ indexName: indexName,
27816
+ query: query,
27817
+ });
27818
+ } else {
27819
+ win.webContents.send(events$3.ALGOLIA_ANALYTICS_FOR_QUERY_ERROR, {
27820
+ error: true,
27821
+ message: "Failed request",
27822
+ });
27823
+ }
27824
+ })
27825
+ .catch((e) => {
27826
+ win.webContents.send(events$3.ALGOLIA_ANALYTICS_FOR_QUERY_ERROR, {
27827
+ error: true,
27828
+ message: e.message,
27829
+ });
27830
+ });
27769
27831
  } catch (e) {
27770
- win.webContents.send("plugin-install-error", { error: e.message });
27771
- }
27772
- },
27773
- };
27774
-
27775
- var pluginController_1 = pluginController$1;
27776
-
27777
- /**
27778
- * cliController.js
27779
- *
27780
- * Manages Claude Code CLI (`claude -p`) as an alternative LLM backend.
27781
- * Spawns the CLI subprocess, parses stream-json NDJSON output, and emits
27782
- * the same LLM_STREAM_* events as the Anthropic SDK path.
27783
- *
27784
- * Users with a Claude Pro/Max subscription and Claude Code installed
27785
- * can use the Chat widget without a separate API key.
27786
- */
27787
-
27788
- const { spawn, execSync } = require$$6$1;
27789
- const {
27790
- LLM_STREAM_DELTA: LLM_STREAM_DELTA$2,
27791
- LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$2,
27792
- LLM_STREAM_TOOL_RESULT: LLM_STREAM_TOOL_RESULT$2,
27793
- LLM_STREAM_COMPLETE: LLM_STREAM_COMPLETE$2,
27794
- LLM_STREAM_ERROR: LLM_STREAM_ERROR$2,
27795
- } = llmEvents$1;
27796
-
27797
- /**
27798
- * Cached shell PATH result (resolved once, reused for all spawns).
27799
- * Same pattern as mcpController.js.
27800
- */
27801
- let _shellPath = null;
27802
-
27803
- function getShellPath() {
27804
- if (_shellPath !== null) return _shellPath;
27805
-
27806
- try {
27807
- const shell = process.env.SHELL || "/bin/bash";
27808
- _shellPath = execSync(`${shell} -ilc 'echo -n "$PATH"'`, {
27809
- encoding: "utf8",
27810
- timeout: 5000,
27811
- });
27812
- } catch {
27813
- _shellPath = process.env.PATH || "";
27814
- }
27815
-
27816
- return _shellPath;
27817
- }
27818
-
27819
- /**
27820
- * Cached CLI binary path (resolved once via `which claude`).
27821
- */
27822
- let _cliBinaryPath = undefined; // undefined = not yet checked
27823
-
27824
- function resolveCliBinary() {
27825
- if (_cliBinaryPath !== undefined) return _cliBinaryPath;
27826
-
27827
- try {
27828
- const fullPath = getShellPath();
27829
- _cliBinaryPath = execSync("which claude", {
27830
- encoding: "utf8",
27831
- timeout: 5000,
27832
- env: { ...process.env, PATH: fullPath },
27833
- }).trim();
27834
- } catch {
27835
- _cliBinaryPath = null;
27836
- }
27837
-
27838
- return _cliBinaryPath;
27839
- }
27840
-
27841
- /**
27842
- * Active CLI processes for abort support.
27843
- * Map<requestId, ChildProcess>
27844
- */
27845
- const activeProcesses = new Map();
27846
-
27847
- /**
27848
- * Session IDs for conversation continuity.
27849
- * Map<widgetUuid, sessionId>
27850
- */
27851
- const sessions = new Map();
27852
-
27853
- /**
27854
- * Send events safely to a window.
27855
- */
27856
- function safeSend(win, channel, data) {
27857
- if (win && !win.isDestroyed()) {
27858
- win.webContents.send(channel, data);
27859
- }
27860
- }
27861
-
27862
- const cliController$2 = {
27863
- /**
27864
- * isAvailable
27865
- * Check if the Claude Code CLI is installed and accessible.
27866
- *
27867
- * @returns {{ available: boolean, path?: string }}
27868
- */
27869
- isAvailable: () => {
27870
- const binaryPath = resolveCliBinary();
27871
- if (binaryPath) {
27872
- return { available: true, path: binaryPath };
27832
+ win.webContents.send(events$3.ALGOLIA_ANALYTICS_FOR_QUERY_ERROR, {
27833
+ error: true,
27834
+ message: e.message,
27835
+ });
27873
27836
  }
27874
- return { available: false };
27875
27837
  },
27876
27838
 
27877
27839
  /**
27878
- * sendMessage
27879
- * Stream a response from the Claude Code CLI with NDJSON parsing.
27840
+ * browseObjectsToFile
27841
+ * Lets try and browse an index and pull down the hits and save as a file
27880
27842
  *
27881
- * @param {BrowserWindow} win - the window to send stream events to
27882
- * @param {string} requestId - unique ID for this request
27883
- * @param {object} params - { model, messages, systemPrompt, maxToolRounds, widgetUuid }
27843
+ * @param {*} win
27844
+ * @param {*} appId
27845
+ * @param {*} apiKey
27846
+ * @param {*} indexName
27847
+ * @param {*} toFilename
27848
+ * @param {*} query
27884
27849
  */
27885
- sendMessage: async (win, requestId, params) => {
27886
- const { model, messages, systemPrompt, widgetUuid, cwd } = params;
27887
-
27888
- const binaryPath = resolveCliBinary();
27889
- if (!binaryPath) {
27890
- safeSend(win, LLM_STREAM_ERROR$2, {
27891
- requestId,
27892
- error:
27893
- "Claude Code CLI not found. Install from https://claude.ai/download",
27894
- code: "CLI_NOT_FOUND",
27895
- });
27896
- return;
27897
- }
27898
-
27899
- // Build CLI args
27900
- const args = ["-p", "--output-format", "stream-json", "--verbose"];
27901
-
27902
- if (model) {
27903
- args.push("--model", model);
27904
- }
27905
-
27906
- if (systemPrompt) {
27907
- args.push("--append-system-prompt", systemPrompt);
27908
- }
27850
+ browseObjectsToFile: (
27851
+ win,
27852
+ appId,
27853
+ apiKey,
27854
+ indexName,
27855
+ toFilename,
27856
+ query = "",
27857
+ ) => {
27858
+ try {
27859
+ if (
27860
+ toFilename !== "" &&
27861
+ apiKey !== "" &&
27862
+ indexName !== "" &&
27863
+ appId !== ""
27864
+ ) {
27865
+ // init the Algolia Index helper
27866
+ const a = new AlgoliaIndex(appId, apiKey, indexName);
27867
+ // create the write stream to store the hits
27868
+ const writeStream = fs$4.createWriteStream(toFilename);
27869
+ writeStream.write("[");
27909
27870
 
27910
- // Resume existing session for conversation continuity
27911
- const sessionId = widgetUuid ? sessions.get(widgetUuid) : null;
27912
- if (sessionId) {
27913
- args.push("--resume", sessionId);
27914
- }
27871
+ let sep = "";
27915
27872
 
27916
- // Extract the user message (last user message in the array)
27917
- const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
27918
- const userText =
27919
- typeof lastUserMsg?.content === "string"
27920
- ? lastUserMsg.content
27921
- : Array.isArray(lastUserMsg?.content)
27922
- ? lastUserMsg.content
27923
- .filter((b) => b.type === "text")
27924
- .map((b) => b.text)
27925
- .join("\n")
27926
- : "";
27873
+ // call the algolia browseObjects helper method
27874
+ a.browseObjects(query, (hits) => {
27875
+ win.webContents.send(events$3.ALGOLIA_BROWSE_OBJECTS_UPDATE, hits);
27927
27876
 
27928
- if (!userText) {
27929
- safeSend(win, LLM_STREAM_ERROR$2, {
27930
- requestId,
27931
- error: "No user message to send.",
27932
- code: "CLI_ERROR",
27877
+ let count = 0;
27878
+ // write to the file
27879
+ hits.forEach((hit) => {
27880
+ writeStream.write(sep + JSON.stringify(hit));
27881
+ count++;
27882
+ sep = ",\n";
27883
+ });
27884
+ })
27885
+ .then((result) => {
27886
+ writeStream.write("]");
27887
+ win.webContents.send(
27888
+ events$3.ALGOLIA_BROWSE_OBJECTS_COMPLETE,
27889
+ result,
27890
+ );
27891
+ })
27892
+ .catch((e) => {
27893
+ win.webContents.send(events$3.ALGOLIA_BROWSE_OBJECTS_ERROR, e);
27894
+ });
27895
+ } else {
27896
+ win.webContents.send(
27897
+ events$3.ALGOLIA_BROWSE_OBJECTS_ERROR,
27898
+ new Error("Missing parameters"),
27899
+ );
27900
+ }
27901
+ } catch (e) {
27902
+ win.webContents.send(events$3.ALGOLIA_BROWSE_OBJECTS_ERROR, {
27903
+ error: e.message,
27933
27904
  });
27934
- return;
27935
27905
  }
27906
+ },
27936
27907
 
27937
- try {
27938
- const fullPath = getShellPath();
27939
- const spawnOpts = {
27940
- env: { ...process.env, PATH: fullPath },
27941
- stdio: ["pipe", "pipe", "pipe"],
27942
- };
27943
- if (cwd) {
27944
- const fs = require("fs");
27945
- if (!fs.existsSync(cwd)) {
27946
- fs.mkdirSync(cwd, { recursive: true });
27947
- }
27948
- spawnOpts.cwd = cwd;
27949
- }
27950
- const child = spawn(binaryPath, args, spawnOpts);
27908
+ async partialUpdateObjectsFromDirectory(
27909
+ win,
27910
+ appId,
27911
+ apiKey,
27912
+ indexName,
27913
+ dir,
27914
+ createIfNotExists = false,
27915
+ ) {
27916
+ try {
27917
+ const a = new AlgoliaIndex(appId, apiKey, indexName);
27918
+ // now we can make the call to the utility and we are passing in the createIfNotExists FALSE by default
27919
+ a.partialUpdateObjectsFromDirectorySync(
27920
+ dir,
27921
+ createIfNotExists,
27922
+ (data) => {
27923
+ win.webContents.send(
27924
+ events$3.ALGOLIA_PARTIAL_UPDATE_OBJECTS_UPDATE,
27925
+ data,
27926
+ );
27927
+ },
27928
+ )
27929
+ .then((result) => {
27930
+ win.webContents.send(
27931
+ events$3.ALGOLIA_PARTIAL_UPDATE_OBJECTS_COMPLETE,
27932
+ result,
27933
+ );
27934
+ })
27935
+ .catch((e) => {
27936
+ win.webContents.send(events$3.ALGOLIA_PARTIAL_UPDATE_OBJECTS_ERROR, e);
27937
+ });
27938
+ } catch (e) {
27939
+ win.webContents.send(events$3.ALGOLIA_PARTIAL_UPDATE_OBJECTS_ERROR, {
27940
+ error: e.message,
27941
+ });
27942
+ }
27943
+ },
27951
27944
 
27952
- activeProcesses.set(requestId, child);
27945
+ /**
27946
+ * createBatchesFromFile
27947
+ * @param {*} win
27948
+ * @param {*} filepath
27949
+ * @param {*} batchFilepath
27950
+ * @param {*} batchSize
27951
+ * @param {*} callback
27952
+ */
27953
+ createBatchesFromFile: (
27954
+ win,
27955
+ filepath,
27956
+ batchFilepath = "/data/batch",
27957
+ batchSize = 500,
27958
+ ) => {
27959
+ try {
27960
+ const a = new AlgoliaIndex();
27961
+ a.createBatchesFromJSONFile(
27962
+ filepath,
27963
+ batchFilepath,
27964
+ batchSize,
27965
+ (data) => {
27966
+ win.webContents.send(events$3.ALGOLIA_CREATE_BATCH_UPDATE, data);
27967
+ },
27968
+ )
27969
+ .then((result) => {
27970
+ win.webContents.send(events$3.ALGOLIA_CREATE_BATCH_COMPLETE, result);
27971
+ })
27972
+ .catch((e) => {
27973
+ win.webContents.send(events$3.ALGOLIA_CREATE_BATCH_ERROR, e);
27974
+ });
27975
+ } catch (e) {
27976
+ win.webContents.send(events$3.ALGOLIA_CREATE_BATCH_ERROR, {
27977
+ error: e.message,
27978
+ });
27979
+ }
27980
+ },
27981
+ /**
27982
+ * search
27983
+ * Search an index and return results in-memory (no file export).
27984
+ * Returns the result directly for use with ipcMain.handle / ipcRenderer.invoke.
27985
+ */
27986
+ search: async (win, appId, apiKey, indexName, query = "", options = {}) => {
27987
+ try {
27988
+ const a = new AlgoliaIndex(appId, apiKey, indexName);
27989
+ return await a.search(query, options);
27990
+ } catch (e) {
27991
+ return { error: true, message: e.message || String(e) };
27992
+ }
27993
+ },
27994
+ };
27953
27995
 
27954
- // Pipe user message via stdin (not visible in ps)
27955
- child.stdin.write(userText);
27956
- child.stdin.end();
27996
+ var algoliaController_1 = algoliaController$1;
27957
27997
 
27958
- let stdoutBuffer = "";
27959
- let stderrBuffer = "";
27960
- let capturedSessionId = null;
27961
- let retried = false;
27998
+ const OpenAI = require$$0$7;
27999
+ const events$2 = events$8;
27962
28000
 
27963
- // Track active tool calls for mapping results
27964
- const activeToolCalls = new Map();
28001
+ const openaiController$1 = {
28002
+ async describeImage(win, imageUrl, apiKey, prompt = "What's in this image?") {
28003
+ try {
28004
+ const openai = new OpenAI({
28005
+ apiKey: apiKey,
28006
+ });
28007
+ const response = await openai.chat.completions.create({
28008
+ model: "gpt-4-vision-preview",
28009
+ messages: [
28010
+ {
28011
+ role: "user",
28012
+ content: [
28013
+ { type: "text", text: prompt },
28014
+ {
28015
+ type: "image_url",
28016
+ image_url: imageUrl,
28017
+ },
28018
+ ],
28019
+ },
28020
+ ],
28021
+ });
27965
28022
 
27966
- child.stdout.on("data", (chunk) => {
27967
- stdoutBuffer += chunk.toString();
28023
+ win.webContents.send(events$2.OPENAI_DESCRIBE_IMAGE_COMPLETE, {
28024
+ succes: true,
28025
+ imageUrl,
28026
+ response,
28027
+ });
28028
+ } catch (e) {
28029
+ win.webContents.send(events$2.OPENAI_DESCRIBE_IMAGE_ERROR, {
28030
+ succes: true,
28031
+ error: e.message,
28032
+ });
28033
+ }
28034
+ },
28035
+ };
27968
28036
 
27969
- // Process complete lines
27970
- const lines = stdoutBuffer.split("\n");
27971
- stdoutBuffer = lines.pop(); // keep incomplete line in buffer
28037
+ var openaiController_1 = openaiController$1;
27972
28038
 
27973
- for (const line of lines) {
27974
- if (!line.trim()) continue;
28039
+ const { app: app$4 } = require$$0$1;
28040
+ const path$8 = require$$1$2;
28041
+ const { writeFileSync } = require$$0$2;
28042
+ const { getFileContents: getFileContents$2 } = file;
27975
28043
 
27976
- let parsed;
27977
- try {
27978
- parsed = JSON.parse(line);
27979
- } catch {
27980
- console.warn("[cliController] Skipping invalid JSON line:", line);
27981
- continue;
27982
- }
28044
+ const configFilename$1 = "menuItems.json";
28045
+ const appName$2 = "Dashboard";
27983
28046
 
27984
- // Capture session ID from any message that has it
27985
- if (parsed.session_id && widgetUuid) {
27986
- capturedSessionId = parsed.session_id;
27987
- sessions.set(widgetUuid, capturedSessionId);
27988
- }
28047
+ const menuItemsController$1 = {
28048
+ saveMenuItemForApplication: (win, appId, menuItem) => {
28049
+ try {
28050
+ // filename to the pages file (live pages)
28051
+ const filename = path$8.join(
28052
+ app$4.getPath("userData"),
28053
+ appName$2,
28054
+ appId,
28055
+ configFilename$1,
28056
+ );
28057
+ const menuItemsArray = getFileContents$2(filename);
27989
28058
 
27990
- // Map CLI stream-json events to IPC events
27991
- if (parsed.type === "content_block_delta") {
27992
- if (parsed.delta?.type === "text_delta" && parsed.delta.text) {
27993
- safeSend(win, LLM_STREAM_DELTA$2, {
27994
- requestId,
27995
- text: parsed.delta.text,
27996
- });
27997
- } else if (parsed.delta?.type === "input_json_delta") {
27998
- // Update tool input incrementally
27999
- const tc = activeToolCalls.get(parsed.index);
28000
- if (tc) {
28001
- tc.partialInput =
28002
- (tc.partialInput || "") + (parsed.delta.partial_json || "");
28003
- }
28004
- }
28005
- } else if (parsed.type === "content_block_start") {
28006
- if (parsed.content_block?.type === "tool_use") {
28007
- const toolBlock = parsed.content_block;
28008
- activeToolCalls.set(parsed.index, {
28009
- toolUseId: toolBlock.id,
28010
- toolName: toolBlock.name,
28011
- partialInput: "",
28012
- });
28013
- safeSend(win, LLM_STREAM_TOOL_CALL$2, {
28014
- requestId,
28015
- toolUseId: toolBlock.id,
28016
- toolName: toolBlock.name,
28017
- serverName: "Claude Code",
28018
- input: toolBlock.input || {},
28019
- });
28020
- }
28021
- } else if (parsed.type === "content_block_stop") {
28022
- // Tool call completed — try to parse the accumulated input
28023
- const tc = activeToolCalls.get(parsed.index);
28024
- if (tc && tc.partialInput) {
28025
- try {
28026
- tc.finalInput = JSON.parse(tc.partialInput);
28027
- } catch {
28028
- tc.finalInput = tc.partialInput;
28029
- }
28030
- }
28031
- } else if (parsed.type === "message_stop") {
28032
- // Individual message completed (may be followed by more in tool-use loops)
28033
- } else if (parsed.type === "result") {
28034
- // Final result — conversation complete
28035
- const content = [];
28036
- if (parsed.result) {
28037
- content.push({ type: "text", text: parsed.result });
28038
- }
28059
+ menuItemsArray.filter((mi) => mi !== null);
28039
28060
 
28040
- safeSend(win, LLM_STREAM_COMPLETE$2, {
28041
- requestId,
28042
- content,
28043
- stopReason: parsed.stop_reason || "end_turn",
28044
- usage: parsed.usage || {},
28045
- });
28046
- }
28047
- }
28048
- });
28061
+ // add the menuItems object to the file
28062
+ menuItemsArray.push(menuItem);
28049
28063
 
28050
- child.stderr.on("data", (chunk) => {
28051
- stderrBuffer += chunk.toString();
28052
- });
28064
+ // write the new pages configuration back to the file
28065
+ writeFileSync(filename, JSON.stringify(menuItemsArray, null, 2));
28053
28066
 
28054
- child.on("error", (err) => {
28055
- activeProcesses.delete(requestId);
28056
- safeSend(win, LLM_STREAM_ERROR$2, {
28057
- requestId,
28058
- error: `Failed to start Claude CLI: ${err.message}`,
28059
- code: "CLI_SPAWN_ERROR",
28060
- });
28061
- });
28067
+ console.log("[menuItemsController] Menu item saved successfully");
28062
28068
 
28063
- child.on("close", (code) => {
28064
- activeProcesses.delete(requestId);
28069
+ // Return the data for ipcMain.handle() - modern promise-based approach
28070
+ return {
28071
+ menuItems: menuItemsArray,
28072
+ success: true,
28073
+ };
28074
+ } catch (e) {
28075
+ console.error("[menuItemsController] Error saving menu item:", e);
28076
+ // Return error object with empty menu items array
28077
+ return {
28078
+ error: true,
28079
+ message: e.message,
28080
+ menuItems: [],
28081
+ };
28082
+ }
28083
+ },
28065
28084
 
28066
- // Process any remaining buffer
28067
- if (stdoutBuffer.trim()) {
28068
- try {
28069
- const parsed = JSON.parse(stdoutBuffer);
28070
- if (parsed.session_id && widgetUuid) {
28071
- sessions.set(widgetUuid, parsed.session_id);
28072
- }
28073
- if (parsed.type === "result") {
28074
- const content = [];
28075
- if (parsed.result) {
28076
- content.push({ type: "text", text: parsed.result });
28077
- }
28078
- safeSend(win, LLM_STREAM_COMPLETE$2, {
28079
- requestId,
28080
- content,
28081
- stopReason: parsed.stop_reason || "end_turn",
28082
- usage: parsed.usage || {},
28083
- });
28084
- return;
28085
- }
28086
- } catch {
28087
- // ignore
28088
- }
28089
- }
28085
+ listMenuItemsForApplication: (win, appId) => {
28086
+ try {
28087
+ const filename = path$8.join(
28088
+ app$4.getPath("userData"),
28089
+ appName$2,
28090
+ appId,
28091
+ configFilename$1,
28092
+ );
28093
+ const menuItemsArray = getFileContents$2(filename);
28094
+ const filtered = menuItemsArray.filter((mi) => mi !== null);
28095
+ // Return the data for ipcMain.handle() - modern promise-based approach
28096
+ return {
28097
+ menuItems: filtered,
28098
+ };
28099
+ } catch (e) {
28100
+ console.error("[menuItemsController] Error listing menu items:", e);
28101
+ // Return error object with empty menu items array
28102
+ return {
28103
+ error: true,
28104
+ message: e.message,
28105
+ menuItems: [],
28106
+ };
28107
+ }
28108
+ },
28109
+ };
28090
28110
 
28091
- if (code !== 0 && code !== null) {
28092
- // Check if resume failed and retry without it
28093
- if (sessionId && !retried && stderrBuffer.includes("session")) {
28094
- retried = true;
28095
- if (widgetUuid) sessions.delete(widgetUuid);
28096
- // Retry without --resume
28097
- cliController$2.sendMessage(win, requestId, {
28098
- ...params,
28099
- _retryWithoutResume: true,
28100
- });
28101
- return;
28102
- }
28111
+ var menuItemsController_1 = menuItemsController$1;
28103
28112
 
28104
- // Check for auth errors
28105
- if (
28106
- stderrBuffer.includes("auth") ||
28107
- stderrBuffer.includes("login") ||
28108
- stderrBuffer.includes("not authenticated")
28109
- ) {
28110
- safeSend(win, LLM_STREAM_ERROR$2, {
28111
- requestId,
28112
- error:
28113
- "Claude Code CLI is not authenticated. Run `claude auth login` in your terminal.",
28114
- code: "CLI_AUTH_ERROR",
28115
- });
28116
- return;
28117
- }
28113
+ const path$7 = require$$1$2;
28114
+ const { app: app$3 } = require$$0$1;
28118
28115
 
28119
- safeSend(win, LLM_STREAM_ERROR$2, {
28120
- requestId,
28121
- error: `Claude CLI exited with code ${code}${stderrBuffer ? ": " + stderrBuffer.slice(0, 500) : ""}`,
28122
- code: "CLI_ERROR",
28123
- });
28124
- }
28125
- });
28126
- } catch (err) {
28127
- activeProcesses.delete(requestId);
28128
- safeSend(win, LLM_STREAM_ERROR$2, {
28129
- requestId,
28130
- error: `Failed to start Claude CLI: ${err.message}`,
28131
- code: "CLI_SPAWN_ERROR",
28132
- });
28116
+ const pluginController$1 = {
28117
+ install: (win, packageName, filepath) => {
28118
+ try {
28119
+ const rootPath = path$7.join(
28120
+ app$3.getPath("userData"),
28121
+ "plugins",
28122
+ packageName,
28123
+ );
28124
+ } catch (e) {
28125
+ win.webContents.send("plugin-install-error", { error: e.message });
28133
28126
  }
28134
28127
  },
28128
+ };
28129
+
28130
+ var pluginController_1 = pluginController$1;
28131
+
28132
+ /**
28133
+ * cliController.js
28134
+ *
28135
+ * Manages Claude Code CLI (`claude -p`) as an alternative LLM backend.
28136
+ * Spawns the CLI subprocess, parses stream-json NDJSON output, and emits
28137
+ * the same LLM_STREAM_* events as the Anthropic SDK path.
28138
+ *
28139
+ * Users with a Claude Pro/Max subscription and Claude Code installed
28140
+ * can use the Chat widget without a separate API key.
28141
+ */
28142
+
28143
+ const { spawn, execSync } = require$$6$1;
28144
+ const {
28145
+ LLM_STREAM_DELTA: LLM_STREAM_DELTA$2,
28146
+ LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$2,
28147
+ LLM_STREAM_TOOL_RESULT: LLM_STREAM_TOOL_RESULT$2,
28148
+ LLM_STREAM_COMPLETE: LLM_STREAM_COMPLETE$2,
28149
+ LLM_STREAM_ERROR: LLM_STREAM_ERROR$2,
28150
+ } = llmEvents$1;
28151
+
28152
+ /**
28153
+ * Cached shell PATH result (resolved once, reused for all spawns).
28154
+ * Same pattern as mcpController.js.
28155
+ */
28156
+ let _shellPath = null;
28157
+
28158
+ function getShellPath() {
28159
+ if (_shellPath !== null) return _shellPath;
28160
+
28161
+ try {
28162
+ const shell = process.env.SHELL || "/bin/bash";
28163
+ _shellPath = execSync(`${shell} -ilc 'echo -n "$PATH"'`, {
28164
+ encoding: "utf8",
28165
+ timeout: 5000,
28166
+ });
28167
+ } catch {
28168
+ _shellPath = process.env.PATH || "";
28169
+ }
28170
+
28171
+ return _shellPath;
28172
+ }
28173
+
28174
+ /**
28175
+ * Cached CLI binary path (resolved once via `which claude`).
28176
+ */
28177
+ let _cliBinaryPath = undefined; // undefined = not yet checked
28178
+
28179
+ function resolveCliBinary() {
28180
+ if (_cliBinaryPath !== undefined) return _cliBinaryPath;
28181
+
28182
+ try {
28183
+ const fullPath = getShellPath();
28184
+ _cliBinaryPath = execSync("which claude", {
28185
+ encoding: "utf8",
28186
+ timeout: 5000,
28187
+ env: { ...process.env, PATH: fullPath },
28188
+ }).trim();
28189
+ } catch {
28190
+ _cliBinaryPath = null;
28191
+ }
28192
+
28193
+ return _cliBinaryPath;
28194
+ }
28195
+
28196
+ /**
28197
+ * Active CLI processes for abort support.
28198
+ * Map<requestId, ChildProcess>
28199
+ */
28200
+ const activeProcesses = new Map();
28201
+
28202
+ /**
28203
+ * Session IDs for conversation continuity.
28204
+ * Map<widgetUuid, sessionId>
28205
+ */
28206
+ const sessions = new Map();
28207
+
28208
+ /**
28209
+ * Send events safely to a window.
28210
+ */
28211
+ function safeSend(win, channel, data) {
28212
+ if (win && !win.isDestroyed()) {
28213
+ win.webContents.send(channel, data);
28214
+ }
28215
+ }
28135
28216
 
28217
+ const cliController$2 = {
28136
28218
  /**
28137
- * abortRequest
28138
- * Kill an in-flight CLI process.
28219
+ * isAvailable
28220
+ * Check if the Claude Code CLI is installed and accessible.
28139
28221
  *
28140
- * @param {string} requestId - the request to cancel
28141
- * @returns {{ success: boolean }}
28222
+ * @returns {{ available: boolean, path?: string }}
28142
28223
  */
28143
- abortRequest: (requestId) => {
28144
- const child = activeProcesses.get(requestId);
28145
- if (child) {
28146
- child.kill("SIGTERM");
28147
- activeProcesses.delete(requestId);
28148
- return { success: true };
28224
+ isAvailable: () => {
28225
+ const binaryPath = resolveCliBinary();
28226
+ if (binaryPath) {
28227
+ return { available: true, path: binaryPath };
28149
28228
  }
28150
- return { success: false, message: "Request not found" };
28229
+ return { available: false };
28151
28230
  },
28152
28231
 
28153
28232
  /**
28154
- * clearSession
28155
- * Remove the stored session ID for a widget (called on "New Chat").
28233
+ * sendMessage
28234
+ * Stream a response from the Claude Code CLI with NDJSON parsing.
28156
28235
  *
28157
- * @param {string} widgetUuid - the widget whose session to clear
28158
- * @returns {{ success: boolean }}
28236
+ * @param {BrowserWindow} win - the window to send stream events to
28237
+ * @param {string} requestId - unique ID for this request
28238
+ * @param {object} params - { model, messages, systemPrompt, maxToolRounds, widgetUuid }
28159
28239
  */
28160
- clearSession: (widgetUuid) => {
28161
- if (widgetUuid && sessions.has(widgetUuid)) {
28162
- sessions.delete(widgetUuid);
28163
- return { success: true };
28240
+ sendMessage: async (win, requestId, params) => {
28241
+ const { model, messages, systemPrompt, widgetUuid, cwd } = params;
28242
+
28243
+ const binaryPath = resolveCliBinary();
28244
+ if (!binaryPath) {
28245
+ safeSend(win, LLM_STREAM_ERROR$2, {
28246
+ requestId,
28247
+ error:
28248
+ "Claude Code CLI not found. Install from https://claude.ai/download",
28249
+ code: "CLI_NOT_FOUND",
28250
+ });
28251
+ return;
28164
28252
  }
28165
- return { success: false };
28166
- },
28167
28253
 
28168
- /**
28254
+ // Build CLI args
28255
+ const args = ["-p", "--output-format", "stream-json", "--verbose"];
28256
+
28257
+ if (model) {
28258
+ args.push("--model", model);
28259
+ }
28260
+
28261
+ if (systemPrompt) {
28262
+ args.push("--append-system-prompt", systemPrompt);
28263
+ }
28264
+
28265
+ // Resume existing session for conversation continuity
28266
+ const sessionId = widgetUuid ? sessions.get(widgetUuid) : null;
28267
+ if (sessionId) {
28268
+ args.push("--resume", sessionId);
28269
+ }
28270
+
28271
+ // Extract the user message (last user message in the array)
28272
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
28273
+ const userText =
28274
+ typeof lastUserMsg?.content === "string"
28275
+ ? lastUserMsg.content
28276
+ : Array.isArray(lastUserMsg?.content)
28277
+ ? lastUserMsg.content
28278
+ .filter((b) => b.type === "text")
28279
+ .map((b) => b.text)
28280
+ .join("\n")
28281
+ : "";
28282
+
28283
+ if (!userText) {
28284
+ safeSend(win, LLM_STREAM_ERROR$2, {
28285
+ requestId,
28286
+ error: "No user message to send.",
28287
+ code: "CLI_ERROR",
28288
+ });
28289
+ return;
28290
+ }
28291
+
28292
+ try {
28293
+ const fullPath = getShellPath();
28294
+ const spawnOpts = {
28295
+ env: { ...process.env, PATH: fullPath },
28296
+ stdio: ["pipe", "pipe", "pipe"],
28297
+ };
28298
+ if (cwd) {
28299
+ const fs = require("fs");
28300
+ if (!fs.existsSync(cwd)) {
28301
+ fs.mkdirSync(cwd, { recursive: true });
28302
+ }
28303
+ spawnOpts.cwd = cwd;
28304
+ }
28305
+ const child = spawn(binaryPath, args, spawnOpts);
28306
+
28307
+ activeProcesses.set(requestId, child);
28308
+
28309
+ // Pipe user message via stdin (not visible in ps)
28310
+ child.stdin.write(userText);
28311
+ child.stdin.end();
28312
+
28313
+ let stdoutBuffer = "";
28314
+ let stderrBuffer = "";
28315
+ let capturedSessionId = null;
28316
+ let retried = false;
28317
+
28318
+ // Track active tool calls for mapping results
28319
+ const activeToolCalls = new Map();
28320
+
28321
+ child.stdout.on("data", (chunk) => {
28322
+ stdoutBuffer += chunk.toString();
28323
+
28324
+ // Process complete lines
28325
+ const lines = stdoutBuffer.split("\n");
28326
+ stdoutBuffer = lines.pop(); // keep incomplete line in buffer
28327
+
28328
+ for (const line of lines) {
28329
+ if (!line.trim()) continue;
28330
+
28331
+ let parsed;
28332
+ try {
28333
+ parsed = JSON.parse(line);
28334
+ } catch {
28335
+ console.warn("[cliController] Skipping invalid JSON line:", line);
28336
+ continue;
28337
+ }
28338
+
28339
+ // Capture session ID from any message that has it
28340
+ if (parsed.session_id && widgetUuid) {
28341
+ capturedSessionId = parsed.session_id;
28342
+ sessions.set(widgetUuid, capturedSessionId);
28343
+ }
28344
+
28345
+ // Map CLI stream-json events to IPC events
28346
+ if (parsed.type === "content_block_delta") {
28347
+ if (parsed.delta?.type === "text_delta" && parsed.delta.text) {
28348
+ safeSend(win, LLM_STREAM_DELTA$2, {
28349
+ requestId,
28350
+ text: parsed.delta.text,
28351
+ });
28352
+ } else if (parsed.delta?.type === "input_json_delta") {
28353
+ // Update tool input incrementally
28354
+ const tc = activeToolCalls.get(parsed.index);
28355
+ if (tc) {
28356
+ tc.partialInput =
28357
+ (tc.partialInput || "") + (parsed.delta.partial_json || "");
28358
+ }
28359
+ }
28360
+ } else if (parsed.type === "content_block_start") {
28361
+ if (parsed.content_block?.type === "tool_use") {
28362
+ const toolBlock = parsed.content_block;
28363
+ activeToolCalls.set(parsed.index, {
28364
+ toolUseId: toolBlock.id,
28365
+ toolName: toolBlock.name,
28366
+ partialInput: "",
28367
+ });
28368
+ safeSend(win, LLM_STREAM_TOOL_CALL$2, {
28369
+ requestId,
28370
+ toolUseId: toolBlock.id,
28371
+ toolName: toolBlock.name,
28372
+ serverName: "Claude Code",
28373
+ input: toolBlock.input || {},
28374
+ });
28375
+ }
28376
+ } else if (parsed.type === "content_block_stop") {
28377
+ // Tool call completed — try to parse the accumulated input
28378
+ const tc = activeToolCalls.get(parsed.index);
28379
+ if (tc && tc.partialInput) {
28380
+ try {
28381
+ tc.finalInput = JSON.parse(tc.partialInput);
28382
+ } catch {
28383
+ tc.finalInput = tc.partialInput;
28384
+ }
28385
+ }
28386
+ } else if (parsed.type === "message_stop") {
28387
+ // Individual message completed (may be followed by more in tool-use loops)
28388
+ } else if (parsed.type === "result") {
28389
+ // Final result — conversation complete
28390
+ const content = [];
28391
+ if (parsed.result) {
28392
+ content.push({ type: "text", text: parsed.result });
28393
+ }
28394
+
28395
+ safeSend(win, LLM_STREAM_COMPLETE$2, {
28396
+ requestId,
28397
+ content,
28398
+ stopReason: parsed.stop_reason || "end_turn",
28399
+ usage: parsed.usage || {},
28400
+ });
28401
+ }
28402
+ }
28403
+ });
28404
+
28405
+ child.stderr.on("data", (chunk) => {
28406
+ stderrBuffer += chunk.toString();
28407
+ });
28408
+
28409
+ child.on("error", (err) => {
28410
+ activeProcesses.delete(requestId);
28411
+ safeSend(win, LLM_STREAM_ERROR$2, {
28412
+ requestId,
28413
+ error: `Failed to start Claude CLI: ${err.message}`,
28414
+ code: "CLI_SPAWN_ERROR",
28415
+ });
28416
+ });
28417
+
28418
+ child.on("close", (code) => {
28419
+ activeProcesses.delete(requestId);
28420
+
28421
+ // Process any remaining buffer
28422
+ if (stdoutBuffer.trim()) {
28423
+ try {
28424
+ const parsed = JSON.parse(stdoutBuffer);
28425
+ if (parsed.session_id && widgetUuid) {
28426
+ sessions.set(widgetUuid, parsed.session_id);
28427
+ }
28428
+ if (parsed.type === "result") {
28429
+ const content = [];
28430
+ if (parsed.result) {
28431
+ content.push({ type: "text", text: parsed.result });
28432
+ }
28433
+ safeSend(win, LLM_STREAM_COMPLETE$2, {
28434
+ requestId,
28435
+ content,
28436
+ stopReason: parsed.stop_reason || "end_turn",
28437
+ usage: parsed.usage || {},
28438
+ });
28439
+ return;
28440
+ }
28441
+ } catch {
28442
+ // ignore
28443
+ }
28444
+ }
28445
+
28446
+ if (code !== 0 && code !== null) {
28447
+ // Check if resume failed and retry without it
28448
+ if (sessionId && !retried && stderrBuffer.includes("session")) {
28449
+ retried = true;
28450
+ if (widgetUuid) sessions.delete(widgetUuid);
28451
+ // Retry without --resume
28452
+ cliController$2.sendMessage(win, requestId, {
28453
+ ...params,
28454
+ _retryWithoutResume: true,
28455
+ });
28456
+ return;
28457
+ }
28458
+
28459
+ // Check for auth errors
28460
+ if (
28461
+ stderrBuffer.includes("auth") ||
28462
+ stderrBuffer.includes("login") ||
28463
+ stderrBuffer.includes("not authenticated")
28464
+ ) {
28465
+ safeSend(win, LLM_STREAM_ERROR$2, {
28466
+ requestId,
28467
+ error:
28468
+ "Claude Code CLI is not authenticated. Run `claude auth login` in your terminal.",
28469
+ code: "CLI_AUTH_ERROR",
28470
+ });
28471
+ return;
28472
+ }
28473
+
28474
+ safeSend(win, LLM_STREAM_ERROR$2, {
28475
+ requestId,
28476
+ error: `Claude CLI exited with code ${code}${stderrBuffer ? ": " + stderrBuffer.slice(0, 500) : ""}`,
28477
+ code: "CLI_ERROR",
28478
+ });
28479
+ }
28480
+ });
28481
+ } catch (err) {
28482
+ activeProcesses.delete(requestId);
28483
+ safeSend(win, LLM_STREAM_ERROR$2, {
28484
+ requestId,
28485
+ error: `Failed to start Claude CLI: ${err.message}`,
28486
+ code: "CLI_SPAWN_ERROR",
28487
+ });
28488
+ }
28489
+ },
28490
+
28491
+ /**
28492
+ * abortRequest
28493
+ * Kill an in-flight CLI process.
28494
+ *
28495
+ * @param {string} requestId - the request to cancel
28496
+ * @returns {{ success: boolean }}
28497
+ */
28498
+ abortRequest: (requestId) => {
28499
+ const child = activeProcesses.get(requestId);
28500
+ if (child) {
28501
+ child.kill("SIGTERM");
28502
+ activeProcesses.delete(requestId);
28503
+ return { success: true };
28504
+ }
28505
+ return { success: false, message: "Request not found" };
28506
+ },
28507
+
28508
+ /**
28509
+ * clearSession
28510
+ * Remove the stored session ID for a widget (called on "New Chat").
28511
+ *
28512
+ * @param {string} widgetUuid - the widget whose session to clear
28513
+ * @returns {{ success: boolean }}
28514
+ */
28515
+ clearSession: (widgetUuid) => {
28516
+ if (widgetUuid && sessions.has(widgetUuid)) {
28517
+ sessions.delete(widgetUuid);
28518
+ return { success: true };
28519
+ }
28520
+ return { success: false };
28521
+ },
28522
+
28523
+ /**
28169
28524
  * getSessionStatus
28170
28525
  * Check if a CLI session exists and whether a process is active for a widget.
28171
28526
  *
@@ -48637,1013 +48992,685 @@ let StreamableHTTPServerTransport$1 = class StreamableHTTPServerTransport {
48637
48992
  const handler = (0, node_server_1.getRequestListener)(async (webRequest) => {
48638
48993
  return this._webStandardTransport.handleRequest(webRequest, {
48639
48994
  authInfo,
48640
- parsedBody
48641
- });
48642
- }, { overrideGlobalObjects: false });
48643
- // Delegate to the request listener which handles all the Node.js <-> Web Standard conversion
48644
- // including proper SSE streaming support
48645
- await handler(req, res);
48646
- }
48647
- /**
48648
- * Close an SSE stream for a specific request, triggering client reconnection.
48649
- * Use this to implement polling behavior during long-running operations -
48650
- * client will reconnect after the retry interval specified in the priming event.
48651
- */
48652
- closeSSEStream(requestId) {
48653
- this._webStandardTransport.closeSSEStream(requestId);
48654
- }
48655
- /**
48656
- * Close the standalone GET SSE stream, triggering client reconnection.
48657
- * Use this to implement polling behavior for server-initiated notifications.
48658
- */
48659
- closeStandaloneSSEStream() {
48660
- this._webStandardTransport.closeStandaloneSSEStream();
48661
- }
48662
- };
48663
- streamableHttp.StreamableHTTPServerTransport = StreamableHTTPServerTransport$1;
48664
-
48665
- /**
48666
- * tlsCert.js
48667
- *
48668
- * Generates and caches a self-signed TLS certificate for the MCP Dash Server.
48669
- * Uses node-forge (already in the dependency tree) to create a cert valid for
48670
- * 127.0.0.1 and localhost, stored in the app's userData directory.
48671
- *
48672
- * Usage:
48673
- * const { getOrCreateCert } = require('./tlsCert');
48674
- * const { cert, key } = getOrCreateCert(certsDir);
48675
- * https.createServer({ key, cert }, handler);
48676
- */
48677
-
48678
- const fs$3 = require$$0$2;
48679
- const path$6 = require$$1$2;
48680
- const forge = require$$2$3;
48681
-
48682
- /**
48683
- * Get or create a self-signed TLS certificate for localhost.
48684
- * @param {string} certsDir - Directory to store cert.pem and key.pem
48685
- * @returns {{ cert: string, key: string }} PEM-encoded certificate and private key
48686
- */
48687
- function getOrCreateCert$1(certsDir) {
48688
- const certPath = path$6.join(certsDir, "cert.pem");
48689
- const keyPath = path$6.join(certsDir, "key.pem");
48690
-
48691
- // Return existing cert if valid
48692
- if (fs$3.existsSync(certPath) && fs$3.existsSync(keyPath)) {
48693
- try {
48694
- const cert = fs$3.readFileSync(certPath, "utf8");
48695
- const key = fs$3.readFileSync(keyPath, "utf8");
48696
- // Verify cert is not expired
48697
- const parsed = forge.pki.certificateFromPem(cert);
48698
- if (parsed.validity.notAfter > new Date()) {
48699
- return { cert, key };
48700
- }
48701
- console.log("[tlsCert] Existing certificate expired, regenerating...");
48702
- } catch (e) {
48703
- console.log("[tlsCert] Existing certificate invalid, regenerating...");
48704
- }
48705
- }
48706
-
48707
- console.log("[tlsCert] Generating self-signed certificate for localhost...");
48708
-
48709
- // Generate 2048-bit RSA key pair
48710
- const keys = forge.pki.rsa.generateKeyPair(2048);
48711
-
48712
- // Create certificate
48713
- const cert = forge.pki.createCertificate();
48714
- cert.publicKey = keys.publicKey;
48715
- cert.serialNumber = "01";
48716
-
48717
- // Valid for 10 years
48718
- cert.validity.notBefore = new Date();
48719
- cert.validity.notAfter = new Date();
48720
- cert.validity.notAfter.setFullYear(
48721
- cert.validity.notBefore.getFullYear() + 10,
48722
- );
48723
-
48724
- // Subject and issuer (self-signed)
48725
- const attrs = [
48726
- { name: "commonName", value: "Dash MCP Server" },
48727
- { name: "organizationName", value: "Dash" },
48728
- ];
48729
- cert.setSubject(attrs);
48730
- cert.setIssuer(attrs);
48731
-
48732
- // Subject Alternative Names (SAN) — required for modern TLS clients
48733
- cert.setExtensions([
48734
- { name: "basicConstraints", cA: false },
48735
- {
48736
- name: "keyUsage",
48737
- digitalSignature: true,
48738
- keyEncipherment: true,
48739
- },
48740
- {
48741
- name: "extKeyUsage",
48742
- serverAuth: true,
48743
- },
48744
- {
48745
- name: "subjectAltName",
48746
- altNames: [
48747
- { type: 7, ip: "127.0.0.1" }, // IP SAN
48748
- { type: 2, value: "localhost" }, // DNS SAN
48749
- ],
48750
- },
48751
- ]);
48752
-
48753
- // Self-sign with SHA-256
48754
- cert.sign(keys.privateKey, forge.md.sha256.create());
48755
-
48756
- // Convert to PEM
48757
- const certPem = forge.pki.certificateToPem(cert);
48758
- const keyPem = forge.pki.privateKeyToPem(keys.privateKey);
48759
-
48760
- // Write to disk
48761
- fs$3.mkdirSync(certsDir, { recursive: true });
48762
- fs$3.writeFileSync(certPath, certPem, { mode: 0o644 });
48763
- fs$3.writeFileSync(keyPath, keyPem, { mode: 0o600 });
48764
-
48765
- console.log(`[tlsCert] Certificate saved to ${certsDir}`);
48766
-
48767
- return { cert: certPem, key: keyPem };
48768
- }
48769
-
48770
- var tlsCert = { getOrCreateCert: getOrCreateCert$1 };
48771
-
48772
- /**
48773
- * jsonSchemaToZod.js
48774
- *
48775
- * Converts JSON Schema objects to Zod v3 schemas.
48776
- * Used by the MCP Dash server to satisfy the MCP SDK's requirement
48777
- * for Zod schemas in tool input validation (safeParseAsync).
48778
- */
48779
-
48780
- const z$1 = zod;
48781
-
48782
- /**
48783
- * Convert a JSON Schema property definition to a Zod v3 schema.
48784
- * Handles: string (+ enum), number, boolean, object (recursive), array.
48785
- */
48786
- function jsonSchemaPropertyToZod(prop) {
48787
- if (!prop || typeof prop !== "object") return z$1.any();
48788
-
48789
- let schema;
48790
-
48791
- switch (prop.type) {
48792
- case "string":
48793
- if (Array.isArray(prop.enum) && prop.enum.length > 0) {
48794
- schema = z$1.enum(prop.enum);
48795
- } else {
48796
- schema = z$1.string();
48797
- }
48798
- break;
48799
- case "number":
48800
- schema = z$1.number();
48801
- break;
48802
- case "boolean":
48803
- schema = z$1.boolean();
48804
- break;
48805
- case "array":
48806
- schema = z$1.array(
48807
- prop.items ? jsonSchemaPropertyToZod(prop.items) : z$1.any(),
48808
- );
48809
- break;
48810
- case "object":
48811
- if (prop.properties && Object.keys(prop.properties).length > 0) {
48812
- schema = jsonSchemaToZod$1(prop);
48813
- } else {
48814
- // Generic object with no known properties (e.g. config, credentials)
48815
- schema = z$1.object({}).passthrough();
48816
- }
48817
- break;
48818
- default:
48819
- schema = z$1.any();
48820
- }
48821
-
48822
- if (prop.description) {
48823
- schema = schema.describe(prop.description);
48824
- }
48825
-
48826
- return schema;
48827
- }
48828
-
48829
- /**
48830
- * Convert a top-level JSON Schema inputSchema to a Zod v3 object schema.
48831
- * The MCP SDK requires Zod schemas for input validation (safeParseAsync).
48832
- */
48833
- function jsonSchemaToZod$1(schema) {
48834
- if (!schema || schema.type !== "object") {
48835
- return z$1.object({});
48836
- }
48837
-
48838
- const properties = schema.properties || {};
48839
- const required = Array.isArray(schema.required) ? schema.required : [];
48840
- const shape = {};
48841
-
48842
- for (const [key, prop] of Object.entries(properties)) {
48843
- let fieldSchema = jsonSchemaPropertyToZod(prop);
48844
- if (!required.includes(key)) {
48845
- fieldSchema = fieldSchema.optional();
48995
+ parsedBody
48996
+ });
48997
+ }, { overrideGlobalObjects: false });
48998
+ // Delegate to the request listener which handles all the Node.js <-> Web Standard conversion
48999
+ // including proper SSE streaming support
49000
+ await handler(req, res);
48846
49001
  }
48847
- shape[key] = fieldSchema;
48848
- }
48849
-
48850
- return z$1.object(shape);
48851
- }
48852
-
48853
- var jsonSchemaToZod_1 = { jsonSchemaToZod: jsonSchemaToZod$1, jsonSchemaPropertyToZod };
49002
+ /**
49003
+ * Close an SSE stream for a specific request, triggering client reconnection.
49004
+ * Use this to implement polling behavior during long-running operations -
49005
+ * client will reconnect after the retry interval specified in the priming event.
49006
+ */
49007
+ closeSSEStream(requestId) {
49008
+ this._webStandardTransport.closeSSEStream(requestId);
49009
+ }
49010
+ /**
49011
+ * Close the standalone GET SSE stream, triggering client reconnection.
49012
+ * Use this to implement polling behavior for server-initiated notifications.
49013
+ */
49014
+ closeStandaloneSSEStream() {
49015
+ this._webStandardTransport.closeStandaloneSSEStream();
49016
+ }
49017
+ };
49018
+ streamableHttp.StreamableHTTPServerTransport = StreamableHTTPServerTransport$1;
48854
49019
 
48855
49020
  /**
48856
- * mcpDashServerController.js
48857
- *
48858
- * Manages the hosted MCP server that exposes Dash capabilities to external
48859
- * LLM clients (Claude Desktop, ChatGPT, etc.) via Streamable HTTP transport.
49021
+ * tlsCert.js
48860
49022
  *
48861
- * This is the MCP *server* distinct from mcpController.js which is the
48862
- * MCP *client* that connects to external tool servers for widgets.
49023
+ * Generates and caches a self-signed TLS certificate for the MCP Dash Server.
49024
+ * Uses node-forge (already in the dependency tree) to create a cert valid for
49025
+ * 127.0.0.1 and localhost, stored in the app's userData directory.
48863
49026
  *
48864
- * Architecture:
48865
- * - Node https server bound to 127.0.0.1 (localhost only)
48866
- * - Auto-generated self-signed TLS certificate for localhost
48867
- * - StreamableHTTPServerTransport from @modelcontextprotocol/sdk
48868
- * - McpServer registers tools and resources
48869
- * - Bearer token authentication on all requests
48870
- * - Rate limiting via token bucket (60 req/min)
48871
- */
48872
-
48873
- const https$2 = require$$8$1;
48874
- const { randomUUID } = require$$1$5;
48875
- const { McpServer } = mcp;
48876
- const {
48877
- StreamableHTTPServerTransport,
48878
- } = streamableHttp;
48879
-
48880
- const settingsController$3 = settingsController_1;
48881
- const { getOrCreateCert } = tlsCert;
48882
-
48883
- // --- State ---
48884
- let mcpServer = null;
48885
- let httpsServer = null;
48886
- let transport = null;
48887
- let startTime = null;
48888
- let connectionCount = 0;
48889
- let activeWin = null;
48890
-
48891
- // --- Rate Limiting ---
48892
- const RATE_LIMIT = 60; // requests per minute
48893
- const RATE_WINDOW = 60 * 1000; // 1 minute in ms
48894
- const rateBuckets$1 = new Map(); // ip -> { count, resetAt }
48895
-
48896
- function isRateLimited$1(ip) {
48897
- const now = Date.now();
48898
- let bucket = rateBuckets$1.get(ip);
48899
- if (!bucket || now > bucket.resetAt) {
48900
- bucket = { count: 0, resetAt: now + RATE_WINDOW };
48901
- rateBuckets$1.set(ip, bucket);
48902
- }
48903
- bucket.count++;
48904
- return bucket.count > RATE_LIMIT;
48905
- }
48906
-
48907
- // Clean up stale buckets periodically
48908
- let cleanupInterval = null;
48909
- function startCleanup() {
48910
- if (cleanupInterval) return;
48911
- cleanupInterval = setInterval(() => {
48912
- const now = Date.now();
48913
- for (const [ip, bucket] of rateBuckets$1) {
48914
- if (now > bucket.resetAt) rateBuckets$1.delete(ip);
48915
- }
48916
- }, RATE_WINDOW);
48917
- }
48918
- function stopCleanup() {
48919
- if (cleanupInterval) {
48920
- clearInterval(cleanupInterval);
48921
- cleanupInterval = null;
48922
- }
48923
- rateBuckets$1.clear();
48924
- }
48925
-
48926
- // --- Tool, Resource & Prompt Registration ---
48927
- // These are populated by other modules (DASH-78, DASH-79, etc.)
48928
- // Each entry: { name, description, inputSchema, handler }
48929
- const registeredTools = [];
48930
- const registeredResources = [];
48931
- // Each entry: { name, description, args, handler }
48932
- const registeredPrompts = [];
48933
-
48934
- /**
48935
- * Register a tool to be exposed via the MCP server.
48936
- * Call this before starting the server (or restart after registering).
48937
- */
48938
- function registerTool$6(toolDef) {
48939
- registeredTools.push(toolDef);
48940
- }
48941
-
48942
- /**
48943
- * Register a resource to be exposed via the MCP server.
48944
- */
48945
- function registerResource$1(resourceDef) {
48946
- registeredResources.push(resourceDef);
48947
- }
48948
-
48949
- /**
48950
- * Register a prompt to be exposed via the MCP server.
48951
- * Prompts are guided entry points that LLM clients display as suggested actions.
49027
+ * Usage:
49028
+ * const { getOrCreateCert } = require('./tlsCert');
49029
+ * const { cert, key } = getOrCreateCert(certsDir);
49030
+ * https.createServer({ key, cert }, handler);
48952
49031
  */
48953
- function registerPrompt$1(promptDef) {
48954
- registeredPrompts.push(promptDef);
48955
- }
48956
49032
 
48957
- const z = zod;
48958
- const { jsonSchemaToZod } = jsonSchemaToZod_1;
49033
+ const fs$3 = require$$0$2;
49034
+ const path$6 = require$$1$2;
49035
+ const forge = require$$2$3;
48959
49036
 
48960
49037
  /**
48961
- * Apply all registered tools, resources, and prompts to the McpServer instance.
49038
+ * Get or create a self-signed TLS certificate for localhost.
49039
+ * @param {string} certsDir - Directory to store cert.pem and key.pem
49040
+ * @returns {{ cert: string, key: string }} PEM-encoded certificate and private key
48962
49041
  */
48963
- function applyRegistrations(server) {
48964
- for (const tool of registeredTools) {
48965
- const zodSchema = jsonSchemaToZod(tool.inputSchema);
48966
- // server.tool() expects a raw Zod shape (e.g. { name: z.string() }),
48967
- // NOT a z.object() wrapper. Extract .shape from the Zod object.
48968
- server.tool(
48969
- tool.name,
48970
- tool.description,
48971
- zodSchema.shape || {},
48972
- tool.handler,
48973
- );
48974
- }
48975
- for (const resource of registeredResources) {
48976
- server.resource(
48977
- resource.name,
48978
- resource.uri,
48979
- resource.metadata || {},
48980
- resource.handler,
48981
- );
48982
- }
48983
- for (const prompt of registeredPrompts) {
48984
- if (prompt.args && Object.keys(prompt.args).length > 0) {
48985
- // Prompt with arguments — use the 4-arg overload
48986
- // Build a Zod-compatible arg schema from our plain arg definitions
48987
- const shape = {};
48988
- for (const [key, def] of Object.entries(prompt.args)) {
48989
- shape[key] = def.required
48990
- ? z.string().describe(def.description)
48991
- : z.string().optional().describe(def.description);
48992
- }
48993
- server.prompt(prompt.name, prompt.description, shape, prompt.handler);
48994
- } else {
48995
- // Prompt with no arguments — use the 2-arg overload
48996
- server.prompt(prompt.name, prompt.description, prompt.handler);
48997
- }
48998
- }
48999
- }
49000
-
49001
- // --- Settings Helpers ---
49002
- function getMcpServerSettings(win) {
49003
- const result = settingsController$3.getSettingsForApplication(win);
49004
- const settings = result?.settings || {};
49005
- return settings.mcpDashServer || {};
49006
- }
49007
-
49008
- function saveMcpServerSettings(win, mcpSettings) {
49009
- const result = settingsController$3.getSettingsForApplication(win);
49010
- const settings = result?.settings || {};
49011
- settings.mcpDashServer = mcpSettings;
49012
- settingsController$3.saveSettingsForApplication(win, settings);
49013
- }
49042
+ function getOrCreateCert$1(certsDir) {
49043
+ const certPath = path$6.join(certsDir, "cert.pem");
49044
+ const keyPath = path$6.join(certsDir, "key.pem");
49014
49045
 
49015
- // --- App ID Resolution ---
49016
- /**
49017
- * Resolve the appId by scanning the userData/Dashboard directory for
49018
- * subdirectories containing workspaces.json. Falls back to the default.
49019
- */
49020
- function resolveAppId() {
49021
- const { app } = require$$0$1;
49022
- const fs = require$$0$2;
49023
- const path = require$$1$2;
49024
- const dashboardDir = path.join(app.getPath("userData"), "Dashboard");
49025
- try {
49026
- const entries = fs.readdirSync(dashboardDir, { withFileTypes: true });
49027
- for (const entry of entries) {
49028
- if (entry.isDirectory()) {
49029
- const wsFile = path.join(dashboardDir, entry.name, "workspaces.json");
49030
- if (fs.existsSync(wsFile)) {
49031
- return entry.name;
49032
- }
49046
+ // Return existing cert if valid
49047
+ if (fs$3.existsSync(certPath) && fs$3.existsSync(keyPath)) {
49048
+ try {
49049
+ const cert = fs$3.readFileSync(certPath, "utf8");
49050
+ const key = fs$3.readFileSync(keyPath, "utf8");
49051
+ // Verify cert is not expired
49052
+ const parsed = forge.pki.certificateFromPem(cert);
49053
+ if (parsed.validity.notAfter > new Date()) {
49054
+ return { cert, key };
49033
49055
  }
49056
+ console.log("[tlsCert] Existing certificate expired, regenerating...");
49057
+ } catch (e) {
49058
+ console.log("[tlsCert] Existing certificate invalid, regenerating...");
49034
49059
  }
49035
- } catch (e) {
49036
- // Directory may not exist yet
49037
- }
49038
- return "@trops/dash-electron";
49039
- }
49040
-
49041
- /**
49042
- * Get the current server context (win + appId) for tool handlers.
49043
- * Returns null if the server is not running.
49044
- */
49045
- function getServerContext() {
49046
- if (!activeWin) return null;
49047
- return { win: activeWin, appId: resolveAppId() };
49048
- }
49049
-
49050
- // --- Controller ---
49051
- const mcpDashServerController$4 = {
49052
- /**
49053
- * Start the MCP Dash server.
49054
- * @param {BrowserWindow} win
49055
- * @param {Object} options - { port?: number }
49056
- */
49057
- startServer: async (win, options = {}) => {
49058
- if (httpsServer) {
49059
- return {
49060
- success: false,
49061
- error: "Server is already running",
49062
- };
49063
- }
49060
+ }
49064
49061
 
49065
- try {
49066
- const serverSettings = getMcpServerSettings(win);
49067
- const port = options.port || serverSettings.port || 3141;
49068
- const token =
49069
- serverSettings.token || mcpDashServerController$4.getOrCreateToken(win);
49062
+ console.log("[tlsCert] Generating self-signed certificate for localhost...");
49070
49063
 
49071
- // Create McpServer
49072
- mcpServer = new McpServer({
49073
- name: "dash-electron",
49074
- version: "1.0.0",
49075
- });
49064
+ // Generate 2048-bit RSA key pair
49065
+ const keys = forge.pki.rsa.generateKeyPair(2048);
49076
49066
 
49077
- // Generate or load TLS certificate
49078
- const { app } = require("electron");
49079
- const path = require("path");
49080
- const certsDir = path.join(app.getPath("userData"), "certs");
49081
- const tlsCert = getOrCreateCert(certsDir);
49067
+ // Create certificate
49068
+ const cert = forge.pki.createCertificate();
49069
+ cert.publicKey = keys.publicKey;
49070
+ cert.serialNumber = "01";
49082
49071
 
49083
- // Apply registered tools and resources
49084
- applyRegistrations(mcpServer);
49072
+ // Valid for 10 years
49073
+ cert.validity.notBefore = new Date();
49074
+ cert.validity.notAfter = new Date();
49075
+ cert.validity.notAfter.setFullYear(
49076
+ cert.validity.notBefore.getFullYear() + 10,
49077
+ );
49085
49078
 
49086
- // Create HTTPS server with auth and rate limiting
49087
- httpsServer = https$2.createServer(
49088
- { key: tlsCert.key, cert: tlsCert.cert },
49089
- async (req, res) => {
49090
- const ip = req.socket.remoteAddress || req.connection.remoteAddress;
49079
+ // Subject and issuer (self-signed)
49080
+ const attrs = [
49081
+ { name: "commonName", value: "Dash MCP Server" },
49082
+ { name: "organizationName", value: "Dash" },
49083
+ ];
49084
+ cert.setSubject(attrs);
49085
+ cert.setIssuer(attrs);
49091
49086
 
49092
- // Rate limiting
49093
- if (isRateLimited$1(ip)) {
49094
- res.writeHead(429, { "Content-Type": "application/json" });
49095
- res.end(JSON.stringify({ error: "Rate limit exceeded" }));
49096
- return;
49097
- }
49087
+ // Subject Alternative Names (SAN) — required for modern TLS clients
49088
+ cert.setExtensions([
49089
+ { name: "basicConstraints", cA: false },
49090
+ {
49091
+ name: "keyUsage",
49092
+ digitalSignature: true,
49093
+ keyEncipherment: true,
49094
+ },
49095
+ {
49096
+ name: "extKeyUsage",
49097
+ serverAuth: true,
49098
+ },
49099
+ {
49100
+ name: "subjectAltName",
49101
+ altNames: [
49102
+ { type: 7, ip: "127.0.0.1" }, // IP SAN
49103
+ { type: 2, value: "localhost" }, // DNS SAN
49104
+ ],
49105
+ },
49106
+ ]);
49098
49107
 
49099
- // Bearer token auth
49100
- const authHeader = req.headers.authorization;
49101
- if (!authHeader || authHeader !== `Bearer ${token}`) {
49102
- res.writeHead(401, { "Content-Type": "application/json" });
49103
- res.end(JSON.stringify({ error: "Unauthorized" }));
49104
- return;
49105
- }
49108
+ // Self-sign with SHA-256
49109
+ cert.sign(keys.privateKey, forge.md.sha256.create());
49106
49110
 
49107
- // Handle MCP requests on /mcp path
49108
- if (req.url === "/mcp" || req.url?.startsWith("/mcp")) {
49109
- try {
49110
- // Stateless mode: create a fresh server + transport per request
49111
- const reqServer = new McpServer({
49112
- name: "dash-electron",
49113
- version: "1.0.0",
49114
- });
49115
- applyRegistrations(reqServer);
49116
- const reqTransport = new StreamableHTTPServerTransport({
49117
- sessionIdGenerator: undefined,
49118
- });
49119
- await reqServer.connect(reqTransport);
49120
- connectionCount++;
49121
- await reqTransport.handleRequest(req, res);
49122
- } catch (err) {
49123
- console.error("[mcpDashServer] Error handling MCP request:", err);
49124
- if (!res.headersSent) {
49125
- res.writeHead(500, {
49126
- "Content-Type": "application/json",
49127
- });
49128
- res.end(
49129
- JSON.stringify({
49130
- error: "Internal server error",
49131
- }),
49132
- );
49133
- }
49134
- }
49135
- } else {
49136
- // Health check endpoint
49137
- if (req.url === "/health" && req.method === "GET") {
49138
- res.writeHead(200, {
49139
- "Content-Type": "application/json",
49140
- });
49141
- res.end(
49142
- JSON.stringify({
49143
- status: "ok",
49144
- server: "dash-electron-mcp",
49145
- version: "1.0.0",
49146
- }),
49147
- );
49148
- return;
49149
- }
49150
- res.writeHead(404, { "Content-Type": "application/json" });
49151
- res.end(JSON.stringify({ error: "Not found" }));
49152
- }
49153
- },
49154
- );
49111
+ // Convert to PEM
49112
+ const certPem = forge.pki.certificateToPem(cert);
49113
+ const keyPem = forge.pki.privateKeyToPem(keys.privateKey);
49155
49114
 
49156
- // Bind to localhost only
49157
- await new Promise((resolve, reject) => {
49158
- httpsServer.on("error", (err) => {
49159
- httpsServer = null;
49160
- mcpServer = null;
49161
- if (err.code === "EADDRINUSE") {
49162
- reject(
49163
- new Error(
49164
- `Port ${port} is already in use. Choose a different port in Settings.`,
49165
- ),
49166
- );
49167
- } else {
49168
- reject(err);
49169
- }
49170
- });
49171
- httpsServer.listen(port, "127.0.0.1", () => {
49172
- resolve();
49173
- });
49174
- });
49115
+ // Write to disk
49116
+ fs$3.mkdirSync(certsDir, { recursive: true });
49117
+ fs$3.writeFileSync(certPath, certPem, { mode: 0o644 });
49118
+ fs$3.writeFileSync(keyPath, keyPem, { mode: 0o600 });
49175
49119
 
49176
- startTime = Date.now();
49177
- connectionCount = 0;
49178
- activeWin = win;
49179
- startCleanup();
49120
+ console.log(`[tlsCert] Certificate saved to ${certsDir}`);
49180
49121
 
49181
- // Save enabled state
49182
- saveMcpServerSettings(win, {
49183
- ...serverSettings,
49184
- enabled: true,
49185
- port,
49186
- token,
49187
- });
49122
+ return { cert: certPem, key: keyPem };
49123
+ }
49188
49124
 
49189
- console.log(
49190
- `[mcpDashServer] Server started on https://127.0.0.1:${port}/mcp`,
49191
- );
49125
+ var tlsCert = { getOrCreateCert: getOrCreateCert$1 };
49192
49126
 
49193
- return {
49194
- success: true,
49195
- port,
49196
- url: `https://127.0.0.1:${port}/mcp`,
49197
- };
49198
- } catch (err) {
49199
- console.error("[mcpDashServer] Failed to start server:", err);
49200
- httpsServer = null;
49201
- mcpServer = null;
49202
- return {
49203
- success: false,
49204
- error: err.message,
49205
- };
49206
- }
49207
- },
49127
+ /**
49128
+ * jsonSchemaToZod.js
49129
+ *
49130
+ * Converts JSON Schema objects to Zod v3 schemas.
49131
+ * Used by the MCP Dash server to satisfy the MCP SDK's requirement
49132
+ * for Zod schemas in tool input validation (safeParseAsync).
49133
+ */
49208
49134
 
49209
- /**
49210
- * Stop the MCP Dash server.
49211
- */
49212
- stopServer: async (win) => {
49213
- if (!httpsServer) {
49214
- return { success: true, message: "Server was not running" };
49215
- }
49135
+ const z$1 = zod;
49216
49136
 
49217
- try {
49218
- stopCleanup();
49137
+ /**
49138
+ * Convert a JSON Schema property definition to a Zod v3 schema.
49139
+ * Handles: string (+ enum), number, boolean, object (recursive), array.
49140
+ */
49141
+ function jsonSchemaPropertyToZod(prop) {
49142
+ if (!prop || typeof prop !== "object") return z$1.any();
49219
49143
 
49220
- await new Promise((resolve) => {
49221
- httpsServer.close(() => resolve());
49222
- // Force close after 5 seconds
49223
- setTimeout(() => resolve(), 5000);
49224
- });
49144
+ let schema;
49225
49145
 
49226
- if (mcpServer) {
49227
- try {
49228
- await mcpServer.close();
49229
- } catch (e) {
49230
- // Ignore close errors
49231
- }
49146
+ switch (prop.type) {
49147
+ case "string":
49148
+ if (Array.isArray(prop.enum) && prop.enum.length > 0) {
49149
+ schema = z$1.enum(prop.enum);
49150
+ } else {
49151
+ schema = z$1.string();
49232
49152
  }
49233
-
49234
- httpsServer = null;
49235
- mcpServer = null;
49236
- transport = null;
49237
- startTime = null;
49238
- connectionCount = 0;
49239
- activeWin = null;
49240
-
49241
- // Update settings
49242
- if (win) {
49243
- const serverSettings = getMcpServerSettings(win);
49244
- saveMcpServerSettings(win, {
49245
- ...serverSettings,
49246
- enabled: false,
49247
- });
49153
+ break;
49154
+ case "number":
49155
+ schema = z$1.number();
49156
+ break;
49157
+ case "boolean":
49158
+ schema = z$1.boolean();
49159
+ break;
49160
+ case "array":
49161
+ schema = z$1.array(
49162
+ prop.items ? jsonSchemaPropertyToZod(prop.items) : z$1.any(),
49163
+ );
49164
+ break;
49165
+ case "object":
49166
+ if (prop.properties && Object.keys(prop.properties).length > 0) {
49167
+ schema = jsonSchemaToZod$1(prop);
49168
+ } else {
49169
+ // Generic object with no known properties (e.g. config, credentials)
49170
+ schema = z$1.object({}).passthrough();
49248
49171
  }
49172
+ break;
49173
+ default:
49174
+ schema = z$1.any();
49175
+ }
49249
49176
 
49250
- console.log("[mcpDashServer] Server stopped");
49251
- return { success: true };
49252
- } catch (err) {
49253
- console.error("[mcpDashServer] Error stopping server:", err);
49254
- return { success: false, error: err.message };
49255
- }
49256
- },
49177
+ if (prop.description) {
49178
+ schema = schema.describe(prop.description);
49179
+ }
49257
49180
 
49258
- /**
49259
- * Restart the server (stop + start).
49260
- */
49261
- restartServer: async (win, options = {}) => {
49262
- await mcpDashServerController$4.stopServer(win);
49263
- return mcpDashServerController$4.startServer(win, options);
49264
- },
49181
+ return schema;
49182
+ }
49265
49183
 
49266
- /**
49267
- * Get server status.
49268
- */
49269
- getStatus: (win) => {
49270
- const serverSettings = getMcpServerSettings(win);
49271
- return {
49272
- running: !!httpsServer,
49273
- enabled: serverSettings.enabled || false,
49274
- port: serverSettings.port || 3141,
49275
- connectionCount,
49276
- uptime: startTime ? Math.floor((Date.now() - startTime) / 1000) : 0,
49277
- toolCount: registeredTools.length,
49278
- resourceCount: registeredResources.length,
49279
- };
49280
- },
49184
+ /**
49185
+ * Convert a top-level JSON Schema inputSchema to a Zod v3 object schema.
49186
+ * The MCP SDK requires Zod schemas for input validation (safeParseAsync).
49187
+ */
49188
+ function jsonSchemaToZod$1(schema) {
49189
+ if (!schema || schema.type !== "object") {
49190
+ return z$1.object({});
49191
+ }
49281
49192
 
49282
- /**
49283
- * Get or create the bearer token.
49284
- */
49285
- getOrCreateToken: (win) => {
49286
- const serverSettings = getMcpServerSettings(win);
49287
- if (serverSettings.token) {
49288
- return serverSettings.token;
49289
- }
49290
- const token = randomUUID();
49291
- saveMcpServerSettings(win, { ...serverSettings, token });
49292
- return token;
49293
- },
49193
+ const properties = schema.properties || {};
49194
+ const required = Array.isArray(schema.required) ? schema.required : [];
49195
+ const shape = {};
49294
49196
 
49295
- /**
49296
- * Auto-start server if enabled in settings.
49297
- * Called from dash-electron on app ready.
49298
- */
49299
- autoStart: async (win) => {
49300
- const serverSettings = getMcpServerSettings(win);
49301
- if (serverSettings.enabled) {
49302
- console.log("[mcpDashServer] Auto-starting server...");
49303
- return mcpDashServerController$4.startServer(win, {
49304
- port: serverSettings.port,
49305
- });
49197
+ for (const [key, prop] of Object.entries(properties)) {
49198
+ let fieldSchema = jsonSchemaPropertyToZod(prop);
49199
+ if (!required.includes(key)) {
49200
+ fieldSchema = fieldSchema.optional();
49306
49201
  }
49307
- return { success: false, message: "Server not enabled" };
49308
- },
49202
+ shape[key] = fieldSchema;
49203
+ }
49309
49204
 
49310
- // Expose registration functions for other controllers
49311
- registerTool: registerTool$6,
49312
- registerResource: registerResource$1,
49313
- registerPrompt: registerPrompt$1,
49314
- getServerContext,
49315
- };
49205
+ return z$1.object(shape);
49206
+ }
49316
49207
 
49317
- var mcpDashServerController_1 = mcpDashServerController$4;
49208
+ var jsonSchemaToZod_1 = { jsonSchemaToZod: jsonSchemaToZod$1, jsonSchemaPropertyToZod };
49318
49209
 
49319
49210
  /**
49320
- * registryAuthController.js
49211
+ * mcpDashServerController.js
49321
49212
  *
49322
- * Manages authentication with the Dash registry service.
49323
- * Uses OAuth device code flow for desktop app authentication.
49213
+ * Manages the hosted MCP server that exposes Dash capabilities to external
49214
+ * LLM clients (Claude Desktop, ChatGPT, etc.) via Streamable HTTP transport.
49324
49215
  *
49325
- * Flow:
49326
- * 1. App calls initiateDeviceFlow() gets device code + verification URL
49327
- * 2. User opens verification URL in browser, signs in, enters code
49328
- * 3. App polls pollForToken() until authorized
49329
- * 4. Token stored securely via electron-store (encrypted)
49216
+ * This is the MCP *server* — distinct from mcpController.js which is the
49217
+ * MCP *client* that connects to external tool servers for widgets.
49218
+ *
49219
+ * Architecture:
49220
+ * - Node https server bound to 127.0.0.1 (localhost only)
49221
+ * - Auto-generated self-signed TLS certificate for localhost
49222
+ * - StreamableHTTPServerTransport from @modelcontextprotocol/sdk
49223
+ * - McpServer registers tools and resources
49224
+ * - Bearer token authentication on all requests
49225
+ * - Rate limiting via token bucket (60 req/min)
49330
49226
  */
49331
49227
 
49332
- const REGISTRY_BASE_URL$1 =
49333
- process.env.DASH_REGISTRY_API_URL ||
49334
- "https://main.d919rwhuzp7rj.amplifyapp.com";
49228
+ const https$2 = require$$8$1;
49229
+ const { randomUUID } = require$$1$5;
49230
+ const { McpServer } = mcp;
49231
+ const {
49232
+ StreamableHTTPServerTransport,
49233
+ } = streamableHttp;
49335
49234
 
49336
- // Lazy-load electron-store to avoid issues when not installed
49337
- let store$3 = null;
49338
- function getStore$1() {
49339
- if (!store$3) {
49340
- const Store = require$$1$1;
49341
- store$3 = new Store({
49342
- name: "dash-registry-auth",
49343
- encryptionKey: "dash-registry-v1",
49344
- });
49235
+ const settingsController$3 = settingsController_1;
49236
+ const { getOrCreateCert } = tlsCert;
49237
+
49238
+ // --- State ---
49239
+ let mcpServer = null;
49240
+ let httpsServer = null;
49241
+ let transport = null;
49242
+ let startTime = null;
49243
+ let connectionCount = 0;
49244
+ let activeWin = null;
49245
+
49246
+ // --- Rate Limiting ---
49247
+ const RATE_LIMIT = 60; // requests per minute
49248
+ const RATE_WINDOW = 60 * 1000; // 1 minute in ms
49249
+ const rateBuckets$1 = new Map(); // ip -> { count, resetAt }
49250
+
49251
+ function isRateLimited$1(ip) {
49252
+ const now = Date.now();
49253
+ let bucket = rateBuckets$1.get(ip);
49254
+ if (!bucket || now > bucket.resetAt) {
49255
+ bucket = { count: 0, resetAt: now + RATE_WINDOW };
49256
+ rateBuckets$1.set(ip, bucket);
49345
49257
  }
49346
- return store$3;
49258
+ bucket.count++;
49259
+ return bucket.count > RATE_LIMIT;
49347
49260
  }
49348
49261
 
49349
- /**
49350
- * Initiate the OAuth device code flow.
49351
- * Returns the device code, user code, and verification URL.
49352
- *
49353
- * @returns {Promise<Object>} { deviceCode, userCode, verificationUrl, verificationUrlComplete, expiresIn, interval }
49354
- */
49355
- async function initiateDeviceFlow$1() {
49356
- const response = await fetch(`${REGISTRY_BASE_URL$1}/api/auth/device`, {
49357
- method: "POST",
49358
- headers: { "Content-Type": "application/json" },
49359
- });
49360
-
49361
- if (!response.ok) {
49362
- throw new Error(`Device flow initiation failed: ${response.status}`);
49262
+ // Clean up stale buckets periodically
49263
+ let cleanupInterval = null;
49264
+ function startCleanup() {
49265
+ if (cleanupInterval) return;
49266
+ cleanupInterval = setInterval(() => {
49267
+ const now = Date.now();
49268
+ for (const [ip, bucket] of rateBuckets$1) {
49269
+ if (now > bucket.resetAt) rateBuckets$1.delete(ip);
49270
+ }
49271
+ }, RATE_WINDOW);
49272
+ }
49273
+ function stopCleanup() {
49274
+ if (cleanupInterval) {
49275
+ clearInterval(cleanupInterval);
49276
+ cleanupInterval = null;
49363
49277
  }
49278
+ rateBuckets$1.clear();
49279
+ }
49364
49280
 
49365
- const data = await response.json();
49281
+ // --- Tool, Resource & Prompt Registration ---
49282
+ // These are populated by other modules (DASH-78, DASH-79, etc.)
49283
+ // Each entry: { name, description, inputSchema, handler }
49284
+ const registeredTools = [];
49285
+ const registeredResources = [];
49286
+ // Each entry: { name, description, args, handler }
49287
+ const registeredPrompts = [];
49366
49288
 
49367
- return {
49368
- deviceCode: data.device_code,
49369
- userCode: data.user_code,
49370
- verificationUrl: data.verification_uri,
49371
- verificationUrlComplete: data.verification_uri_complete,
49372
- expiresIn: data.expires_in,
49373
- interval: data.interval,
49374
- };
49289
+ /**
49290
+ * Register a tool to be exposed via the MCP server.
49291
+ * Call this before starting the server (or restart after registering).
49292
+ */
49293
+ function registerTool$6(toolDef) {
49294
+ registeredTools.push(toolDef);
49375
49295
  }
49376
49296
 
49377
49297
  /**
49378
- * Poll the registry for token after user completes browser auth.
49379
- *
49380
- * @param {string} deviceCode - The device code from initiateDeviceFlow()
49381
- * @returns {Promise<Object>} { status: 'pending' | 'authorized' | 'expired', token?, userId? }
49298
+ * Register a resource to be exposed via the MCP server.
49382
49299
  */
49383
- async function pollForToken$1(deviceCode) {
49384
- const response = await fetch(
49385
- `${REGISTRY_BASE_URL$1}/api/auth/device?device_code=${encodeURIComponent(deviceCode)}`,
49386
- );
49300
+ function registerResource$1(resourceDef) {
49301
+ registeredResources.push(resourceDef);
49302
+ }
49387
49303
 
49388
- if (response.status === 428) {
49389
- return { status: "pending" };
49304
+ /**
49305
+ * Register a prompt to be exposed via the MCP server.
49306
+ * Prompts are guided entry points that LLM clients display as suggested actions.
49307
+ */
49308
+ function registerPrompt$1(promptDef) {
49309
+ registeredPrompts.push(promptDef);
49310
+ }
49311
+
49312
+ const z = zod;
49313
+ const { jsonSchemaToZod } = jsonSchemaToZod_1;
49314
+
49315
+ /**
49316
+ * Apply all registered tools, resources, and prompts to the McpServer instance.
49317
+ */
49318
+ function applyRegistrations(server) {
49319
+ for (const tool of registeredTools) {
49320
+ const zodSchema = jsonSchemaToZod(tool.inputSchema);
49321
+ // server.tool() expects a raw Zod shape (e.g. { name: z.string() }),
49322
+ // NOT a z.object() wrapper. Extract .shape from the Zod object.
49323
+ server.tool(
49324
+ tool.name,
49325
+ tool.description,
49326
+ zodSchema.shape || {},
49327
+ tool.handler,
49328
+ );
49329
+ }
49330
+ for (const resource of registeredResources) {
49331
+ server.resource(
49332
+ resource.name,
49333
+ resource.uri,
49334
+ resource.metadata || {},
49335
+ resource.handler,
49336
+ );
49390
49337
  }
49391
-
49392
- if (response.status === 400) {
49393
- const data = await response.json();
49394
- if (data.error === "expired_token") {
49395
- return { status: "expired" };
49338
+ for (const prompt of registeredPrompts) {
49339
+ if (prompt.args && Object.keys(prompt.args).length > 0) {
49340
+ // Prompt with arguments — use the 4-arg overload
49341
+ // Build a Zod-compatible arg schema from our plain arg definitions
49342
+ const shape = {};
49343
+ for (const [key, def] of Object.entries(prompt.args)) {
49344
+ shape[key] = def.required
49345
+ ? z.string().describe(def.description)
49346
+ : z.string().optional().describe(def.description);
49347
+ }
49348
+ server.prompt(prompt.name, prompt.description, shape, prompt.handler);
49349
+ } else {
49350
+ // Prompt with no arguments — use the 2-arg overload
49351
+ server.prompt(prompt.name, prompt.description, prompt.handler);
49396
49352
  }
49397
- return { status: "pending" };
49398
49353
  }
49354
+ }
49399
49355
 
49400
- if (response.ok) {
49401
- const data = await response.json();
49402
-
49403
- // Store the token securely
49404
- const s = getStore$1();
49405
- s.set("accessToken", data.access_token);
49406
- s.set("userId", data.user_id);
49407
- s.set("tokenType", data.token_type);
49408
- s.set("authenticatedAt", new Date().toISOString());
49409
-
49410
- return {
49411
- status: "authorized",
49412
- token: data.access_token,
49413
- userId: data.user_id,
49414
- };
49415
- }
49356
+ // --- Settings Helpers ---
49357
+ function getMcpServerSettings(win) {
49358
+ const result = settingsController$3.getSettingsForApplication(win);
49359
+ const settings = result?.settings || {};
49360
+ return settings.mcpDashServer || {};
49361
+ }
49416
49362
 
49417
- throw new Error(`Unexpected response: ${response.status}`);
49363
+ function saveMcpServerSettings(win, mcpSettings) {
49364
+ const result = settingsController$3.getSettingsForApplication(win);
49365
+ const settings = result?.settings || {};
49366
+ settings.mcpDashServer = mcpSettings;
49367
+ settingsController$3.saveSettingsForApplication(win, settings);
49418
49368
  }
49419
49369
 
49370
+ // --- App ID Resolution ---
49420
49371
  /**
49421
- * Get the stored auth token.
49422
- *
49423
- * @returns {Object|null} { token, userId, authenticatedAt } or null if not authenticated
49372
+ * Resolve the appId by scanning the userData/Dashboard directory for
49373
+ * subdirectories containing workspaces.json. Falls back to the default.
49424
49374
  */
49425
- function getStoredToken$3() {
49375
+ function resolveAppId() {
49376
+ const { app } = require$$0$1;
49377
+ const fs = require$$0$2;
49378
+ const path = require$$1$2;
49379
+ const dashboardDir = path.join(app.getPath("userData"), "Dashboard");
49426
49380
  try {
49427
- const s = getStore$1();
49428
- const token = s.get("accessToken");
49429
- if (!token) return null;
49430
-
49431
- return {
49432
- token,
49433
- userId: s.get("userId"),
49434
- authenticatedAt: s.get("authenticatedAt"),
49435
- };
49436
- } catch {
49437
- return null;
49381
+ const entries = fs.readdirSync(dashboardDir, { withFileTypes: true });
49382
+ for (const entry of entries) {
49383
+ if (entry.isDirectory()) {
49384
+ const wsFile = path.join(dashboardDir, entry.name, "workspaces.json");
49385
+ if (fs.existsSync(wsFile)) {
49386
+ return entry.name;
49387
+ }
49388
+ }
49389
+ }
49390
+ } catch (e) {
49391
+ // Directory may not exist yet
49438
49392
  }
49393
+ return "@trops/dash-electron";
49439
49394
  }
49440
49395
 
49441
49396
  /**
49442
- * Check if the user is authenticated with the registry.
49443
- *
49444
- * @returns {Object} { authenticated: boolean, userId?: string }
49397
+ * Get the current server context (win + appId) for tool handlers.
49398
+ * Returns null if the server is not running.
49445
49399
  */
49446
- function getAuthStatus$1() {
49447
- const stored = getStoredToken$3();
49448
- if (!stored) {
49449
- return { authenticated: false };
49450
- }
49451
-
49452
- return {
49453
- authenticated: true,
49454
- userId: stored.userId,
49455
- authenticatedAt: stored.authenticatedAt,
49456
- };
49400
+ function getServerContext() {
49401
+ if (!activeWin) return null;
49402
+ return { win: activeWin, appId: resolveAppId() };
49457
49403
  }
49458
49404
 
49459
- /**
49460
- * Get the user's registry profile.
49461
- *
49462
- * @returns {Promise<Object|null>} User profile or null
49463
- */
49464
- async function getRegistryProfile$2() {
49465
- const stored = getStoredToken$3();
49466
- if (!stored) return null;
49405
+ // --- Controller ---
49406
+ const mcpDashServerController$4 = {
49407
+ /**
49408
+ * Start the MCP Dash server.
49409
+ * @param {BrowserWindow} win
49410
+ * @param {Object} options - { port?: number }
49411
+ */
49412
+ startServer: async (win, options = {}) => {
49413
+ if (httpsServer) {
49414
+ return {
49415
+ success: false,
49416
+ error: "Server is already running",
49417
+ };
49418
+ }
49467
49419
 
49468
- try {
49469
- const response = await fetch(`${REGISTRY_BASE_URL$1}/api/auth/me`, {
49470
- headers: {
49471
- Authorization: `Bearer ${stored.token}`,
49472
- },
49473
- });
49420
+ try {
49421
+ const serverSettings = getMcpServerSettings(win);
49422
+ const port = options.port || serverSettings.port || 3141;
49423
+ const token =
49424
+ serverSettings.token || mcpDashServerController$4.getOrCreateToken(win);
49474
49425
 
49475
- if (response.status === 401) {
49476
- // Token expired or invalid — clear stored credentials
49477
- clearToken$2();
49478
- return null;
49479
- }
49480
- if (!response.ok) return null;
49426
+ // Create McpServer
49427
+ mcpServer = new McpServer({
49428
+ name: "dash-electron",
49429
+ version: "1.0.0",
49430
+ });
49481
49431
 
49482
- const data = await response.json();
49483
- return data.user || null;
49484
- } catch {
49485
- return null;
49486
- }
49487
- }
49432
+ // Generate or load TLS certificate
49433
+ const { app } = require("electron");
49434
+ const path = require("path");
49435
+ const certsDir = path.join(app.getPath("userData"), "certs");
49436
+ const tlsCert = getOrCreateCert(certsDir);
49488
49437
 
49489
- /**
49490
- * Clear stored auth token (logout).
49491
- */
49492
- function clearToken$2() {
49493
- try {
49494
- const s = getStore$1();
49495
- s.clear();
49496
- console.log("[RegistryAuthController] Token cleared");
49497
- } catch (err) {
49498
- console.error("[RegistryAuthController] Error clearing token:", err);
49499
- }
49500
- }
49438
+ // Apply registered tools and resources
49439
+ applyRegistrations(mcpServer);
49440
+
49441
+ // Create HTTPS server with auth and rate limiting
49442
+ httpsServer = https$2.createServer(
49443
+ { key: tlsCert.key, cert: tlsCert.cert },
49444
+ async (req, res) => {
49445
+ const ip = req.socket.remoteAddress || req.connection.remoteAddress;
49446
+
49447
+ // Rate limiting
49448
+ if (isRateLimited$1(ip)) {
49449
+ res.writeHead(429, { "Content-Type": "application/json" });
49450
+ res.end(JSON.stringify({ error: "Rate limit exceeded" }));
49451
+ return;
49452
+ }
49453
+
49454
+ // Bearer token auth
49455
+ const authHeader = req.headers.authorization;
49456
+ if (!authHeader || authHeader !== `Bearer ${token}`) {
49457
+ res.writeHead(401, { "Content-Type": "application/json" });
49458
+ res.end(JSON.stringify({ error: "Unauthorized" }));
49459
+ return;
49460
+ }
49461
+
49462
+ // Handle MCP requests on /mcp path
49463
+ if (req.url === "/mcp" || req.url?.startsWith("/mcp")) {
49464
+ try {
49465
+ // Stateless mode: create a fresh server + transport per request
49466
+ const reqServer = new McpServer({
49467
+ name: "dash-electron",
49468
+ version: "1.0.0",
49469
+ });
49470
+ applyRegistrations(reqServer);
49471
+ const reqTransport = new StreamableHTTPServerTransport({
49472
+ sessionIdGenerator: undefined,
49473
+ });
49474
+ await reqServer.connect(reqTransport);
49475
+ connectionCount++;
49476
+ await reqTransport.handleRequest(req, res);
49477
+ } catch (err) {
49478
+ console.error("[mcpDashServer] Error handling MCP request:", err);
49479
+ if (!res.headersSent) {
49480
+ res.writeHead(500, {
49481
+ "Content-Type": "application/json",
49482
+ });
49483
+ res.end(
49484
+ JSON.stringify({
49485
+ error: "Internal server error",
49486
+ }),
49487
+ );
49488
+ }
49489
+ }
49490
+ } else {
49491
+ // Health check endpoint
49492
+ if (req.url === "/health" && req.method === "GET") {
49493
+ res.writeHead(200, {
49494
+ "Content-Type": "application/json",
49495
+ });
49496
+ res.end(
49497
+ JSON.stringify({
49498
+ status: "ok",
49499
+ server: "dash-electron-mcp",
49500
+ version: "1.0.0",
49501
+ }),
49502
+ );
49503
+ return;
49504
+ }
49505
+ res.writeHead(404, { "Content-Type": "application/json" });
49506
+ res.end(JSON.stringify({ error: "Not found" }));
49507
+ }
49508
+ },
49509
+ );
49510
+
49511
+ // Bind to localhost only
49512
+ await new Promise((resolve, reject) => {
49513
+ httpsServer.on("error", (err) => {
49514
+ httpsServer = null;
49515
+ mcpServer = null;
49516
+ if (err.code === "EADDRINUSE") {
49517
+ reject(
49518
+ new Error(
49519
+ `Port ${port} is already in use. Choose a different port in Settings.`,
49520
+ ),
49521
+ );
49522
+ } else {
49523
+ reject(err);
49524
+ }
49525
+ });
49526
+ httpsServer.listen(port, "127.0.0.1", () => {
49527
+ resolve();
49528
+ });
49529
+ });
49501
49530
 
49502
- /**
49503
- * Update the authenticated user's registry profile.
49504
- *
49505
- * @param {Object} updates - Fields to update (e.g. { displayName })
49506
- * @returns {Promise<Object|null>} Updated user or null on 401
49507
- */
49508
- async function updateRegistryProfile$1(updates) {
49509
- const stored = getStoredToken$3();
49510
- if (!stored) return null;
49531
+ startTime = Date.now();
49532
+ connectionCount = 0;
49533
+ activeWin = win;
49534
+ startCleanup();
49511
49535
 
49512
- try {
49513
- const response = await fetch(`${REGISTRY_BASE_URL$1}/api/auth/me`, {
49514
- method: "PATCH",
49515
- headers: {
49516
- Authorization: `Bearer ${stored.token}`,
49517
- "Content-Type": "application/json",
49518
- },
49519
- body: JSON.stringify(updates),
49520
- });
49536
+ // Save enabled state
49537
+ saveMcpServerSettings(win, {
49538
+ ...serverSettings,
49539
+ enabled: true,
49540
+ port,
49541
+ token,
49542
+ });
49521
49543
 
49522
- if (response.status === 401) {
49523
- clearToken$2();
49524
- return null;
49525
- }
49526
- if (!response.ok) return null;
49544
+ console.log(
49545
+ `[mcpDashServer] Server started on https://127.0.0.1:${port}/mcp`,
49546
+ );
49527
49547
 
49528
- const data = await response.json();
49529
- return data.user || null;
49530
- } catch {
49531
- return null;
49532
- }
49533
- }
49548
+ return {
49549
+ success: true,
49550
+ port,
49551
+ url: `https://127.0.0.1:${port}/mcp`,
49552
+ };
49553
+ } catch (err) {
49554
+ console.error("[mcpDashServer] Failed to start server:", err);
49555
+ httpsServer = null;
49556
+ mcpServer = null;
49557
+ return {
49558
+ success: false,
49559
+ error: err.message,
49560
+ };
49561
+ }
49562
+ },
49534
49563
 
49535
- /**
49536
- * Get the authenticated user's published packages.
49537
- *
49538
- * @returns {Promise<Object|null>} { packages: [...] } or null
49539
- */
49540
- async function getRegistryPackages$1() {
49541
- const stored = getStoredToken$3();
49542
- if (!stored) return null;
49564
+ /**
49565
+ * Stop the MCP Dash server.
49566
+ */
49567
+ stopServer: async (win) => {
49568
+ if (!httpsServer) {
49569
+ return { success: true, message: "Server was not running" };
49570
+ }
49543
49571
 
49544
- try {
49545
- const response = await fetch(`${REGISTRY_BASE_URL$1}/api/auth/me/packages`, {
49546
- headers: {
49547
- Authorization: `Bearer ${stored.token}`,
49548
- },
49549
- });
49572
+ try {
49573
+ stopCleanup();
49550
49574
 
49551
- if (response.status === 401) {
49552
- clearToken$2();
49553
- return null;
49554
- }
49555
- if (!response.ok) return null;
49575
+ await new Promise((resolve) => {
49576
+ httpsServer.close(() => resolve());
49577
+ // Force close after 5 seconds
49578
+ setTimeout(() => resolve(), 5000);
49579
+ });
49556
49580
 
49557
- return await response.json();
49558
- } catch {
49559
- return null;
49560
- }
49561
- }
49581
+ if (mcpServer) {
49582
+ try {
49583
+ await mcpServer.close();
49584
+ } catch (e) {
49585
+ // Ignore close errors
49586
+ }
49587
+ }
49562
49588
 
49563
- /**
49564
- * Update a published package's metadata.
49565
- *
49566
- * @param {string} scope - Package scope (e.g. "@trops")
49567
- * @param {string} name - Package name
49568
- * @param {Object} updates - Fields to update (displayName, description, category, tags, visibility)
49569
- * @returns {Promise<Object|null>} Updated package or null
49570
- */
49571
- async function updateRegistryPackage$1(scope, name, updates) {
49572
- const stored = getStoredToken$3();
49573
- if (!stored) return null;
49589
+ httpsServer = null;
49590
+ mcpServer = null;
49591
+ transport = null;
49592
+ startTime = null;
49593
+ connectionCount = 0;
49594
+ activeWin = null;
49574
49595
 
49575
- try {
49576
- const response = await fetch(
49577
- `${REGISTRY_BASE_URL$1}/api/packages/${encodeURIComponent(scope)}/${encodeURIComponent(name)}`,
49578
- {
49579
- method: "PATCH",
49580
- headers: {
49581
- Authorization: `Bearer ${stored.token}`,
49582
- "Content-Type": "application/json",
49583
- },
49584
- body: JSON.stringify(updates),
49585
- },
49586
- );
49596
+ // Update settings
49597
+ if (win) {
49598
+ const serverSettings = getMcpServerSettings(win);
49599
+ saveMcpServerSettings(win, {
49600
+ ...serverSettings,
49601
+ enabled: false,
49602
+ });
49603
+ }
49587
49604
 
49588
- if (response.status === 401) {
49589
- clearToken$2();
49590
- return null;
49605
+ console.log("[mcpDashServer] Server stopped");
49606
+ return { success: true };
49607
+ } catch (err) {
49608
+ console.error("[mcpDashServer] Error stopping server:", err);
49609
+ return { success: false, error: err.message };
49591
49610
  }
49592
- if (!response.ok) return null;
49593
-
49594
- return await response.json();
49595
- } catch {
49596
- return null;
49597
- }
49598
- }
49611
+ },
49599
49612
 
49600
- /**
49601
- * Delete a published package from the registry.
49602
- *
49603
- * @param {string} scope - Package scope (e.g. "@trops")
49604
- * @param {string} name - Package name
49605
- * @returns {Promise<Object|null>} Response or null
49606
- */
49607
- async function deleteRegistryPackage(scope, name) {
49608
- const stored = getStoredToken$3();
49609
- if (!stored) return null;
49613
+ /**
49614
+ * Restart the server (stop + start).
49615
+ */
49616
+ restartServer: async (win, options = {}) => {
49617
+ await mcpDashServerController$4.stopServer(win);
49618
+ return mcpDashServerController$4.startServer(win, options);
49619
+ },
49610
49620
 
49611
- try {
49612
- const response = await fetch(
49613
- `${REGISTRY_BASE_URL$1}/api/packages/${encodeURIComponent(scope)}/${encodeURIComponent(name)}`,
49614
- {
49615
- method: "DELETE",
49616
- headers: {
49617
- Authorization: `Bearer ${stored.token}`,
49618
- },
49619
- },
49620
- );
49621
+ /**
49622
+ * Get server status.
49623
+ */
49624
+ getStatus: (win) => {
49625
+ const serverSettings = getMcpServerSettings(win);
49626
+ return {
49627
+ running: !!httpsServer,
49628
+ enabled: serverSettings.enabled || false,
49629
+ port: serverSettings.port || 3141,
49630
+ connectionCount,
49631
+ uptime: startTime ? Math.floor((Date.now() - startTime) / 1000) : 0,
49632
+ toolCount: registeredTools.length,
49633
+ resourceCount: registeredResources.length,
49634
+ };
49635
+ },
49621
49636
 
49622
- if (response.status === 401) {
49623
- clearToken$2();
49624
- return null;
49637
+ /**
49638
+ * Get or create the bearer token.
49639
+ */
49640
+ getOrCreateToken: (win) => {
49641
+ const serverSettings = getMcpServerSettings(win);
49642
+ if (serverSettings.token) {
49643
+ return serverSettings.token;
49625
49644
  }
49626
- if (!response.ok) return null;
49645
+ const token = randomUUID();
49646
+ saveMcpServerSettings(win, { ...serverSettings, token });
49647
+ return token;
49648
+ },
49627
49649
 
49628
- return await response.json();
49629
- } catch {
49630
- return null;
49631
- }
49632
- }
49650
+ /**
49651
+ * Auto-start server if enabled in settings.
49652
+ * Called from dash-electron on app ready.
49653
+ */
49654
+ autoStart: async (win) => {
49655
+ const serverSettings = getMcpServerSettings(win);
49656
+ if (serverSettings.enabled) {
49657
+ console.log("[mcpDashServer] Auto-starting server...");
49658
+ return mcpDashServerController$4.startServer(win, {
49659
+ port: serverSettings.port,
49660
+ });
49661
+ }
49662
+ return { success: false, message: "Server not enabled" };
49663
+ },
49633
49664
 
49634
- var registryAuthController$2 = {
49635
- initiateDeviceFlow: initiateDeviceFlow$1,
49636
- pollForToken: pollForToken$1,
49637
- getStoredToken: getStoredToken$3,
49638
- getAuthStatus: getAuthStatus$1,
49639
- getRegistryProfile: getRegistryProfile$2,
49640
- updateRegistryProfile: updateRegistryProfile$1,
49641
- getRegistryPackages: getRegistryPackages$1,
49642
- updateRegistryPackage: updateRegistryPackage$1,
49643
- deleteRegistryPackage,
49644
- clearToken: clearToken$2,
49665
+ // Expose registration functions for other controllers
49666
+ registerTool: registerTool$6,
49667
+ registerResource: registerResource$1,
49668
+ registerPrompt: registerPrompt$1,
49669
+ getServerContext,
49645
49670
  };
49646
49671
 
49672
+ var mcpDashServerController_1 = mcpDashServerController$4;
49673
+
49647
49674
  var widgetRegistry$1 = {exports: {}};
49648
49675
 
49649
49676
  var dynamicWidgetLoader$2 = {exports: {}};