@trops/dash-core 0.1.350 → 0.1.351
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/electron/index.js +2196 -2169
- package/dist/electron/index.js.map +1 -1
- package/package.json +1 -1
package/dist/electron/index.js
CHANGED
|
@@ -26707,1465 +26707,1820 @@ function parsePackageId(id) {
|
|
|
26707
26707
|
var packageId = { toPackageId: toPackageId$1, parsePackageId };
|
|
26708
26708
|
|
|
26709
26709
|
/**
|
|
26710
|
-
*
|
|
26710
|
+
* registryAuthController.js
|
|
26711
26711
|
*
|
|
26712
|
-
* Manages
|
|
26713
|
-
*
|
|
26712
|
+
* Manages authentication with the Dash registry service.
|
|
26713
|
+
* Uses OAuth device code flow for desktop app authentication.
|
|
26714
26714
|
*
|
|
26715
|
-
*
|
|
26716
|
-
*
|
|
26717
|
-
*
|
|
26718
|
-
*
|
|
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
|
|
26722
|
-
|
|
26723
|
-
|
|
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
|
-
|
|
26736
|
-
|
|
26737
|
-
|
|
26738
|
-
|
|
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
|
-
*
|
|
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
|
|
26745
|
-
|
|
26746
|
-
|
|
26747
|
-
|
|
26748
|
-
|
|
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
|
-
*
|
|
26754
|
-
* Caches the result for CACHE_TTL_MS milliseconds.
|
|
26768
|
+
* Poll the registry for token after user completes browser auth.
|
|
26755
26769
|
*
|
|
26756
|
-
* @param {
|
|
26757
|
-
* @returns {Promise<Object>}
|
|
26770
|
+
* @param {string} deviceCode - The device code from initiateDeviceFlow()
|
|
26771
|
+
* @returns {Promise<Object>} { status: 'pending' | 'authorized' | 'expired', token?, userId? }
|
|
26758
26772
|
*/
|
|
26759
|
-
async function
|
|
26760
|
-
const
|
|
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
|
-
|
|
26763
|
-
|
|
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
|
-
|
|
26769
|
-
|
|
26770
|
-
|
|
26771
|
-
|
|
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
|
-
|
|
26815
|
-
|
|
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
|
-
//
|
|
26823
|
-
|
|
26824
|
-
|
|
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
|
-
|
|
26827
|
-
|
|
26828
|
-
|
|
26829
|
-
|
|
26830
|
-
|
|
26831
|
-
|
|
26800
|
+
return {
|
|
26801
|
+
status: "authorized",
|
|
26802
|
+
token: data.access_token,
|
|
26803
|
+
userId: data.user_id,
|
|
26804
|
+
};
|
|
26805
|
+
}
|
|
26832
26806
|
|
|
26833
|
-
|
|
26834
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
26832
|
+
* Check if the user is authenticated with the registry.
|
|
26847
26833
|
*
|
|
26848
|
-
* @
|
|
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
|
-
|
|
26859
|
-
const
|
|
26860
|
-
|
|
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
|
-
|
|
26871
|
-
|
|
26872
|
-
|
|
26873
|
-
|
|
26874
|
-
|
|
26875
|
-
|
|
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
|
-
|
|
26883
|
-
|
|
26884
|
-
|
|
26885
|
-
|
|
26886
|
-
|
|
26887
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26895
|
-
|
|
26896
|
-
|
|
26897
|
-
|
|
26898
|
-
|
|
26899
|
-
|
|
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
|
-
|
|
26923
|
-
|
|
26924
|
-
|
|
26925
|
-
|
|
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
|
-
|
|
26941
|
-
|
|
26942
|
-
|
|
26943
|
-
|
|
26944
|
-
|
|
26945
|
-
|
|
26946
|
-
|
|
26947
|
-
|
|
26948
|
-
|
|
26949
|
-
|
|
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
|
-
*
|
|
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 {
|
|
26987
|
-
* @returns {Promise<Object|null>}
|
|
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
|
|
26990
|
-
|
|
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
|
-
|
|
27013
|
-
|
|
27014
|
-
|
|
27015
|
-
|
|
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
|
-
|
|
27018
|
-
|
|
27019
|
-
|
|
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
|
|
27025
|
-
});
|
|
26916
|
+
if (!response.ok) return null;
|
|
27026
26917
|
|
|
27027
|
-
|
|
26918
|
+
const data = await response.json();
|
|
26919
|
+
return data.user || null;
|
|
26920
|
+
} catch {
|
|
26921
|
+
return null;
|
|
26922
|
+
}
|
|
27028
26923
|
}
|
|
27029
26924
|
|
|
27030
26925
|
/**
|
|
27031
|
-
*
|
|
26926
|
+
* Get the authenticated user's published packages.
|
|
27032
26927
|
*
|
|
27033
|
-
* @
|
|
27034
|
-
* @returns {Promise<Array<Object>>} Widgets with available updates
|
|
26928
|
+
* @returns {Promise<Object|null>} { packages: [...] } or null
|
|
27035
26929
|
*/
|
|
27036
|
-
async function
|
|
27037
|
-
const
|
|
27038
|
-
|
|
26930
|
+
async function getRegistryPackages$1() {
|
|
26931
|
+
const stored = getStoredToken$4();
|
|
26932
|
+
if (!stored) return null;
|
|
27039
26933
|
|
|
27040
|
-
|
|
27041
|
-
const
|
|
27042
|
-
|
|
27043
|
-
|
|
27044
|
-
|
|
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
|
-
|
|
27051
|
-
|
|
27052
|
-
|
|
27053
|
-
|
|
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
|
-
|
|
26947
|
+
return await response.json();
|
|
26948
|
+
} catch {
|
|
26949
|
+
return null;
|
|
26950
|
+
}
|
|
27062
26951
|
}
|
|
27063
26952
|
|
|
27064
26953
|
/**
|
|
27065
|
-
*
|
|
27066
|
-
* Convenience wrapper around searchRegistry with type: "dashboard".
|
|
26954
|
+
* Update a published package's metadata.
|
|
27067
26955
|
*
|
|
27068
|
-
* @param {string}
|
|
27069
|
-
* @param {
|
|
27070
|
-
* @
|
|
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
|
|
27073
|
-
|
|
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
|
-
*
|
|
27078
|
-
* Convenience wrapper around searchRegistry with type: "theme".
|
|
26991
|
+
* Delete a published package from the registry.
|
|
27079
26992
|
*
|
|
27080
|
-
* @param {string}
|
|
27081
|
-
* @param {
|
|
27082
|
-
* @returns {Promise<Object>}
|
|
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
|
|
27085
|
-
|
|
27086
|
-
|
|
26997
|
+
async function deleteRegistryPackage(scope, name) {
|
|
26998
|
+
const stored = getStoredToken$4();
|
|
26999
|
+
if (!stored) return null;
|
|
27087
27000
|
|
|
27088
|
-
|
|
27089
|
-
|
|
27090
|
-
|
|
27091
|
-
|
|
27092
|
-
|
|
27093
|
-
|
|
27094
|
-
|
|
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
|
-
|
|
27098
|
-
|
|
27099
|
-
|
|
27100
|
-
|
|
27101
|
-
|
|
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
|
-
|
|
27104
|
-
|
|
27105
|
-
|
|
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
|
-
|
|
27111
|
-
|
|
27112
|
-
|
|
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
|
-
|
|
27115
|
-
|
|
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
|
-
|
|
27122
|
-
|
|
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
|
-
|
|
27132
|
-
|
|
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
|
-
|
|
27135
|
-
|
|
27076
|
+
function getCacheKey() {
|
|
27077
|
+
const stored = getStoredToken$3();
|
|
27078
|
+
return stored?.userId || "anon";
|
|
27079
|
+
}
|
|
27136
27080
|
|
|
27137
|
-
|
|
27138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27143
|
-
|
|
27144
|
-
|
|
27145
|
-
|
|
27146
|
-
|
|
27147
|
-
|
|
27148
|
-
|
|
27149
|
-
|
|
27150
|
-
|
|
27151
|
-
|
|
27152
|
-
|
|
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
|
-
|
|
27159
|
-
|
|
27160
|
-
|
|
27161
|
-
|
|
27162
|
-
|
|
27163
|
-
|
|
27164
|
-
|
|
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
|
-
|
|
27183
|
-
|
|
27184
|
-
reject(e);
|
|
27185
|
-
});
|
|
27119
|
+
try {
|
|
27120
|
+
let indexData;
|
|
27186
27121
|
|
|
27187
|
-
|
|
27188
|
-
|
|
27189
|
-
|
|
27190
|
-
|
|
27191
|
-
|
|
27192
|
-
|
|
27193
|
-
|
|
27194
|
-
|
|
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
|
-
|
|
27200
|
-
|
|
27201
|
-
|
|
27202
|
-
|
|
27203
|
-
|
|
27204
|
-
|
|
27205
|
-
|
|
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
|
-
|
|
27220
|
-
|
|
27221
|
-
|
|
27222
|
-
|
|
27223
|
-
|
|
27224
|
-
|
|
27225
|
-
|
|
27226
|
-
|
|
27227
|
-
|
|
27228
|
-
|
|
27229
|
-
|
|
27230
|
-
|
|
27231
|
-
|
|
27232
|
-
|
|
27233
|
-
|
|
27234
|
-
|
|
27235
|
-
|
|
27236
|
-
|
|
27237
|
-
|
|
27238
|
-
|
|
27239
|
-
|
|
27240
|
-
|
|
27241
|
-
|
|
27242
|
-
|
|
27243
|
-
|
|
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
|
-
|
|
27254
|
-
|
|
27255
|
-
|
|
27256
|
-
|
|
27257
|
-
|
|
27258
|
-
|
|
27259
|
-
|
|
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
|
-
|
|
27265
|
-
|
|
27266
|
-
|
|
27267
|
-
|
|
27268
|
-
|
|
27269
|
-
|
|
27270
|
-
|
|
27271
|
-
|
|
27272
|
-
|
|
27273
|
-
|
|
27274
|
-
|
|
27275
|
-
|
|
27276
|
-
|
|
27277
|
-
|
|
27278
|
-
|
|
27279
|
-
|
|
27280
|
-
|
|
27281
|
-
|
|
27282
|
-
|
|
27283
|
-
|
|
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
|
-
|
|
27293
|
-
|
|
27294
|
-
|
|
27295
|
-
|
|
27296
|
-
|
|
27297
|
-
|
|
27298
|
-
|
|
27299
|
-
|
|
27300
|
-
|
|
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
|
-
|
|
27304
|
-
|
|
27305
|
-
|
|
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
|
-
|
|
27309
|
-
|
|
27310
|
-
|
|
27311
|
-
|
|
27312
|
-
|
|
27313
|
-
|
|
27314
|
-
|
|
27315
|
-
|
|
27316
|
-
|
|
27317
|
-
|
|
27318
|
-
|
|
27319
|
-
|
|
27320
|
-
|
|
27321
|
-
|
|
27322
|
-
|
|
27323
|
-
|
|
27324
|
-
|
|
27325
|
-
|
|
27326
|
-
|
|
27327
|
-
|
|
27328
|
-
|
|
27329
|
-
|
|
27330
|
-
|
|
27331
|
-
|
|
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
|
-
|
|
27337
|
-
|
|
27338
|
-
|
|
27339
|
-
|
|
27340
|
-
|
|
27341
|
-
|
|
27342
|
-
|
|
27343
|
-
|
|
27344
|
-
|
|
27345
|
-
|
|
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
|
-
|
|
27354
|
-
|
|
27355
|
-
|
|
27356
|
-
|
|
27357
|
-
|
|
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
|
-
|
|
27360
|
-
|
|
27361
|
-
callback("saving objects ", file);
|
|
27362
|
-
}
|
|
27330
|
+
return { packages, totalWidgets };
|
|
27331
|
+
}
|
|
27363
27332
|
|
|
27364
|
-
|
|
27365
|
-
|
|
27366
|
-
|
|
27367
|
-
|
|
27368
|
-
|
|
27369
|
-
|
|
27370
|
-
|
|
27371
|
-
|
|
27372
|
-
|
|
27373
|
-
|
|
27374
|
-
|
|
27375
|
-
|
|
27376
|
-
|
|
27377
|
-
|
|
27378
|
-
|
|
27379
|
-
|
|
27380
|
-
|
|
27381
|
-
|
|
27382
|
-
|
|
27383
|
-
|
|
27384
|
-
|
|
27385
|
-
|
|
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
|
-
|
|
27416
|
+
return updates;
|
|
27417
|
+
}
|
|
27392
27418
|
|
|
27393
27419
|
/**
|
|
27394
|
-
*
|
|
27420
|
+
* Search the registry for dashboard packages only.
|
|
27421
|
+
* Convenience wrapper around searchRegistry with type: "dashboard".
|
|
27395
27422
|
*
|
|
27396
|
-
*
|
|
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
|
-
*
|
|
27399
|
-
*
|
|
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
|
-
|
|
27403
|
-
|
|
27404
|
-
|
|
27405
|
-
|
|
27443
|
+
var registryController$3 = {
|
|
27444
|
+
fetchRegistryIndex,
|
|
27445
|
+
searchRegistry: searchRegistry$1,
|
|
27446
|
+
searchDashboards,
|
|
27447
|
+
searchThemes,
|
|
27448
|
+
getPackage: getPackage$1,
|
|
27449
|
+
checkUpdates,
|
|
27450
|
+
};
|
|
27406
27451
|
|
|
27407
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27519
|
-
|
|
27520
|
-
|
|
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
|
-
|
|
27523
|
-
|
|
27524
|
-
|
|
27525
|
-
|
|
27526
|
-
|
|
27527
|
-
|
|
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
|
-
.
|
|
27531
|
-
|
|
27532
|
-
|
|
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
|
-
|
|
27554
|
-
|
|
27555
|
-
|
|
27556
|
-
|
|
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
|
-
|
|
27548
|
+
console.log("catch batch ", e.message);
|
|
27549
|
+
reject(e);
|
|
27582
27550
|
});
|
|
27583
|
-
}
|
|
27584
|
-
|
|
27585
|
-
error: e.message,
|
|
27586
|
-
});
|
|
27587
|
-
}
|
|
27588
|
-
},
|
|
27551
|
+
});
|
|
27552
|
+
};
|
|
27589
27553
|
|
|
27590
|
-
|
|
27591
|
-
|
|
27592
|
-
|
|
27593
|
-
|
|
27594
|
-
|
|
27595
|
-
|
|
27596
|
-
|
|
27597
|
-
|
|
27598
|
-
|
|
27599
|
-
|
|
27600
|
-
|
|
27601
|
-
|
|
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
|
-
|
|
27621
|
-
|
|
27622
|
-
|
|
27623
|
-
}
|
|
27624
|
-
}
|
|
27625
|
-
}
|
|
27626
|
-
|
|
27627
|
-
|
|
27628
|
-
|
|
27629
|
-
|
|
27630
|
-
|
|
27631
|
-
|
|
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
|
-
|
|
27634
|
-
|
|
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
|
|
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
|
-
|
|
27647
|
-
|
|
27648
|
-
|
|
27649
|
-
|
|
27650
|
-
|
|
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
|
-
|
|
27653
|
-
|
|
27654
|
-
|
|
27655
|
-
|
|
27656
|
-
|
|
27657
|
-
|
|
27658
|
-
|
|
27659
|
-
|
|
27660
|
-
|
|
27661
|
-
|
|
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
|
-
|
|
27669
|
-
|
|
27670
|
-
|
|
27671
|
-
|
|
27672
|
-
|
|
27673
|
-
|
|
27674
|
-
|
|
27675
|
-
|
|
27676
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27685
|
-
|
|
27686
|
-
|
|
27687
|
-
|
|
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
|
-
|
|
27690
|
-
|
|
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
|
-
|
|
27693
|
-
|
|
27694
|
-
|
|
27695
|
-
|
|
27696
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27707
|
-
|
|
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
|
-
|
|
27710
|
-
writeFileSync(filename, JSON.stringify(menuItemsArray, null, 2));
|
|
27746
|
+
var algolia = AlgoliaIndex$1;
|
|
27711
27747
|
|
|
27712
|
-
|
|
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
|
-
|
|
27715
|
-
|
|
27716
|
-
|
|
27717
|
-
|
|
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
|
-
|
|
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
|
|
27733
|
-
|
|
27734
|
-
|
|
27735
|
-
appId,
|
|
27736
|
-
configFilename$1,
|
|
27773
|
+
const searchClient = algoliasearch(
|
|
27774
|
+
application["appId"],
|
|
27775
|
+
application["key"],
|
|
27737
27776
|
);
|
|
27738
|
-
|
|
27739
|
-
|
|
27740
|
-
|
|
27741
|
-
|
|
27742
|
-
|
|
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
|
-
|
|
27746
|
-
|
|
27747
|
-
|
|
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
|
-
|
|
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
|
|
27765
|
-
|
|
27766
|
-
"
|
|
27767
|
-
|
|
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(
|
|
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
|
-
*
|
|
27879
|
-
*
|
|
27840
|
+
* browseObjectsToFile
|
|
27841
|
+
* Lets try and browse an index and pull down the hits and save as a file
|
|
27880
27842
|
*
|
|
27881
|
-
* @param {
|
|
27882
|
-
* @param {
|
|
27883
|
-
* @param {
|
|
27843
|
+
* @param {*} win
|
|
27844
|
+
* @param {*} appId
|
|
27845
|
+
* @param {*} apiKey
|
|
27846
|
+
* @param {*} indexName
|
|
27847
|
+
* @param {*} toFilename
|
|
27848
|
+
* @param {*} query
|
|
27884
27849
|
*/
|
|
27885
|
-
|
|
27886
|
-
|
|
27887
|
-
|
|
27888
|
-
|
|
27889
|
-
|
|
27890
|
-
|
|
27891
|
-
|
|
27892
|
-
|
|
27893
|
-
|
|
27894
|
-
|
|
27895
|
-
|
|
27896
|
-
|
|
27897
|
-
|
|
27898
|
-
|
|
27899
|
-
|
|
27900
|
-
|
|
27901
|
-
|
|
27902
|
-
|
|
27903
|
-
|
|
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
|
-
|
|
27911
|
-
const sessionId = widgetUuid ? sessions.get(widgetUuid) : null;
|
|
27912
|
-
if (sessionId) {
|
|
27913
|
-
args.push("--resume", sessionId);
|
|
27914
|
-
}
|
|
27871
|
+
let sep = "";
|
|
27915
27872
|
|
|
27916
|
-
|
|
27917
|
-
|
|
27918
|
-
|
|
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
|
-
|
|
27929
|
-
|
|
27930
|
-
|
|
27931
|
-
|
|
27932
|
-
|
|
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
|
-
|
|
27938
|
-
|
|
27939
|
-
|
|
27940
|
-
|
|
27941
|
-
|
|
27942
|
-
|
|
27943
|
-
|
|
27944
|
-
|
|
27945
|
-
|
|
27946
|
-
|
|
27947
|
-
|
|
27948
|
-
|
|
27949
|
-
|
|
27950
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27955
|
-
child.stdin.write(userText);
|
|
27956
|
-
child.stdin.end();
|
|
27996
|
+
var algoliaController_1 = algoliaController$1;
|
|
27957
27997
|
|
|
27958
|
-
|
|
27959
|
-
|
|
27960
|
-
let capturedSessionId = null;
|
|
27961
|
-
let retried = false;
|
|
27998
|
+
const OpenAI = require$$0$7;
|
|
27999
|
+
const events$2 = events$8;
|
|
27962
28000
|
|
|
27963
|
-
|
|
27964
|
-
|
|
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
|
-
|
|
27967
|
-
|
|
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
|
-
|
|
27970
|
-
const lines = stdoutBuffer.split("\n");
|
|
27971
|
-
stdoutBuffer = lines.pop(); // keep incomplete line in buffer
|
|
28037
|
+
var openaiController_1 = openaiController$1;
|
|
27972
28038
|
|
|
27973
|
-
|
|
27974
|
-
|
|
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
|
-
|
|
27977
|
-
|
|
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
|
-
|
|
27985
|
-
|
|
27986
|
-
|
|
27987
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28041
|
-
|
|
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
|
-
|
|
28051
|
-
|
|
28052
|
-
});
|
|
28064
|
+
// write the new pages configuration back to the file
|
|
28065
|
+
writeFileSync(filename, JSON.stringify(menuItemsArray, null, 2));
|
|
28053
28066
|
|
|
28054
|
-
|
|
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
|
-
|
|
28064
|
-
|
|
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
|
-
|
|
28067
|
-
|
|
28068
|
-
|
|
28069
|
-
|
|
28070
|
-
|
|
28071
|
-
|
|
28072
|
-
|
|
28073
|
-
|
|
28074
|
-
|
|
28075
|
-
|
|
28076
|
-
|
|
28077
|
-
|
|
28078
|
-
|
|
28079
|
-
|
|
28080
|
-
|
|
28081
|
-
|
|
28082
|
-
|
|
28083
|
-
|
|
28084
|
-
|
|
28085
|
-
|
|
28086
|
-
|
|
28087
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28105
|
-
|
|
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
|
-
|
|
28120
|
-
|
|
28121
|
-
|
|
28122
|
-
|
|
28123
|
-
|
|
28124
|
-
|
|
28125
|
-
|
|
28126
|
-
|
|
28127
|
-
|
|
28128
|
-
|
|
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
|
-
*
|
|
28138
|
-
*
|
|
28219
|
+
* isAvailable
|
|
28220
|
+
* Check if the Claude Code CLI is installed and accessible.
|
|
28139
28221
|
*
|
|
28140
|
-
* @
|
|
28141
|
-
* @returns {{ success: boolean }}
|
|
28222
|
+
* @returns {{ available: boolean, path?: string }}
|
|
28142
28223
|
*/
|
|
28143
|
-
|
|
28144
|
-
const
|
|
28145
|
-
if (
|
|
28146
|
-
|
|
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 {
|
|
28229
|
+
return { available: false };
|
|
28151
28230
|
},
|
|
28152
28231
|
|
|
28153
28232
|
/**
|
|
28154
|
-
*
|
|
28155
|
-
*
|
|
28233
|
+
* sendMessage
|
|
28234
|
+
* Stream a response from the Claude Code CLI with NDJSON parsing.
|
|
28156
28235
|
*
|
|
28157
|
-
* @param {
|
|
28158
|
-
* @
|
|
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
|
-
|
|
28161
|
-
|
|
28162
|
-
|
|
28163
|
-
|
|
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
|
-
|
|
48848
|
-
|
|
48849
|
-
|
|
48850
|
-
|
|
48851
|
-
|
|
48852
|
-
|
|
48853
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
48862
|
-
*
|
|
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
|
-
*
|
|
48865
|
-
*
|
|
48866
|
-
*
|
|
48867
|
-
*
|
|
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
|
|
48958
|
-
const
|
|
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
|
-
*
|
|
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
|
|
48964
|
-
|
|
48965
|
-
|
|
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
|
-
//
|
|
49016
|
-
|
|
49017
|
-
|
|
49018
|
-
|
|
49019
|
-
|
|
49020
|
-
|
|
49021
|
-
|
|
49022
|
-
|
|
49023
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
49072
|
-
|
|
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
|
-
|
|
49078
|
-
|
|
49079
|
-
|
|
49080
|
-
|
|
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
|
-
|
|
49084
|
-
|
|
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
|
-
|
|
49087
|
-
|
|
49088
|
-
|
|
49089
|
-
|
|
49090
|
-
|
|
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
|
-
|
|
49093
|
-
|
|
49094
|
-
|
|
49095
|
-
|
|
49096
|
-
|
|
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
|
-
|
|
49100
|
-
|
|
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
|
-
|
|
49108
|
-
|
|
49109
|
-
|
|
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
|
-
|
|
49157
|
-
|
|
49158
|
-
|
|
49159
|
-
|
|
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
|
-
|
|
49177
|
-
connectionCount = 0;
|
|
49178
|
-
activeWin = win;
|
|
49179
|
-
startCleanup();
|
|
49120
|
+
console.log(`[tlsCert] Certificate saved to ${certsDir}`);
|
|
49180
49121
|
|
|
49181
|
-
|
|
49182
|
-
|
|
49183
|
-
...serverSettings,
|
|
49184
|
-
enabled: true,
|
|
49185
|
-
port,
|
|
49186
|
-
token,
|
|
49187
|
-
});
|
|
49122
|
+
return { cert: certPem, key: keyPem };
|
|
49123
|
+
}
|
|
49188
49124
|
|
|
49189
|
-
|
|
49190
|
-
`[mcpDashServer] Server started on https://127.0.0.1:${port}/mcp`,
|
|
49191
|
-
);
|
|
49125
|
+
var tlsCert = { getOrCreateCert: getOrCreateCert$1 };
|
|
49192
49126
|
|
|
49193
|
-
|
|
49194
|
-
|
|
49195
|
-
|
|
49196
|
-
|
|
49197
|
-
|
|
49198
|
-
|
|
49199
|
-
|
|
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
|
-
|
|
49218
|
-
|
|
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
|
-
|
|
49221
|
-
httpsServer.close(() => resolve());
|
|
49222
|
-
// Force close after 5 seconds
|
|
49223
|
-
setTimeout(() => resolve(), 5000);
|
|
49224
|
-
});
|
|
49144
|
+
let schema;
|
|
49225
49145
|
|
|
49226
|
-
|
|
49227
|
-
|
|
49228
|
-
|
|
49229
|
-
|
|
49230
|
-
|
|
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
|
-
|
|
49235
|
-
|
|
49236
|
-
|
|
49237
|
-
|
|
49238
|
-
|
|
49239
|
-
|
|
49240
|
-
|
|
49241
|
-
|
|
49242
|
-
|
|
49243
|
-
|
|
49244
|
-
|
|
49245
|
-
|
|
49246
|
-
|
|
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
|
-
|
|
49251
|
-
|
|
49252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49268
|
-
|
|
49269
|
-
|
|
49270
|
-
|
|
49271
|
-
|
|
49272
|
-
|
|
49273
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49297
|
-
|
|
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
|
-
|
|
49308
|
-
}
|
|
49202
|
+
shape[key] = fieldSchema;
|
|
49203
|
+
}
|
|
49309
49204
|
|
|
49310
|
-
|
|
49311
|
-
|
|
49312
|
-
registerResource: registerResource$1,
|
|
49313
|
-
registerPrompt: registerPrompt$1,
|
|
49314
|
-
getServerContext,
|
|
49315
|
-
};
|
|
49205
|
+
return z$1.object(shape);
|
|
49206
|
+
}
|
|
49316
49207
|
|
|
49317
|
-
var
|
|
49208
|
+
var jsonSchemaToZod_1 = { jsonSchemaToZod: jsonSchemaToZod$1, jsonSchemaPropertyToZod };
|
|
49318
49209
|
|
|
49319
49210
|
/**
|
|
49320
|
-
*
|
|
49211
|
+
* mcpDashServerController.js
|
|
49321
49212
|
*
|
|
49322
|
-
* Manages
|
|
49323
|
-
*
|
|
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
|
-
*
|
|
49326
|
-
*
|
|
49327
|
-
*
|
|
49328
|
-
*
|
|
49329
|
-
*
|
|
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
|
|
49333
|
-
|
|
49334
|
-
|
|
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
|
-
|
|
49337
|
-
|
|
49338
|
-
|
|
49339
|
-
|
|
49340
|
-
|
|
49341
|
-
|
|
49342
|
-
|
|
49343
|
-
|
|
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
|
-
|
|
49258
|
+
bucket.count++;
|
|
49259
|
+
return bucket.count > RATE_LIMIT;
|
|
49347
49260
|
}
|
|
49348
49261
|
|
|
49349
|
-
|
|
49350
|
-
|
|
49351
|
-
|
|
49352
|
-
|
|
49353
|
-
|
|
49354
|
-
|
|
49355
|
-
|
|
49356
|
-
|
|
49357
|
-
|
|
49358
|
-
|
|
49359
|
-
|
|
49360
|
-
|
|
49361
|
-
if (
|
|
49362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49368
|
-
|
|
49369
|
-
|
|
49370
|
-
|
|
49371
|
-
|
|
49372
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
49384
|
-
|
|
49385
|
-
|
|
49386
|
-
);
|
|
49300
|
+
function registerResource$1(resourceDef) {
|
|
49301
|
+
registeredResources.push(resourceDef);
|
|
49302
|
+
}
|
|
49387
49303
|
|
|
49388
|
-
|
|
49389
|
-
|
|
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
|
-
|
|
49393
|
-
|
|
49394
|
-
|
|
49395
|
-
|
|
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
|
-
|
|
49401
|
-
|
|
49402
|
-
|
|
49403
|
-
|
|
49404
|
-
|
|
49405
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
49428
|
-
const
|
|
49429
|
-
|
|
49430
|
-
|
|
49431
|
-
|
|
49432
|
-
|
|
49433
|
-
|
|
49434
|
-
|
|
49435
|
-
}
|
|
49436
|
-
} catch {
|
|
49437
|
-
|
|
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
|
-
*
|
|
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
|
|
49447
|
-
|
|
49448
|
-
|
|
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
|
-
|
|
49461
|
-
|
|
49462
|
-
|
|
49463
|
-
|
|
49464
|
-
|
|
49465
|
-
|
|
49466
|
-
|
|
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
|
-
|
|
49469
|
-
|
|
49470
|
-
|
|
49471
|
-
|
|
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
|
-
|
|
49476
|
-
|
|
49477
|
-
|
|
49478
|
-
|
|
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
|
-
|
|
49483
|
-
|
|
49484
|
-
|
|
49485
|
-
|
|
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
|
-
|
|
49491
|
-
|
|
49492
|
-
|
|
49493
|
-
|
|
49494
|
-
|
|
49495
|
-
|
|
49496
|
-
|
|
49497
|
-
|
|
49498
|
-
|
|
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
|
-
|
|
49504
|
-
|
|
49505
|
-
|
|
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
|
-
|
|
49513
|
-
|
|
49514
|
-
|
|
49515
|
-
|
|
49516
|
-
|
|
49517
|
-
|
|
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
|
-
|
|
49523
|
-
|
|
49524
|
-
|
|
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
|
-
|
|
49529
|
-
|
|
49530
|
-
|
|
49531
|
-
|
|
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
|
-
|
|
49537
|
-
|
|
49538
|
-
|
|
49539
|
-
|
|
49540
|
-
|
|
49541
|
-
|
|
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
|
-
|
|
49545
|
-
|
|
49546
|
-
headers: {
|
|
49547
|
-
Authorization: `Bearer ${stored.token}`,
|
|
49548
|
-
},
|
|
49549
|
-
});
|
|
49572
|
+
try {
|
|
49573
|
+
stopCleanup();
|
|
49550
49574
|
|
|
49551
|
-
|
|
49552
|
-
|
|
49553
|
-
|
|
49554
|
-
|
|
49555
|
-
|
|
49575
|
+
await new Promise((resolve) => {
|
|
49576
|
+
httpsServer.close(() => resolve());
|
|
49577
|
+
// Force close after 5 seconds
|
|
49578
|
+
setTimeout(() => resolve(), 5000);
|
|
49579
|
+
});
|
|
49556
49580
|
|
|
49557
|
-
|
|
49558
|
-
|
|
49559
|
-
|
|
49560
|
-
|
|
49561
|
-
|
|
49581
|
+
if (mcpServer) {
|
|
49582
|
+
try {
|
|
49583
|
+
await mcpServer.close();
|
|
49584
|
+
} catch (e) {
|
|
49585
|
+
// Ignore close errors
|
|
49586
|
+
}
|
|
49587
|
+
}
|
|
49562
49588
|
|
|
49563
|
-
|
|
49564
|
-
|
|
49565
|
-
|
|
49566
|
-
|
|
49567
|
-
|
|
49568
|
-
|
|
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
|
-
|
|
49576
|
-
|
|
49577
|
-
|
|
49578
|
-
|
|
49579
|
-
|
|
49580
|
-
|
|
49581
|
-
|
|
49582
|
-
|
|
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
|
-
|
|
49589
|
-
|
|
49590
|
-
|
|
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
|
-
|
|
49593
|
-
|
|
49594
|
-
return await response.json();
|
|
49595
|
-
} catch {
|
|
49596
|
-
return null;
|
|
49597
|
-
}
|
|
49598
|
-
}
|
|
49611
|
+
},
|
|
49599
49612
|
|
|
49600
|
-
/**
|
|
49601
|
-
|
|
49602
|
-
|
|
49603
|
-
|
|
49604
|
-
|
|
49605
|
-
|
|
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
|
-
|
|
49612
|
-
|
|
49613
|
-
|
|
49614
|
-
|
|
49615
|
-
|
|
49616
|
-
|
|
49617
|
-
|
|
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
|
-
|
|
49623
|
-
|
|
49624
|
-
|
|
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
|
-
|
|
49645
|
+
const token = randomUUID();
|
|
49646
|
+
saveMcpServerSettings(win, { ...serverSettings, token });
|
|
49647
|
+
return token;
|
|
49648
|
+
},
|
|
49627
49649
|
|
|
49628
|
-
|
|
49629
|
-
|
|
49630
|
-
|
|
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
|
-
|
|
49635
|
-
|
|
49636
|
-
|
|
49637
|
-
|
|
49638
|
-
|
|
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: {}};
|