@trops/dash-core 0.1.491 → 0.1.493

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.
@@ -17,7 +17,7 @@ var require$$0$5 = require('@modelcontextprotocol/sdk/client/index.js');
17
17
  var require$$1$4 = require('@modelcontextprotocol/sdk/client/stdio.js');
18
18
  var require$$0$4 = require('pkce-challenge');
19
19
  var require$$2$1 = require('os');
20
- var require$$10 = require('child_process');
20
+ var require$$11 = require('child_process');
21
21
  var require$$3$2 = require('adm-zip');
22
22
  var require$$4$1 = require('url');
23
23
  var require$$2$2 = require('vm');
@@ -1107,7 +1107,7 @@ var secureStoreController$1 = {
1107
1107
  getData: getData$1,
1108
1108
  };
1109
1109
 
1110
- const path$m = require$$1$2;
1110
+ const path$n = require$$1$2;
1111
1111
  const {
1112
1112
  readFileSync,
1113
1113
  writeFileSync: writeFileSync$4,
@@ -1125,7 +1125,7 @@ const {
1125
1125
  function ensureDirectoryExistence$2(filePath) {
1126
1126
  try {
1127
1127
  // isDirectory
1128
- var dirname = path$m.dirname(filePath);
1128
+ var dirname = path$n.dirname(filePath);
1129
1129
  // check if the directory exists...return true
1130
1130
  // if not, we can pass in the dirname as the filepath
1131
1131
  // and check each directory recursively.
@@ -1240,7 +1240,7 @@ function removeFilesFromDirectory(directory, excludeFiles = []) {
1240
1240
 
1241
1241
  for (const file of files) {
1242
1242
  if (!excludeFiles.includes(file)) {
1243
- unlinkSync(path$m.join(directory, file), (err) => {
1243
+ unlinkSync(path$n.join(directory, file), (err) => {
1244
1244
  if (err) throw err;
1245
1245
  });
1246
1246
  }
@@ -1258,7 +1258,7 @@ var file = {
1258
1258
  };
1259
1259
 
1260
1260
  const { app: app$f } = require$$0$1;
1261
- const path$l = require$$1$2;
1261
+ const path$m = require$$1$2;
1262
1262
  const { writeFileSync: writeFileSync$3 } = require$$0$2;
1263
1263
  const { getFileContents: getFileContents$7 } = file;
1264
1264
 
@@ -1305,7 +1305,7 @@ const workspaceController$3 = {
1305
1305
  saveWorkspaceForApplication: (win, appId, workspaceObject) => {
1306
1306
  try {
1307
1307
  // filename to the pages file (live pages)
1308
- const filename = path$l.join(
1308
+ const filename = path$m.join(
1309
1309
  app$f.getPath("userData"),
1310
1310
  appName$7,
1311
1311
  appId,
@@ -1354,7 +1354,7 @@ const workspaceController$3 = {
1354
1354
  saveMenuItemsForApplication: (win, appId, menuItems) => {
1355
1355
  try {
1356
1356
  // filename to the workspaces file
1357
- const filename = path$l.join(
1357
+ const filename = path$m.join(
1358
1358
  app$f.getPath("userData"),
1359
1359
  appName$7,
1360
1360
  appId,
@@ -1403,7 +1403,7 @@ const workspaceController$3 = {
1403
1403
  */
1404
1404
  deleteWorkspaceForApplication: (win, appId, workspaceId) => {
1405
1405
  try {
1406
- const filename = path$l.join(
1406
+ const filename = path$m.join(
1407
1407
  app$f.getPath("userData"),
1408
1408
  appName$7,
1409
1409
  appId,
@@ -1437,7 +1437,7 @@ const workspaceController$3 = {
1437
1437
 
1438
1438
  listWorkspacesForApplication: (win, appId) => {
1439
1439
  try {
1440
- const filename = path$l.join(
1440
+ const filename = path$m.join(
1441
1441
  app$f.getPath("userData"),
1442
1442
  appName$7,
1443
1443
  appId,
@@ -1465,7 +1465,7 @@ const workspaceController$3 = {
1465
1465
 
1466
1466
  listMenuItemsForApplication: (win, appId) => {
1467
1467
  try {
1468
- const filename = path$l.join(
1468
+ const filename = path$m.join(
1469
1469
  app$f.getPath("userData"),
1470
1470
  appName$7,
1471
1471
  appId,
@@ -1510,7 +1510,7 @@ const workspaceController$3 = {
1510
1510
  var workspaceController_1 = workspaceController$3;
1511
1511
 
1512
1512
  const { app: app$e } = require$$0$1;
1513
- const path$k = require$$1$2;
1513
+ const path$l = require$$1$2;
1514
1514
  const { writeFileSync: writeFileSync$2 } = require$$0$2;
1515
1515
  const { getFileContents: getFileContents$6 } = file;
1516
1516
 
@@ -1530,7 +1530,7 @@ const themeController$5 = {
1530
1530
  saveThemeForApplication: (win, appId, name, obj) => {
1531
1531
  try {
1532
1532
  // filename to the pages file (live pages)
1533
- const filename = path$k.join(
1533
+ const filename = path$l.join(
1534
1534
  app$e.getPath("userData"),
1535
1535
  appName$6,
1536
1536
  appId,
@@ -1576,7 +1576,7 @@ const themeController$5 = {
1576
1576
  */
1577
1577
  listThemesForApplication: (win, appId) => {
1578
1578
  try {
1579
- const filename = path$k.join(
1579
+ const filename = path$l.join(
1580
1580
  app$e.getPath("userData"),
1581
1581
  appName$6,
1582
1582
  appId,
@@ -1618,7 +1618,7 @@ const themeController$5 = {
1618
1618
  */
1619
1619
  deleteThemeForApplication: (win, appId, themeKey) => {
1620
1620
  try {
1621
- const filename = path$k.join(
1621
+ const filename = path$l.join(
1622
1622
  app$e.getPath("userData"),
1623
1623
  appName$6,
1624
1624
  appId,
@@ -1695,8 +1695,8 @@ var themeController_1 = themeController$5;
1695
1695
  * `/data/`.
1696
1696
  */
1697
1697
 
1698
- const path$j = require$$1$2;
1699
- const fs$e = require$$0$2;
1698
+ const path$k = require$$1$2;
1699
+ const fs$f = require$$0$2;
1700
1700
  const { app: app$d } = require$$0$1;
1701
1701
 
1702
1702
  const APP_NAME = "Dashboard";
@@ -1709,7 +1709,7 @@ function getAllowedRoots$2(category) {
1709
1709
  const userData = app$d.getPath("userData");
1710
1710
  switch (category) {
1711
1711
  case "data": {
1712
- const def = path$j.join(userData, APP_NAME, "data");
1712
+ const def = path$k.join(userData, APP_NAME, "data");
1713
1713
  // The user can configure a custom data directory in
1714
1714
  // Settings → General → Data Directory. If set, that
1715
1715
  // location is ALSO an allowed root. We don't replace the
@@ -1719,11 +1719,11 @@ function getAllowedRoots$2(category) {
1719
1719
  return override ? [def, override] : [def];
1720
1720
  }
1721
1721
  case "themes":
1722
- return [path$j.join(userData, APP_NAME, "themes")];
1722
+ return [path$k.join(userData, APP_NAME, "themes")];
1723
1723
  case "widgets":
1724
- return [path$j.join(userData, "widgets")];
1724
+ return [path$k.join(userData, "widgets")];
1725
1725
  case "plugins":
1726
- return [path$j.join(userData, "plugins")];
1726
+ return [path$k.join(userData, "plugins")];
1727
1727
  case "downloads":
1728
1728
  return [app$d.getPath("downloads")];
1729
1729
  default:
@@ -1740,13 +1740,13 @@ function getAllowedRoots$2(category) {
1740
1740
  */
1741
1741
  function readDataDirectoryFromSettings() {
1742
1742
  try {
1743
- const settingsPath = path$j.join(
1743
+ const settingsPath = path$k.join(
1744
1744
  app$d.getPath("userData"),
1745
1745
  APP_NAME,
1746
1746
  "settings.json",
1747
1747
  );
1748
- if (!fs$e.existsSync(settingsPath)) return undefined;
1749
- const raw = fs$e.readFileSync(settingsPath, "utf8");
1748
+ if (!fs$f.existsSync(settingsPath)) return undefined;
1749
+ const raw = fs$f.readFileSync(settingsPath, "utf8");
1750
1750
  const settings = JSON.parse(raw);
1751
1751
  const dir = settings && settings.dataDirectory;
1752
1752
  if (typeof dir === "string" && dir) return dir;
@@ -1772,18 +1772,18 @@ function safePath$3(requested, allowedRoots) {
1772
1772
  throw new Error("safePath: allowedRoots must be a non-empty array");
1773
1773
  }
1774
1774
 
1775
- const resolved = path$j.resolve(requested);
1775
+ const resolved = path$k.resolve(requested);
1776
1776
 
1777
1777
  // Real-path through symlinks. If the file doesn't exist yet (a
1778
1778
  // create-new operation), real-path the parent so a symlink in the
1779
1779
  // parent chain can't trick us.
1780
1780
  let real = resolved;
1781
1781
  try {
1782
- real = fs$e.realpathSync(resolved);
1782
+ real = fs$f.realpathSync(resolved);
1783
1783
  } catch (_e) {
1784
1784
  try {
1785
- const parent = fs$e.realpathSync(path$j.dirname(resolved));
1786
- real = path$j.join(parent, path$j.basename(resolved));
1785
+ const parent = fs$f.realpathSync(path$k.dirname(resolved));
1786
+ real = path$k.join(parent, path$k.basename(resolved));
1787
1787
  } catch (_e2) {
1788
1788
  // Parent doesn't exist either. Use the resolved-but-not-
1789
1789
  // real path; the caller's mkdirSync will happen inside the
@@ -1795,14 +1795,14 @@ function safePath$3(requested, allowedRoots) {
1795
1795
  for (const root of allowedRoots) {
1796
1796
  let realRoot = root;
1797
1797
  try {
1798
- if (fs$e.existsSync(root)) realRoot = fs$e.realpathSync(root);
1798
+ if (fs$f.existsSync(root)) realRoot = fs$f.realpathSync(root);
1799
1799
  } catch (_e) {
1800
1800
  // root doesn't exist or isn't reachable — keep as-is for
1801
1801
  // the comparison below
1802
1802
  }
1803
1803
  // Exact match OR strictly-inside (with separator to prevent
1804
1804
  // /data-evil/ matching /data/).
1805
- if (real === realRoot || real.startsWith(realRoot + path$j.sep)) {
1805
+ if (real === realRoot || real.startsWith(realRoot + path$k.sep)) {
1806
1806
  return real;
1807
1807
  }
1808
1808
  }
@@ -2060,14 +2060,14 @@ var safeJsExecutor$1 = {
2060
2060
  * - CSV -> JSON
2061
2061
  */
2062
2062
 
2063
- var fs$d = require$$0$2;
2063
+ var fs$e = require$$0$2;
2064
2064
  var readline = require$$1$3;
2065
2065
  const xtreamer = require$$2;
2066
2066
  var xmlParser = require$$3$1;
2067
2067
  var JSONStream$1 = require$$4;
2068
2068
  const stream$1 = require$$5;
2069
2069
  var csv = require$$6;
2070
- const path$i = require$$1$2;
2070
+ const path$j = require$$1$2;
2071
2071
  const { app: app$c } = require$$0$1;
2072
2072
  const { ensureDirectoryExistence: ensureDirectoryExistence$1 } = file;
2073
2073
  const safeJsExecutor = safeJsExecutor$1;
@@ -2122,7 +2122,7 @@ let Transform$1 = class Transform {
2122
2122
  let lineObject = [];
2123
2123
 
2124
2124
  const readInterface = readline.createInterface({
2125
- input: fs$d.createReadStream(filepath),
2125
+ input: fs$e.createReadStream(filepath),
2126
2126
  output: process.stdout,
2127
2127
  console: false,
2128
2128
  });
@@ -2157,7 +2157,7 @@ let Transform$1 = class Transform {
2157
2157
  return new Promise((resolve, reject) => {
2158
2158
  try {
2159
2159
  const parser = JSONStream$1.parse("*");
2160
- const readStream = fs$d.createReadStream(file).pipe(parser);
2160
+ const readStream = fs$e.createReadStream(file).pipe(parser);
2161
2161
 
2162
2162
  let count = 0;
2163
2163
 
@@ -2210,7 +2210,7 @@ let Transform$1 = class Transform {
2210
2210
  parseXMLStream = (filepath, outpath, start) => {
2211
2211
  return new Promise((resolve, reject) => {
2212
2212
  try {
2213
- const xmlFileReadStream = fs$d.createReadStream(filepath);
2213
+ const xmlFileReadStream = fs$e.createReadStream(filepath);
2214
2214
 
2215
2215
  xmlFileReadStream.on("end", () => {
2216
2216
  writeStream.write("\n]");
@@ -2221,7 +2221,7 @@ let Transform$1 = class Transform {
2221
2221
  resolve("Read Finish");
2222
2222
  });
2223
2223
 
2224
- const writeStream = fs$d.createWriteStream(outpath);
2224
+ const writeStream = fs$e.createWriteStream(outpath);
2225
2225
  writeStream.write("[\n");
2226
2226
 
2227
2227
  const options = {
@@ -2273,10 +2273,10 @@ let Transform$1 = class Transform {
2273
2273
  ) => {
2274
2274
  return new Promise((resolve, reject) => {
2275
2275
  try {
2276
- const readStream = fs$d
2276
+ const readStream = fs$e
2277
2277
  .createReadStream(filepath)
2278
2278
  .pipe(csv({ separator: delimiter }));
2279
- const writeStream = fs$d.createWriteStream(outpath);
2279
+ const writeStream = fs$e.createWriteStream(outpath);
2280
2280
 
2281
2281
  let canParse = true;
2282
2282
 
@@ -2362,18 +2362,18 @@ let Transform$1 = class Transform {
2362
2362
  }
2363
2363
 
2364
2364
  // Validate file paths are within app data directory
2365
- const appDataDir = path$i.join(app$c.getPath("userData"), TRANSFORM_APP_NAME);
2366
- const resolvedFilepath = path$i.resolve(filepath);
2367
- const resolvedOutFilepath = path$i.resolve(outFilepath);
2365
+ const appDataDir = path$j.join(app$c.getPath("userData"), TRANSFORM_APP_NAME);
2366
+ const resolvedFilepath = path$j.resolve(filepath);
2367
+ const resolvedOutFilepath = path$j.resolve(outFilepath);
2368
2368
 
2369
- if (!resolvedFilepath.startsWith(appDataDir + path$i.sep)) {
2369
+ if (!resolvedFilepath.startsWith(appDataDir + path$j.sep)) {
2370
2370
  return reject(
2371
2371
  new Error(
2372
2372
  "Input file path must be within the application data directory",
2373
2373
  ),
2374
2374
  );
2375
2375
  }
2376
- if (!resolvedOutFilepath.startsWith(appDataDir + path$i.sep)) {
2376
+ if (!resolvedOutFilepath.startsWith(appDataDir + path$j.sep)) {
2377
2377
  return reject(
2378
2378
  new Error(
2379
2379
  "Output file path must be within the application data directory",
@@ -2384,16 +2384,16 @@ let Transform$1 = class Transform {
2384
2384
  // JSON parser
2385
2385
  var parser = JSONStream$1.parse("*");
2386
2386
 
2387
- if (!fs$d.existsSync(resolvedFilepath)) {
2387
+ if (!fs$e.existsSync(resolvedFilepath)) {
2388
2388
  return reject(new Error("File doesnt exist"));
2389
2389
  }
2390
2390
  console.log("file exists ", resolvedFilepath);
2391
2391
  // create the readStream to parse the large file (json)
2392
- var readStream = fs$d.createReadStream(resolvedFilepath).pipe(parser);
2392
+ var readStream = fs$e.createReadStream(resolvedFilepath).pipe(parser);
2393
2393
 
2394
2394
  ensureDirectoryExistence$1(resolvedOutFilepath);
2395
2395
 
2396
- var writeStream = fs$d.createWriteStream(resolvedOutFilepath);
2396
+ var writeStream = fs$e.createWriteStream(resolvedOutFilepath);
2397
2397
 
2398
2398
  let sep = "";
2399
2399
  let count = 0;
@@ -2506,8 +2506,8 @@ let Transform$1 = class Transform {
2506
2506
  var transform = Transform$1;
2507
2507
 
2508
2508
  const { app: app$b } = require$$0$1;
2509
- var fs$c = require$$0$2;
2510
- const path$h = require$$1$2;
2509
+ var fs$d = require$$0$2;
2510
+ const path$i = require$$1$2;
2511
2511
  const events$5 = events$8;
2512
2512
  const { getFileContents: getFileContents$5, writeToFile: writeToFile$2 } = file;
2513
2513
  const { safePath: safePath$2, getAllowedRoots: getAllowedRoots$1 } = safePath_1;
@@ -2532,7 +2532,7 @@ const dataController$1 = {
2532
2532
  // Validate the renderer-supplied filename is contained within
2533
2533
  // the data directory. path.join doesn't reject `..` segments;
2534
2534
  // safePath does.
2535
- const candidate = path$h.join(
2535
+ const candidate = path$i.join(
2536
2536
  app$b.getPath("userData"),
2537
2537
  appName$5,
2538
2538
  appId,
@@ -2691,7 +2691,7 @@ const dataController$1 = {
2691
2691
  // intent, plus realpath/symlink protection.
2692
2692
  const resolvedFilepath = safePath$2(toFilepath, getAllowedRoots$1("data"));
2693
2693
 
2694
- const writeStream = fs$c.createWriteStream(resolvedFilepath);
2694
+ const writeStream = fs$d.createWriteStream(resolvedFilepath);
2695
2695
 
2696
2696
  https$3
2697
2697
  .get(url, (resp) => {
@@ -2855,7 +2855,7 @@ const dataController$1 = {
2855
2855
  if (data) {
2856
2856
  // Validate filename is contained within the data directory.
2857
2857
  // path.join doesn't reject `..` segments; safePath does.
2858
- const candidate = path$h.join(
2858
+ const candidate = path$i.join(
2859
2859
  app$b.getPath("userData"),
2860
2860
  appName$5,
2861
2861
  "data",
@@ -2948,7 +2948,7 @@ const dataController$1 = {
2948
2948
  try {
2949
2949
  if (filename) {
2950
2950
  // filename to the pages file (live pages)
2951
- const fromFilename = path$h.join(
2951
+ const fromFilename = path$i.join(
2952
2952
  app$b.getPath("userData"),
2953
2953
  appName$5,
2954
2954
  "data",
@@ -3030,8 +3030,8 @@ var dataController_1 = dataController$1;
3030
3030
  */
3031
3031
 
3032
3032
  const { app: app$a } = require$$0$1;
3033
- const path$g = require$$1$2;
3034
- const fs$b = require$$0$2;
3033
+ const path$h = require$$1$2;
3034
+ const fs$c = require$$0$2;
3035
3035
  const { getFileContents: getFileContents$4, writeToFile: writeToFile$1 } = file;
3036
3036
 
3037
3037
  const configFilename$3 = "settings.json";
@@ -3039,15 +3039,15 @@ const appName$4 = "Dashboard";
3039
3039
 
3040
3040
  // Helper function to recursively copy directory
3041
3041
  function copyDirectory(source, destination) {
3042
- if (!fs$b.existsSync(destination)) {
3043
- fs$b.mkdirSync(destination, { recursive: true });
3042
+ if (!fs$c.existsSync(destination)) {
3043
+ fs$c.mkdirSync(destination, { recursive: true });
3044
3044
  }
3045
3045
 
3046
- const files = fs$b.readdirSync(source);
3046
+ const files = fs$c.readdirSync(source);
3047
3047
  for (const file of files) {
3048
- const srcPath = path$g.join(source, file);
3049
- const destPath = path$g.join(destination, file);
3050
- const stat = fs$b.lstatSync(srcPath);
3048
+ const srcPath = path$h.join(source, file);
3049
+ const destPath = path$h.join(destination, file);
3050
+ const stat = fs$c.lstatSync(srcPath);
3051
3051
 
3052
3052
  // Skip symlinks to prevent following links to sensitive files
3053
3053
  if (stat.isSymbolicLink()) {
@@ -3058,7 +3058,7 @@ function copyDirectory(source, destination) {
3058
3058
  if (stat.isDirectory()) {
3059
3059
  copyDirectory(srcPath, destPath);
3060
3060
  } else {
3061
- fs$b.copyFileSync(srcPath, destPath);
3061
+ fs$c.copyFileSync(srcPath, destPath);
3062
3062
  }
3063
3063
  }
3064
3064
  }
@@ -3074,7 +3074,7 @@ const settingsController$4 = {
3074
3074
  try {
3075
3075
  if (data) {
3076
3076
  // <appId>/settings.json
3077
- const filename = path$g.join(
3077
+ const filename = path$h.join(
3078
3078
  app$a.getPath("userData"),
3079
3079
  appName$4,
3080
3080
  configFilename$3,
@@ -3110,7 +3110,7 @@ const settingsController$4 = {
3110
3110
  getSettingsForApplication: (win) => {
3111
3111
  try {
3112
3112
  // <appId>/settings.json
3113
- const filename = path$g.join(
3113
+ const filename = path$h.join(
3114
3114
  app$a.getPath("userData"),
3115
3115
  appName$4,
3116
3116
  configFilename$3,
@@ -3141,7 +3141,7 @@ const settingsController$4 = {
3141
3141
  */
3142
3142
  getDataDirectory: (win) => {
3143
3143
  try {
3144
- const settingsPath = path$g.join(
3144
+ const settingsPath = path$h.join(
3145
3145
  app$a.getPath("userData"),
3146
3146
  appName$4,
3147
3147
  configFilename$3,
@@ -3149,7 +3149,7 @@ const settingsController$4 = {
3149
3149
  const settings = getFileContents$4(settingsPath, {});
3150
3150
  const userDataDir =
3151
3151
  settings.userDataDirectory ||
3152
- path$g.join(app$a.getPath("userData"), appName$4);
3152
+ path$h.join(app$a.getPath("userData"), appName$4);
3153
3153
 
3154
3154
  console.log("[settingsController] Data directory retrieved successfully");
3155
3155
  // Return the data for ipcMain.handle() - modern promise-based approach
@@ -3176,17 +3176,17 @@ const settingsController$4 = {
3176
3176
  setDataDirectory: (win, newPath) => {
3177
3177
  try {
3178
3178
  // Validate the path exists and is a directory
3179
- if (!fs$b.existsSync(newPath)) {
3180
- fs$b.mkdirSync(newPath, { recursive: true });
3179
+ if (!fs$c.existsSync(newPath)) {
3180
+ fs$c.mkdirSync(newPath, { recursive: true });
3181
3181
  }
3182
3182
 
3183
- const stats = fs$b.statSync(newPath);
3183
+ const stats = fs$c.statSync(newPath);
3184
3184
  if (!stats.isDirectory()) {
3185
3185
  throw new Error("Path is not a directory");
3186
3186
  }
3187
3187
 
3188
3188
  // Update settings
3189
- const settingsPath = path$g.join(
3189
+ const settingsPath = path$h.join(
3190
3190
  app$a.getPath("userData"),
3191
3191
  appName$4,
3192
3192
  configFilename$3,
@@ -3220,11 +3220,11 @@ const settingsController$4 = {
3220
3220
  migrateDataDirectory: (win, oldPath, newPath) => {
3221
3221
  try {
3222
3222
  // Resolve paths to prevent traversal
3223
- const resolvedOldPath = path$g.resolve(oldPath);
3224
- const resolvedNewPath = path$g.resolve(newPath);
3223
+ const resolvedOldPath = path$h.resolve(oldPath);
3224
+ const resolvedNewPath = path$h.resolve(newPath);
3225
3225
 
3226
3226
  // Validate oldPath is the current configured data directory
3227
- const settingsCheckPath = path$g.join(
3227
+ const settingsCheckPath = path$h.join(
3228
3228
  app$a.getPath("userData"),
3229
3229
  appName$4,
3230
3230
  configFilename$3,
@@ -3232,8 +3232,8 @@ const settingsController$4 = {
3232
3232
  const currentSettings = getFileContents$4(settingsCheckPath, {});
3233
3233
  const currentDataDir =
3234
3234
  currentSettings.userDataDirectory ||
3235
- path$g.join(app$a.getPath("userData"), appName$4);
3236
- if (resolvedOldPath !== path$g.resolve(currentDataDir)) {
3235
+ path$h.join(app$a.getPath("userData"), appName$4);
3236
+ if (resolvedOldPath !== path$h.resolve(currentDataDir)) {
3237
3237
  throw new Error("Source path must be the current data directory");
3238
3238
  }
3239
3239
 
@@ -3257,19 +3257,19 @@ const settingsController$4 = {
3257
3257
  }
3258
3258
 
3259
3259
  // Validate paths
3260
- if (!fs$b.existsSync(resolvedOldPath)) {
3260
+ if (!fs$c.existsSync(resolvedOldPath)) {
3261
3261
  throw new Error("Source directory does not exist");
3262
3262
  }
3263
3263
 
3264
- if (!fs$b.existsSync(resolvedNewPath)) {
3265
- fs$b.mkdirSync(resolvedNewPath, { recursive: true });
3264
+ if (!fs$c.existsSync(resolvedNewPath)) {
3265
+ fs$c.mkdirSync(resolvedNewPath, { recursive: true });
3266
3266
  }
3267
3267
 
3268
3268
  // Copy files
3269
3269
  copyDirectory(resolvedOldPath, resolvedNewPath);
3270
3270
 
3271
3271
  // Update settings to use new path
3272
- const settingsPath = path$g.join(
3272
+ const settingsPath = path$h.join(
3273
3273
  app$a.getPath("userData"),
3274
3274
  appName$4,
3275
3275
  configFilename$3,
@@ -3935,7 +3935,7 @@ function requireProviderController () {
3935
3935
  }
3936
3936
 
3937
3937
  const { app: app$9 } = require$$0$1;
3938
- const path$f = require$$1$2;
3938
+ const path$g = require$$1$2;
3939
3939
  const { writeFileSync: writeFileSync$1 } = require$$0$2;
3940
3940
  const events$4 = events$8;
3941
3941
  const { getFileContents: getFileContents$3 } = file;
@@ -3955,7 +3955,7 @@ const layoutController$1 = {
3955
3955
  saveLayoutForApplication: (win, appId, layoutObject) => {
3956
3956
  try {
3957
3957
  // filename to the pages file (live pages)
3958
- const filename = path$f.join(
3958
+ const filename = path$g.join(
3959
3959
  app$9.getPath("userData"),
3960
3960
  appName$3,
3961
3961
  appId,
@@ -3988,7 +3988,7 @@ const layoutController$1 = {
3988
3988
  */
3989
3989
  listLayoutsForApplication: (win, appId) => {
3990
3990
  try {
3991
- const filename = path$f.join(
3991
+ const filename = path$g.join(
3992
3992
  app$9.getPath("userData"),
3993
3993
  appName$3,
3994
3994
  appId,
@@ -21192,8 +21192,8 @@ streamableHttp$1.StreamableHTTPClientTransport = StreamableHTTPClientTransport$1
21192
21192
  * clearCache() → void // test-only
21193
21193
  */
21194
21194
 
21195
- const fs$a = require$$0$2;
21196
- const path$e = require$$1$2;
21195
+ const fs$b = require$$0$2;
21196
+ const path$f = require$$1$2;
21197
21197
  const { app: app$8 } = require$$0$1;
21198
21198
 
21199
21199
  const FILE_NAME = "widgetMcpGrants.json";
@@ -21203,14 +21203,14 @@ const FILE_NAME = "widgetMcpGrants.json";
21203
21203
  let _cache$1 = null;
21204
21204
 
21205
21205
  function grantsFilePath() {
21206
- return path$e.join(app$8.getPath("userData"), FILE_NAME);
21206
+ return path$f.join(app$8.getPath("userData"), FILE_NAME);
21207
21207
  }
21208
21208
 
21209
21209
  function loadFromDisk() {
21210
21210
  const p = grantsFilePath();
21211
- if (!fs$a.existsSync(p)) return {};
21211
+ if (!fs$b.existsSync(p)) return {};
21212
21212
  try {
21213
- const raw = fs$a.readFileSync(p, "utf8");
21213
+ const raw = fs$b.readFileSync(p, "utf8");
21214
21214
  const parsed = JSON.parse(raw);
21215
21215
  if (!parsed || typeof parsed !== "object") return {};
21216
21216
  return parsed;
@@ -21230,16 +21230,27 @@ function writeToDisk(data) {
21230
21230
  const tmp = p + ".tmp";
21231
21231
  // Ensure parent dir exists (userData should already, but be defensive
21232
21232
  // for first-launch / freshly-cleared profile cases).
21233
- fs$a.mkdirSync(path$e.dirname(p), { recursive: true });
21234
- fs$a.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
21235
- fs$a.renameSync(tmp, p);
21233
+ fs$b.mkdirSync(path$f.dirname(p), { recursive: true });
21234
+ fs$b.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
21235
+ fs$b.renameSync(tmp, p);
21236
21236
  }
21237
21237
 
21238
+ // Recognized origins for a persisted grant. "declared" means the user
21239
+ // approved against the developer's declared dash.permissions.mcp block;
21240
+ // "discovered" means the install-time scanner produced a synthetic
21241
+ // manifest the user approved; "manual" means the user typed entries
21242
+ // themselves in Settings → Privacy & Security with no manifest backing.
21243
+ // Other values are dropped on persist (legacy grants stay null).
21244
+ const ALLOWED_GRANT_ORIGINS = new Set(["declared", "discovered", "manual"]);
21245
+
21238
21246
  /**
21239
21247
  * Sanitize a perms object before persisting. Drops unknown keys, coerces
21240
21248
  * arrays of strings, and silently ignores malformed servers. Mirrors the
21241
21249
  * shape produced by parseManifestPermissions so the gate reads either
21242
21250
  * declared or granted with the same code path.
21251
+ *
21252
+ * Optional `grantOrigin` field is preserved when it's one of the
21253
+ * recognized values; bogus values are dropped.
21243
21254
  */
21244
21255
  function sanitizePerms(perms) {
21245
21256
  if (!perms || typeof perms !== "object") return null;
@@ -21262,7 +21273,14 @@ function sanitizePerms(perms) {
21262
21273
  : [],
21263
21274
  };
21264
21275
  }
21265
- return { servers };
21276
+ const out = { servers };
21277
+ if (
21278
+ typeof perms.grantOrigin === "string" &&
21279
+ ALLOWED_GRANT_ORIGINS.has(perms.grantOrigin)
21280
+ ) {
21281
+ out.grantOrigin = perms.grantOrigin;
21282
+ }
21283
+ return out;
21266
21284
  }
21267
21285
 
21268
21286
  function getGrant$2(widgetId) {
@@ -21358,6 +21376,7 @@ var grantedPermissions = {
21358
21376
  revokeServer: revokeServer$1,
21359
21377
  listAllGrants: listAllGrants$1,
21360
21378
  clearCache: clearCache$1,
21379
+ ALLOWED_GRANT_ORIGINS,
21361
21380
  };
21362
21381
 
21363
21382
  /**
@@ -21585,6 +21604,120 @@ var mcpServerKey = {
21585
21604
  SEP,
21586
21605
  };
21587
21606
 
21607
+ /**
21608
+ * mcpScopeResolver.js
21609
+ *
21610
+ * Slice 3b: per-workspace path scope reconfiguration.
21611
+ *
21612
+ * The Slice-3a process isolation key (`workspaceId::serverName`) is the
21613
+ * lifecycle handle. This module computes WHAT the spawned process is
21614
+ * configured to see — the union of granted paths from widgets on the
21615
+ * active workspace, applied as credential overrides at spawn time.
21616
+ *
21617
+ * Design:
21618
+ * - The renderer enumerates widgets on the active workspace, looks up
21619
+ * each widget's grant via window.mainApi.widgetMcp.getGrant, and
21620
+ * hands the array to unionPathScope() to compute the workspace
21621
+ * scope for a given server (e.g. "filesystem").
21622
+ * - The renderer passes the resulting scope as `pathScope` to
21623
+ * mcpStartServer.
21624
+ * - mcpController applies the scope to credentials (replacing
21625
+ * allowedPaths etc.) before its existing argsMapping spreads them
21626
+ * into the spawn args. Servers that don't declare argsMapping for
21627
+ * the path keys are unaffected.
21628
+ *
21629
+ * Feature flag: the controller only applies the override when
21630
+ * `security.enforceWidgetMcpPermissions` is on. When off, server starts
21631
+ * with credentials as-configured (pre-3b behavior).
21632
+ *
21633
+ * Out of scope here:
21634
+ * - Hot-respawn on widget add/remove (see Slice 3b plan, deferred).
21635
+ * - Catalog schema for new path-scoped servers (filesystem already
21636
+ * has argsMapping.allowedPaths; others added as discovered).
21637
+ */
21638
+
21639
+ /**
21640
+ * Compute the workspace-scoped path union for a given server.
21641
+ *
21642
+ * @param {Array<{widgetId, granted}>} grants - widgets-on-workspace + their grants
21643
+ * @param {string} serverName - the MCP server name (e.g. "filesystem")
21644
+ * @returns {{ readPaths: string[], writePaths: string[], allowedPaths: string[] }}
21645
+ *
21646
+ * `allowedPaths` is the dedup union of read+write — used by
21647
+ * filesystem-style servers that take a single allowed-list. Servers
21648
+ * that distinguish read-only vs read-write can use the readPaths /
21649
+ * writePaths arrays directly.
21650
+ */
21651
+ function unionPathScope(grants, serverName) {
21652
+ const reads = new Set();
21653
+ const writes = new Set();
21654
+
21655
+ if (!Array.isArray(grants)) {
21656
+ return { readPaths: [], writePaths: [], allowedPaths: [] };
21657
+ }
21658
+
21659
+ for (const entry of grants) {
21660
+ if (!entry || typeof entry !== "object") continue;
21661
+ const granted = entry.granted;
21662
+ if (!granted || typeof granted !== "object") continue;
21663
+ const servers = granted.servers;
21664
+ if (!servers || typeof servers !== "object") continue;
21665
+ const serverPerms = servers[serverName];
21666
+ if (!serverPerms || typeof serverPerms !== "object") continue;
21667
+
21668
+ if (Array.isArray(serverPerms.readPaths)) {
21669
+ for (const p of serverPerms.readPaths) {
21670
+ if (typeof p === "string" && p) reads.add(p);
21671
+ }
21672
+ }
21673
+ if (Array.isArray(serverPerms.writePaths)) {
21674
+ for (const p of serverPerms.writePaths) {
21675
+ if (typeof p === "string" && p) writes.add(p);
21676
+ }
21677
+ }
21678
+ }
21679
+
21680
+ const readPaths = [...reads];
21681
+ const writePaths = [...writes];
21682
+ const allowedPaths = [...new Set([...reads, ...writes])];
21683
+
21684
+ return { readPaths, writePaths, allowedPaths };
21685
+ }
21686
+
21687
+ /**
21688
+ * Override credential keys with values derived from a path scope.
21689
+ *
21690
+ * Filesystem-style servers expect `allowedPaths` as a comma-separated
21691
+ * string (the catalog's `argsMapping.allowedPaths.split` then expands
21692
+ * it back into positional args at spawn time). This helper joins the
21693
+ * scope's allowedPaths to match that convention.
21694
+ *
21695
+ * Returns a NEW credentials object — does not mutate the input.
21696
+ *
21697
+ * If pathScope is empty (no granted paths at all), the existing
21698
+ * credentials are returned unchanged so the user's globally-configured
21699
+ * allowedPaths still works for the LLM tool path / NO_WORKSPACE bucket.
21700
+ */
21701
+ function applyPathScopeToCredentials$1(credentials, pathScope) {
21702
+ const base =
21703
+ credentials && typeof credentials === "object" ? { ...credentials } : {};
21704
+
21705
+ if (!pathScope || typeof pathScope !== "object") return base;
21706
+
21707
+ const allowed = Array.isArray(pathScope.allowedPaths)
21708
+ ? pathScope.allowedPaths
21709
+ : [];
21710
+ if (allowed.length === 0) return base;
21711
+
21712
+ base.allowedPaths = allowed.join(",");
21713
+ return base;
21714
+ }
21715
+
21716
+ var mcpScopeResolver = {
21717
+ unionPathScope,
21718
+ applyPathScopeToCredentials: applyPathScopeToCredentials$1,
21719
+ };
21720
+
21588
21721
  /**
21589
21722
  * mcpController.js
21590
21723
  *
@@ -21605,12 +21738,13 @@ const {
21605
21738
  const {
21606
21739
  StreamableHTTPClientTransport,
21607
21740
  } = streamableHttp$1;
21608
- const path$d = require$$1$2;
21609
- const fs$9 = require$$0$2;
21741
+ const path$e = require$$1$2;
21742
+ const fs$a = require$$0$2;
21610
21743
  const os$2 = require$$2$1;
21611
21744
  const responseCache$2 = responseCache_1;
21612
21745
  const { gateToolCall } = permissionGate;
21613
21746
  const { serverKey, parseServerKey } = mcpServerKey;
21747
+ const { applyPathScopeToCredentials } = mcpScopeResolver;
21614
21748
  const { app: app$7 } = require$$0$1;
21615
21749
 
21616
21750
  // Read the widget-MCP-enforcement feature flag from settings.json.
@@ -21619,13 +21753,13 @@ const { app: app$7 } = require$$0$1;
21619
21753
  // and electron/mcp/permissionGate.js for context.
21620
21754
  function isWidgetPermissionEnforcementEnabled() {
21621
21755
  try {
21622
- const settingsPath = path$d.join(
21756
+ const settingsPath = path$e.join(
21623
21757
  app$7.getPath("userData"),
21624
21758
  "Dashboard",
21625
21759
  "settings.json",
21626
21760
  );
21627
- if (!fs$9.existsSync(settingsPath)) return false;
21628
- const raw = fs$9.readFileSync(settingsPath, "utf8");
21761
+ if (!fs$a.existsSync(settingsPath)) return false;
21762
+ const raw = fs$a.readFileSync(settingsPath, "utf8");
21629
21763
  const settings = JSON.parse(raw);
21630
21764
  return Boolean(settings?.security?.enforceWidgetMcpPermissions);
21631
21765
  } catch (_e) {
@@ -21767,7 +21901,7 @@ function getShellPath$1() {
21767
21901
  return _shellPath$1;
21768
21902
  }
21769
21903
 
21770
- const { execSync } = require$$10;
21904
+ const { execSync } = require$$11;
21771
21905
  const fallbackDirs = ["/usr/local/bin", "/opt/homebrew/bin"];
21772
21906
 
21773
21907
  // Scan nvm versions, tracking both latest and best compatible version
@@ -21778,7 +21912,7 @@ function getShellPath$1() {
21778
21912
  fallbackDirs.push(`${home}/.nodenv/shims`);
21779
21913
  try {
21780
21914
  const nvmDir = `${home}/.nvm/versions/node`;
21781
- const versions = fs$9.readdirSync(nvmDir).sort();
21915
+ const versions = fs$a.readdirSync(nvmDir).sort();
21782
21916
  if (versions.length > 0) {
21783
21917
  // Find the highest compatible version (v18/v20/v22)
21784
21918
  for (let i = versions.length - 1; i >= 0; i--) {
@@ -21914,15 +22048,15 @@ async function refreshGoogleOAuthToken(tokenRefresh) {
21914
22048
  const credPath = tokenRefresh.credentialsPath.replace(/^~/, home);
21915
22049
  const keysPath = tokenRefresh.oauthKeysPath.replace(/^~/, home);
21916
22050
 
21917
- if (!fs$9.existsSync(credPath) || !fs$9.existsSync(keysPath)) {
22051
+ if (!fs$a.existsSync(credPath) || !fs$a.existsSync(keysPath)) {
21918
22052
  console.log(
21919
22053
  "[mcpController] Token refresh skipped: credential files not found",
21920
22054
  );
21921
22055
  return;
21922
22056
  }
21923
22057
 
21924
- const credentials = JSON.parse(fs$9.readFileSync(credPath, "utf8"));
21925
- const keysFile = JSON.parse(fs$9.readFileSync(keysPath, "utf8"));
22058
+ const credentials = JSON.parse(fs$a.readFileSync(credPath, "utf8"));
22059
+ const keysFile = JSON.parse(fs$a.readFileSync(keysPath, "utf8"));
21926
22060
  const keyData = keysFile.installed || keysFile.web;
21927
22061
 
21928
22062
  if (
@@ -21991,7 +22125,7 @@ async function refreshGoogleOAuthToken(tokenRefresh) {
21991
22125
  credentials.refresh_token = body.refresh_token;
21992
22126
  }
21993
22127
 
21994
- fs$9.writeFileSync(credPath, JSON.stringify(credentials, null, 2));
22128
+ fs$a.writeFileSync(credPath, JSON.stringify(credentials, null, 2));
21995
22129
  console.log("[mcpController] Google OAuth token refreshed successfully");
21996
22130
  }
21997
22131
 
@@ -22005,15 +22139,41 @@ const mcpController$3 = {
22005
22139
  * Pass `null`/`undefined` workspaceId to land on the legacy
22006
22140
  * NO_WORKSPACE bucket (e.g. dash MCP server tools, AI Builder previews).
22007
22141
  *
22142
+ * Slice 3b: when `security.enforceWidgetMcpPermissions` is on AND a
22143
+ * non-empty `pathScope` is supplied, the scope's allowed paths
22144
+ * override the server's existing path-style credentials before the
22145
+ * catalog's argsMapping spreads them into spawn args. This scopes
22146
+ * the server's OS-level capability to the union of widget grants on
22147
+ * the active workspace.
22148
+ *
22008
22149
  * @param {BrowserWindow} win the main window
22009
22150
  * @param {string} serverName unique name for this server instance
22010
22151
  * @param {object} mcpConfig { transport, command, args, envMapping }
22011
22152
  * @param {object} credentials decrypted credentials object
22012
22153
  * @param {string|null} workspaceId active workspace id (Slice 3a)
22154
+ * @param {object|null} pathScope { readPaths, writePaths, allowedPaths } (Slice 3b)
22013
22155
  * @returns {{ success, serverName, tools, status } | { error, message }}
22014
22156
  */
22015
- startServer: async (win, serverName, mcpConfig, credentials, workspaceId) => {
22157
+ startServer: async (
22158
+ win,
22159
+ serverName,
22160
+ mcpConfig,
22161
+ credentials,
22162
+ workspaceId,
22163
+ pathScope = null,
22164
+ ) => {
22016
22165
  const key = serverKey(workspaceId, serverName);
22166
+ // Slice 3b: when the gate is enforced, override credentials with
22167
+ // the workspace's union of granted paths so the spawned process
22168
+ // can't see anything broader than the user has consented to.
22169
+ if (
22170
+ isWidgetPermissionEnforcementEnabled() &&
22171
+ pathScope &&
22172
+ Array.isArray(pathScope.allowedPaths) &&
22173
+ pathScope.allowedPaths.length > 0
22174
+ ) {
22175
+ credentials = applyPathScopeToCredentials(credentials, pathScope);
22176
+ }
22017
22177
  // 1. Already connected? Return existing connection
22018
22178
  const existing = activeServers.get(key);
22019
22179
  if (existing && existing.status === STATUS$1.CONNECTED && existing.client) {
@@ -22152,7 +22312,7 @@ const mcpController$3 = {
22152
22312
  }
22153
22313
 
22154
22314
  // Interpolate {{MCP_DIR}} in args to resolve local MCP server scripts
22155
- const mcpDir = path$d.join(__dirname, "..", "mcp");
22315
+ const mcpDir = path$e.join(__dirname, "..", "mcp");
22156
22316
  for (let i = 0; i < args.length; i++) {
22157
22317
  if (
22158
22318
  typeof args[i] === "string" &&
@@ -22590,20 +22750,20 @@ const mcpController$3 = {
22590
22750
  */
22591
22751
  getCatalog: (win) => {
22592
22752
  try {
22593
- const catalogPath = path$d.join(
22753
+ const catalogPath = path$e.join(
22594
22754
  __dirname,
22595
22755
  "..",
22596
22756
  "mcp",
22597
22757
  "mcpServerCatalog.json",
22598
22758
  );
22599
22759
 
22600
- if (!fs$9.existsSync(catalogPath)) {
22760
+ if (!fs$a.existsSync(catalogPath)) {
22601
22761
  return {
22602
22762
  catalog: [],
22603
22763
  };
22604
22764
  }
22605
22765
 
22606
- const catalogData = fs$9.readFileSync(catalogPath, "utf8");
22766
+ const catalogData = fs$a.readFileSync(catalogPath, "utf8");
22607
22767
  const catalog = JSON.parse(catalogData);
22608
22768
 
22609
22769
  return {
@@ -22631,18 +22791,18 @@ const mcpController$3 = {
22631
22791
  */
22632
22792
  getKnownExternalCatalog: () => {
22633
22793
  try {
22634
- const catalogPath = path$d.join(
22794
+ const catalogPath = path$e.join(
22635
22795
  __dirname,
22636
22796
  "..",
22637
22797
  "mcp",
22638
22798
  "knownExternalMcpServers.json",
22639
22799
  );
22640
22800
 
22641
- if (!fs$9.existsSync(catalogPath)) {
22801
+ if (!fs$a.existsSync(catalogPath)) {
22642
22802
  return { success: true, servers: [] };
22643
22803
  }
22644
22804
 
22645
- const catalogData = fs$9.readFileSync(catalogPath, "utf8");
22805
+ const catalogData = fs$a.readFileSync(catalogPath, "utf8");
22646
22806
  const catalog = JSON.parse(catalogData);
22647
22807
 
22648
22808
  return {
@@ -22695,7 +22855,7 @@ const mcpController$3 = {
22695
22855
  * @returns {{ success } | { error, message }}
22696
22856
  */
22697
22857
  runAuth: async (win, mcpConfig, credentials, authCommand) => {
22698
- const { spawn } = require$$10;
22858
+ const { spawn } = require$$11;
22699
22859
 
22700
22860
  const env = cleanEnvForChildProcess();
22701
22861
 
@@ -22707,8 +22867,8 @@ const mcpController$3 = {
22707
22867
  const destPath = to.replace(/^~/, os$2.homedir());
22708
22868
  const destDir = require$$1$2.dirname(destPath);
22709
22869
  try {
22710
- fs$9.mkdirSync(destDir, { recursive: true });
22711
- fs$9.copyFileSync(sourcePath, destPath);
22870
+ fs$a.mkdirSync(destDir, { recursive: true });
22871
+ fs$a.copyFileSync(sourcePath, destPath);
22712
22872
  } catch (err) {
22713
22873
  return {
22714
22874
  error: true,
@@ -22744,7 +22904,7 @@ const mcpController$3 = {
22744
22904
  }
22745
22905
 
22746
22906
  // Interpolate {{MCP_DIR}} in authCommand args (same as startServer)
22747
- const mcpDir = path$d.join(__dirname, "..", "mcp");
22907
+ const mcpDir = path$e.join(__dirname, "..", "mcp");
22748
22908
  const resolvedArgs = (authCommand.args || []).map((arg) =>
22749
22909
  typeof arg === "string" && arg.includes("{{MCP_DIR}}")
22750
22910
  ? arg.replace(/\{\{MCP_DIR\}\}/g, mcpDir)
@@ -23292,8 +23452,8 @@ function commonjsRequire(path) {
23292
23452
  * Runs in the Electron main process at widget install time.
23293
23453
  */
23294
23454
 
23295
- const fs$8 = require$$0$2;
23296
- const path$c = require$$1$2;
23455
+ const fs$9 = require$$0$2;
23456
+ const path$d = require$$1$2;
23297
23457
 
23298
23458
  /**
23299
23459
  * Structured error thrown by compileWidget() when the underlying
@@ -23328,7 +23488,7 @@ function getEsbuildDiagnostics() {
23328
23488
 
23329
23489
  try {
23330
23490
  const pkgJsonPath = require.resolve("esbuild/package.json");
23331
- diagnostics.esbuildPackageDir = path$c.dirname(pkgJsonPath);
23491
+ diagnostics.esbuildPackageDir = path$d.dirname(pkgJsonPath);
23332
23492
  diagnostics.esbuildVersion = commonjsRequire(pkgJsonPath).version;
23333
23493
  } catch (err) {
23334
23494
  diagnostics.esbuildResolveError = err.message;
@@ -23338,15 +23498,15 @@ function getEsbuildDiagnostics() {
23338
23498
  const archPkgJson = require.resolve(
23339
23499
  `${diagnostics.archPackage}/package.json`,
23340
23500
  );
23341
- const archDir = path$c.dirname(archPkgJson);
23501
+ const archDir = path$d.dirname(archPkgJson);
23342
23502
  // esbuild's native binary on macOS/Linux is bin/esbuild;
23343
23503
  // on Windows it's esbuild.exe at the package root.
23344
23504
  const candidate =
23345
23505
  process.platform === "win32"
23346
- ? path$c.join(archDir, "esbuild.exe")
23347
- : path$c.join(archDir, "bin", "esbuild");
23506
+ ? path$d.join(archDir, "esbuild.exe")
23507
+ : path$d.join(archDir, "bin", "esbuild");
23348
23508
  diagnostics.nativeBinaryPath = candidate;
23349
- diagnostics.nativeBinaryExists = fs$8.existsSync(candidate);
23509
+ diagnostics.nativeBinaryExists = fs$9.existsSync(candidate);
23350
23510
  } catch (err) {
23351
23511
  diagnostics.archResolveError = err.message;
23352
23512
  }
@@ -23392,26 +23552,26 @@ async function healthCheck() {
23392
23552
  * @returns {string|null} Path to the widgets/ directory, or null
23393
23553
  */
23394
23554
  function findWidgetsDir$2(widgetPath) {
23395
- const direct = path$c.join(widgetPath, "widgets");
23396
- if (fs$8.existsSync(direct)) {
23555
+ const direct = path$d.join(widgetPath, "widgets");
23556
+ if (fs$9.existsSync(direct)) {
23397
23557
  return direct;
23398
23558
  }
23399
23559
 
23400
23560
  // Check configs/widgets/ (packageZip.js nests .dash.js files here)
23401
- const configsWidgets = path$c.join(widgetPath, "configs", "widgets");
23402
- if (fs$8.existsSync(configsWidgets)) {
23561
+ const configsWidgets = path$d.join(widgetPath, "configs", "widgets");
23562
+ if (fs$9.existsSync(configsWidgets)) {
23403
23563
  return configsWidgets;
23404
23564
  }
23405
23565
 
23406
23566
  // Check configs/ directory (used by packageZip.js for distributed widgets)
23407
- const configs = path$c.join(widgetPath, "configs");
23408
- if (fs$8.existsSync(configs)) {
23567
+ const configs = path$d.join(widgetPath, "configs");
23568
+ if (fs$9.existsSync(configs)) {
23409
23569
  return configs;
23410
23570
  }
23411
23571
 
23412
23572
  // Check one level deeper for nested ZIP extraction
23413
23573
  try {
23414
- const entries = fs$8.readdirSync(widgetPath, { withFileTypes: true });
23574
+ const entries = fs$9.readdirSync(widgetPath, { withFileTypes: true });
23415
23575
  const subdirs = entries.filter(
23416
23576
  (e) =>
23417
23577
  e.isDirectory() &&
@@ -23421,8 +23581,8 @@ function findWidgetsDir$2(widgetPath) {
23421
23581
  );
23422
23582
 
23423
23583
  for (const subdir of subdirs) {
23424
- const nested = path$c.join(widgetPath, subdir.name, "widgets");
23425
- if (fs$8.existsSync(nested)) {
23584
+ const nested = path$d.join(widgetPath, subdir.name, "widgets");
23585
+ if (fs$9.existsSync(nested)) {
23426
23586
  console.log(`[WidgetCompiler] Found nested widgets/ at ${nested}`);
23427
23587
  return nested;
23428
23588
  }
@@ -23456,7 +23616,7 @@ async function compileWidget$1(widgetPath) {
23456
23616
  }
23457
23617
 
23458
23618
  // Discover .dash.js config files
23459
- const files = fs$8.readdirSync(widgetsDir);
23619
+ const files = fs$9.readdirSync(widgetsDir);
23460
23620
  const dashFiles = files.filter((f) => f.endsWith(".dash.js"));
23461
23621
 
23462
23622
  if (dashFiles.length === 0) {
@@ -23470,15 +23630,15 @@ async function compileWidget$1(widgetPath) {
23470
23630
  // Compute relative path from the entry file (in widgetPath) to widgetsDir,
23471
23631
  // since widgetsDir may be nested (e.g., ./weather-widget/widgets/).
23472
23632
  const relWidgetsDir =
23473
- "./" + path$c.relative(widgetPath, widgetsDir).split(path$c.sep).join("/");
23633
+ "./" + path$d.relative(widgetPath, widgetsDir).split(path$d.sep).join("/");
23474
23634
  const imports = [];
23475
23635
  const exportParts = [];
23476
23636
 
23477
23637
  for (const dashFile of dashFiles) {
23478
23638
  const componentName = dashFile.replace(".dash.js", "");
23479
23639
  const componentFile = `${componentName}.js`;
23480
- const componentFilePath = path$c.join(widgetsDir, componentFile);
23481
- const hasComponent = fs$8.existsSync(componentFilePath);
23640
+ const componentFilePath = path$d.join(widgetsDir, componentFile);
23641
+ const hasComponent = fs$9.existsSync(componentFilePath);
23482
23642
 
23483
23643
  // Import the config (always)
23484
23644
  imports.push(
@@ -23510,17 +23670,17 @@ async function compileWidget$1(widgetPath) {
23510
23670
  const entryContent = [...imports, "", ...exportParts, ""].join("\n");
23511
23671
 
23512
23672
  // Write temporary entry file in the widget root
23513
- const entryPath = path$c.join(widgetPath, "__compile_entry.js");
23514
- const distDir = path$c.join(widgetPath, "dist");
23515
- const outPath = path$c.join(distDir, "index.cjs.js");
23673
+ const entryPath = path$d.join(widgetPath, "__compile_entry.js");
23674
+ const distDir = path$d.join(widgetPath, "dist");
23675
+ const outPath = path$d.join(distDir, "index.cjs.js");
23516
23676
 
23517
23677
  try {
23518
23678
  // Ensure dist/ directory exists
23519
- if (!fs$8.existsSync(distDir)) {
23520
- fs$8.mkdirSync(distDir, { recursive: true });
23679
+ if (!fs$9.existsSync(distDir)) {
23680
+ fs$9.mkdirSync(distDir, { recursive: true });
23521
23681
  }
23522
23682
 
23523
- fs$8.writeFileSync(entryPath, entryContent, "utf8");
23683
+ fs$9.writeFileSync(entryPath, entryContent, "utf8");
23524
23684
 
23525
23685
  console.log(
23526
23686
  `[WidgetCompiler] Compiling ${dashFiles.length} component(s) from ${widgetPath}`,
@@ -23576,8 +23736,8 @@ async function compileWidget$1(widgetPath) {
23576
23736
  } finally {
23577
23737
  // Clean up temporary entry file
23578
23738
  try {
23579
- if (fs$8.existsSync(entryPath)) {
23580
- fs$8.unlinkSync(entryPath);
23739
+ if (fs$9.existsSync(entryPath)) {
23740
+ fs$9.unlinkSync(entryPath);
23581
23741
  }
23582
23742
  } catch (cleanupError) {
23583
23743
  // Non-fatal
@@ -23609,8 +23769,8 @@ var widgetCompiler$1 = {
23609
23769
  * Integrates with ComponentManager for automatic registration
23610
23770
  */
23611
23771
 
23612
- const fs$7 = require$$0$2;
23613
- const path$b = require$$1$2;
23772
+ const fs$8 = require$$0$2;
23773
+ const path$c = require$$1$2;
23614
23774
  const vm = require$$2$2;
23615
23775
  const { findWidgetsDir: findWidgetsDir$1 } = widgetCompiler$1;
23616
23776
 
@@ -23698,14 +23858,14 @@ class DynamicWidgetLoader {
23698
23858
  );
23699
23859
 
23700
23860
  const widgetsDir =
23701
- findWidgetsDir$1(widgetPath) || path$b.join(widgetPath, "widgets");
23702
- const componentPath = path$b.join(widgetsDir, `${componentName}.js`);
23703
- const configPath = path$b.join(widgetsDir, `${componentName}.dash.js`);
23861
+ findWidgetsDir$1(widgetPath) || path$c.join(widgetPath, "widgets");
23862
+ const componentPath = path$c.join(widgetsDir, `${componentName}.js`);
23863
+ const configPath = path$c.join(widgetsDir, `${componentName}.dash.js`);
23704
23864
 
23705
- if (!fs$7.existsSync(componentPath)) {
23865
+ if (!fs$8.existsSync(componentPath)) {
23706
23866
  throw new Error(`Component file not found: ${componentPath}`);
23707
23867
  }
23708
- if (!fs$7.existsSync(configPath)) {
23868
+ if (!fs$8.existsSync(configPath)) {
23709
23869
  throw new Error(`Config file not found: ${configPath}`);
23710
23870
  }
23711
23871
 
@@ -23756,7 +23916,7 @@ class DynamicWidgetLoader {
23756
23916
  */
23757
23917
  async loadConfigFile(configPath) {
23758
23918
  try {
23759
- const source = fs$7.readFileSync(configPath, "utf8");
23919
+ const source = fs$8.readFileSync(configPath, "utf8");
23760
23920
 
23761
23921
  let exportMatch = source.match(/export\s+default\s+({[\s\S]*});?\s*$/);
23762
23922
 
@@ -23824,7 +23984,7 @@ class DynamicWidgetLoader {
23824
23984
  return [];
23825
23985
  }
23826
23986
 
23827
- const files = fs$7.readdirSync(widgetsDir);
23987
+ const files = fs$8.readdirSync(widgetsDir);
23828
23988
  const widgets = new Set();
23829
23989
 
23830
23990
  files.forEach((file) => {
@@ -23900,8 +24060,8 @@ var dynamicWidgetLoaderExports = dynamicWidgetLoader$3.exports;
23900
24060
  * Test-only. Drops the in-process cache so tests can re-read.
23901
24061
  */
23902
24062
 
23903
- const fs$6 = require$$0$2;
23904
- const path$a = require$$1$2;
24063
+ const fs$7 = require$$0$2;
24064
+ const path$b = require$$1$2;
23905
24065
  const os$1 = require$$2$1;
23906
24066
  const { app: app$6 } = require$$0$1;
23907
24067
 
@@ -23918,7 +24078,7 @@ function expandHome(p) {
23918
24078
  if (typeof p !== "string" || !p) return p;
23919
24079
  if (p === "~") return os$1.homedir();
23920
24080
  if (p.startsWith("~/") || p.startsWith("~\\")) {
23921
- return path$a.join(os$1.homedir(), p.slice(2));
24081
+ return path$b.join(os$1.homedir(), p.slice(2));
23922
24082
  }
23923
24083
  return p;
23924
24084
  }
@@ -23958,10 +24118,10 @@ function parseManifestPermissions(packageJson) {
23958
24118
  */
23959
24119
  function resolveWidgetPackagePath(widgetId) {
23960
24120
  if (typeof widgetId !== "string" || !widgetId) return null;
23961
- const widgetsRoot = path$a.join(app$6.getPath("userData"), "widgets");
24121
+ const widgetsRoot = path$b.join(app$6.getPath("userData"), "widgets");
23962
24122
  // Split scope from name for "@scope/name" form.
23963
24123
  const parts = widgetId.startsWith("@") ? widgetId.split("/") : [widgetId];
23964
- return path$a.join(widgetsRoot, ...parts, "package.json");
24124
+ return path$b.join(widgetsRoot, ...parts, "package.json");
23965
24125
  }
23966
24126
 
23967
24127
  /**
@@ -23975,12 +24135,12 @@ function resolveWidgetPackagePath(widgetId) {
23975
24135
  function getWidgetMcpPermissions$1(widgetId) {
23976
24136
  if (_cache.has(widgetId)) return _cache.get(widgetId);
23977
24137
  const pkgPath = resolveWidgetPackagePath(widgetId);
23978
- if (!pkgPath || !fs$6.existsSync(pkgPath)) {
24138
+ if (!pkgPath || !fs$7.existsSync(pkgPath)) {
23979
24139
  _cache.set(widgetId, null);
23980
24140
  return null;
23981
24141
  }
23982
24142
  try {
23983
- const raw = fs$6.readFileSync(pkgPath, "utf8");
24143
+ const raw = fs$7.readFileSync(pkgPath, "utf8");
23984
24144
  const pkg = JSON.parse(raw);
23985
24145
  const perms = parseManifestPermissions(pkg);
23986
24146
  _cache.set(widgetId, perms);
@@ -24008,6 +24168,200 @@ var widgetPermissions = {
24008
24168
  clearCache,
24009
24169
  };
24010
24170
 
24171
+ /**
24172
+ * manifestScanner.js
24173
+ *
24174
+ * Literal-only static scanner for widget MCP usage. Three callers:
24175
+ * 1. publish-time CLI (`dash-scan-manifest`) — the dev runs it before
24176
+ * shipping a widget; it diffs detected calls against the package's
24177
+ * `dash.permissions.mcp` block.
24178
+ * 2. install-time hook in widgetRegistry — when a widget arrives
24179
+ * without a manifest, the scanner is run on the installed source
24180
+ * and the result is offered as a "discovered" consent prompt.
24181
+ * 3. Future: lints during widget builds, IDE plugins, etc.
24182
+ *
24183
+ * Scope: detects literal-string `callTool("server","tool", ...)` and
24184
+ * `useMcpProvider("server")` patterns. Anything dynamic (variable,
24185
+ * template literal, function arg) is recorded as a `warnings[]` entry,
24186
+ * NOT a silent miss.
24187
+ *
24188
+ * This is a *linter*, not a security mechanism. The runtime gate
24189
+ * (Slices 1-3) is the actual boundary.
24190
+ */
24191
+
24192
+ const fs$6 = require$$0$2;
24193
+ const path$a = require$$1$2;
24194
+
24195
+ const SOURCE_EXTENSIONS = new Set([
24196
+ ".js",
24197
+ ".jsx",
24198
+ ".ts",
24199
+ ".tsx",
24200
+ ".mjs",
24201
+ ".cjs",
24202
+ ]);
24203
+ const SCAN_FILE_LIMIT = 200;
24204
+
24205
+ const CALL_TOOL_REGEX =
24206
+ /(?:mainApi\.mcp\.|window\.mainApi\.mcp\.|\b)callTool\s*\(\s*([^,)]+?)\s*,\s*([^,)]+?)\s*[,)]/g;
24207
+
24208
+ const USE_PROVIDER_REGEX = /useMcpProvider\s*\(\s*([^,)]+?)\s*[,)]/g;
24209
+
24210
+ function stripComments(src) {
24211
+ return src
24212
+ .replace(/\/\*[\s\S]*?\*\//g, "")
24213
+ .replace(/(^|[^:])\/\/.*$/gm, "$1");
24214
+ }
24215
+
24216
+ function tryLiteralString(arg) {
24217
+ if (typeof arg !== "string") return null;
24218
+ const trimmed = arg.trim();
24219
+ const m =
24220
+ /^"([^"\\]*(?:\\.[^"\\]*)*)"$/.exec(trimmed) ||
24221
+ /^'([^'\\]*(?:\\.[^'\\]*)*)'$/.exec(trimmed);
24222
+ if (!m) return null;
24223
+ return m[1];
24224
+ }
24225
+
24226
+ function lineNumberOf(src, charIndex) {
24227
+ let n = 1;
24228
+ for (let i = 0; i < charIndex && i < src.length; i++) {
24229
+ if (src.charCodeAt(i) === 10) n++;
24230
+ }
24231
+ return n;
24232
+ }
24233
+
24234
+ function readSourceFiles(dir) {
24235
+ const result = [];
24236
+ const skipDirs = new Set([
24237
+ "node_modules",
24238
+ "dist",
24239
+ "package",
24240
+ "build",
24241
+ ".git",
24242
+ ]);
24243
+ function walk(current, relBase) {
24244
+ let entries;
24245
+ try {
24246
+ entries = fs$6.readdirSync(current, { withFileTypes: true });
24247
+ } catch {
24248
+ return;
24249
+ }
24250
+ for (const entry of entries) {
24251
+ const abs = path$a.join(current, entry.name);
24252
+ const rel = relBase ? path$a.join(relBase, entry.name) : entry.name;
24253
+ if (entry.isDirectory()) {
24254
+ if (skipDirs.has(entry.name)) continue;
24255
+ walk(abs, rel);
24256
+ } else if (entry.isFile()) {
24257
+ const ext = path$a.extname(entry.name).toLowerCase();
24258
+ if (!SOURCE_EXTENSIONS.has(ext)) continue;
24259
+ if (result.length >= SCAN_FILE_LIMIT) return;
24260
+ try {
24261
+ result.push({
24262
+ relPath: rel,
24263
+ source: fs$6.readFileSync(abs, "utf8"),
24264
+ });
24265
+ } catch {
24266
+ // unreadable — skip
24267
+ }
24268
+ }
24269
+ }
24270
+ }
24271
+ walk(dir, "");
24272
+ return result;
24273
+ }
24274
+
24275
+ function scanForMcpUsage(input) {
24276
+ if (!input || typeof input !== "object") {
24277
+ return { servers: {}, warnings: [] };
24278
+ }
24279
+
24280
+ let fileList = [];
24281
+ if (input.files && typeof input.files === "object") {
24282
+ for (const [relPath, source] of Object.entries(input.files)) {
24283
+ const ext = path$a.extname(relPath).toLowerCase();
24284
+ if (!SOURCE_EXTENSIONS.has(ext)) continue;
24285
+ if (typeof source !== "string") continue;
24286
+ fileList.push({ relPath, source });
24287
+ }
24288
+ } else if (typeof input.dir === "string" && input.dir) {
24289
+ fileList = readSourceFiles(input.dir);
24290
+ }
24291
+
24292
+ const servers = {};
24293
+ const warnings = [];
24294
+
24295
+ function ensureServer(name) {
24296
+ if (!servers[name]) servers[name] = { tools: new Set() };
24297
+ return servers[name];
24298
+ }
24299
+
24300
+ for (const { relPath, source } of fileList) {
24301
+ const stripped = stripComments(source);
24302
+
24303
+ USE_PROVIDER_REGEX.lastIndex = 0;
24304
+ let m;
24305
+ while ((m = USE_PROVIDER_REGEX.exec(stripped)) !== null) {
24306
+ const lit = tryLiteralString(m[1]);
24307
+ const line = lineNumberOf(stripped, m.index);
24308
+ if (lit) {
24309
+ ensureServer(lit);
24310
+ } else {
24311
+ warnings.push({
24312
+ file: relPath,
24313
+ line,
24314
+ kind: "dynamic-server-name",
24315
+ snippet: m[0].trim(),
24316
+ });
24317
+ }
24318
+ }
24319
+
24320
+ CALL_TOOL_REGEX.lastIndex = 0;
24321
+ while ((m = CALL_TOOL_REGEX.exec(stripped)) !== null) {
24322
+ const serverLit = tryLiteralString(m[1]);
24323
+ const toolLit = tryLiteralString(m[2]);
24324
+ const line = lineNumberOf(stripped, m.index);
24325
+
24326
+ if (serverLit && toolLit) {
24327
+ ensureServer(serverLit).tools.add(toolLit);
24328
+ } else if (!serverLit && !toolLit) {
24329
+ warnings.push({
24330
+ file: relPath,
24331
+ line,
24332
+ kind: "dynamic-server-and-tool",
24333
+ snippet: m[0].trim(),
24334
+ });
24335
+ } else if (!serverLit) {
24336
+ warnings.push({
24337
+ file: relPath,
24338
+ line,
24339
+ kind: "dynamic-server-name",
24340
+ snippet: m[0].trim(),
24341
+ });
24342
+ } else {
24343
+ warnings.push({
24344
+ file: relPath,
24345
+ line,
24346
+ kind: "dynamic-tool-name",
24347
+ snippet: m[0].trim(),
24348
+ });
24349
+ }
24350
+ }
24351
+ }
24352
+
24353
+ const out = {};
24354
+ for (const [name, entry] of Object.entries(servers)) {
24355
+ out[name] = { tools: [...entry.tools].sort() };
24356
+ }
24357
+ return { servers: out, warnings };
24358
+ }
24359
+
24360
+ var manifestScanner = {
24361
+ scanForMcpUsage,
24362
+ SCAN_FILE_LIMIT,
24363
+ };
24364
+
24011
24365
  /**
24012
24366
  * schedulerController.js
24013
24367
  *
@@ -24552,6 +24906,7 @@ var schedulerController_1 = schedulerController$2;
24552
24906
  getWidgetMcpPermissions,
24553
24907
  clearCache: clearWidgetPermsCache,
24554
24908
  } = widgetPermissions;
24909
+ const { scanForMcpUsage } = manifestScanner;
24555
24910
 
24556
24911
  let WIDGETS_CACHE_DIR = null;
24557
24912
  let REGISTRY_CONFIG_FILE = null;
@@ -25633,6 +25988,14 @@ var schedulerController_1 = schedulerController$2;
25633
25988
  * with the user's selections (Slice 2) or quietly drops the message
25634
25989
  * (older renderer pre-Slice-2 — the gate still fail-closes).
25635
25990
  *
25991
+ * Fallback: if there's no manifest, run the literal-only scanner on the
25992
+ * installed source. If the scan finds any literal MCP usage, emit the
25993
+ * same event with `discovered: true` and a synthetic declared blob —
25994
+ * the consent modal renders this with amber framing so the user knows
25995
+ * they're approving a guess, not the developer's declaration. If the
25996
+ * scan finds nothing, no event fires; the widget appears in
25997
+ * Settings → Privacy & Security with a "Grant manually" button.
25998
+ *
25636
25999
  * Cache invalidation: widgetPermissions caches per-process, so an upgrade
25637
26000
  * over a stale cached entry would otherwise keep the old manifest. Drop
25638
26001
  * the whole cache here — cheap, infrequent.
@@ -25641,11 +26004,41 @@ var schedulerController_1 = schedulerController$2;
25641
26004
  try {
25642
26005
  clearWidgetPermsCache();
25643
26006
  const declared = getWidgetMcpPermissions(widgetName);
25644
- if (!declared) return;
26007
+ if (declared) {
26008
+ BrowserWindow.getAllWindows().forEach((win) => {
26009
+ win.webContents.send("widget:mcp-consent-required", {
26010
+ widgetId: widgetName,
26011
+ declared,
26012
+ });
26013
+ });
26014
+ return;
26015
+ }
26016
+
26017
+ // No manifest — try a scan of the installed source.
26018
+ if (!WIDGETS_CACHE_DIR) return;
26019
+ const widgetPath = path.join(WIDGETS_CACHE_DIR, ...widgetName.split("/"));
26020
+ if (!fs.existsSync(widgetPath)) return;
26021
+ const scanResult = scanForMcpUsage({ dir: widgetPath });
26022
+ const detectedServers = Object.keys(scanResult.servers);
26023
+ if (detectedServers.length === 0) return; // nothing actionable to prompt about
26024
+
26025
+ // Build a synthetic declared blob in the same shape parseManifestPermissions
26026
+ // produces so the consent modal can render it identically (modulo the
26027
+ // amber "discovered" framing).
26028
+ const syntheticServers = {};
26029
+ for (const [name, entry] of Object.entries(scanResult.servers)) {
26030
+ syntheticServers[name] = {
26031
+ tools: entry.tools,
26032
+ readPaths: [],
26033
+ writePaths: [],
26034
+ };
26035
+ }
25645
26036
  BrowserWindow.getAllWindows().forEach((win) => {
25646
26037
  win.webContents.send("widget:mcp-consent-required", {
25647
26038
  widgetId: widgetName,
25648
- declared,
26039
+ declared: { servers: syntheticServers },
26040
+ discovered: true,
26041
+ warnings: scanResult.warnings,
25649
26042
  });
25650
26043
  });
25651
26044
  } catch (e) {
@@ -47862,7 +48255,7 @@ var mcpDashServerController_1 = mcpDashServerController$4;
47862
48255
  * can use the Chat widget without a separate API key.
47863
48256
  */
47864
48257
 
47865
- const { spawn, execSync } = require$$10;
48258
+ const { spawn, execSync } = require$$11;
47866
48259
  const {
47867
48260
  LLM_STREAM_DELTA: LLM_STREAM_DELTA$2,
47868
48261
  LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$2,
@@ -59939,6 +60332,91 @@ const webSocketController$1 = {
59939
60332
 
59940
60333
  var webSocketController_1 = webSocketController$1;
59941
60334
 
60335
+ /**
60336
+ * widgetMcpGrantsListing.js
60337
+ *
60338
+ * Pure helper that joins three data sources into rows for the
60339
+ * Settings → Privacy & Security panel:
60340
+ * - installed widgets (from widgetRegistry.getWidgets())
60341
+ * - persisted grants (from grantedPermissions.listAllGrants())
60342
+ * - declared manifests (from widgetPermissions.getWidgetMcpPermissions())
60343
+ *
60344
+ * Output row shape:
60345
+ * {
60346
+ * widgetId: string,
60347
+ * declared: object|null, // dash.permissions.mcp block from package.json
60348
+ * granted: object|null, // user grant from widgetMcpGrants.json
60349
+ * hasManifest: boolean, // declared !== null
60350
+ * grantOrigin: string|null // "declared" | "discovered" | "manual" | null
60351
+ * }
60352
+ *
60353
+ * Includes ALL installed widgets (even unmanifested + ungranted) so the
60354
+ * panel can offer "Grant manually" for every widget. Also surfaces
60355
+ * orphan grants (granted but uninstalled — rare, only happens if
60356
+ * uninstall didn't revoke).
60357
+ */
60358
+
60359
+ function buildGrantsListing$1(
60360
+ installedWidgets,
60361
+ grantsByWidgetId,
60362
+ declaredByWidgetId,
60363
+ ) {
60364
+ const rows = [];
60365
+ const seen = new Set();
60366
+
60367
+ const installed = Array.isArray(installedWidgets) ? installedWidgets : [];
60368
+ const grants = grantsByWidgetId instanceof Map ? grantsByWidgetId : new Map();
60369
+ const declared =
60370
+ declaredByWidgetId instanceof Map ? declaredByWidgetId : new Map();
60371
+
60372
+ for (const w of installed) {
60373
+ if (!w || typeof w !== "object") continue;
60374
+ const widgetId = w.name;
60375
+ if (typeof widgetId !== "string" || !widgetId) continue;
60376
+ if (seen.has(widgetId)) continue;
60377
+ seen.add(widgetId);
60378
+
60379
+ const decl = declared.get(widgetId) || null;
60380
+ const grant = grants.get(widgetId) || null;
60381
+ const grantOrigin =
60382
+ grant &&
60383
+ typeof grant === "object" &&
60384
+ typeof grant.grantOrigin === "string"
60385
+ ? grant.grantOrigin
60386
+ : null;
60387
+
60388
+ rows.push({
60389
+ widgetId,
60390
+ declared: decl,
60391
+ granted: grant,
60392
+ hasManifest: decl !== null,
60393
+ grantOrigin,
60394
+ });
60395
+ }
60396
+
60397
+ // Orphan grants: granted but not in the installed list.
60398
+ for (const [widgetId, grant] of grants) {
60399
+ if (seen.has(widgetId)) continue;
60400
+ const grantOrigin =
60401
+ grant &&
60402
+ typeof grant === "object" &&
60403
+ typeof grant.grantOrigin === "string"
60404
+ ? grant.grantOrigin
60405
+ : null;
60406
+ rows.push({
60407
+ widgetId,
60408
+ declared: null,
60409
+ granted: grant,
60410
+ hasManifest: false,
60411
+ grantOrigin,
60412
+ });
60413
+ }
60414
+
60415
+ return rows;
60416
+ }
60417
+
60418
+ var widgetMcpGrantsListing = { buildGrantsListing: buildGrantsListing$1 };
60419
+
59942
60420
  /**
59943
60421
  * widgetMcpGrantsController.js
59944
60422
  *
@@ -59964,6 +60442,7 @@ const {
59964
60442
  } = grantedPermissions;
59965
60443
  const { getWidgetMcpPermissions } = widgetPermissions;
59966
60444
  const { getWidgetRegistry } = widgetRegistryExports;
60445
+ const { buildGrantsListing } = widgetMcpGrantsListing;
59967
60446
 
59968
60447
  function setupWidgetMcpGrantsHandlers() {
59969
60448
  ipcMain$1.handle("widget-mcp:get-grant", (event, widgetId) => {
@@ -59982,19 +60461,18 @@ function setupWidgetMcpGrantsHandlers() {
59982
60461
  return revokeServer(widgetId, serverName);
59983
60462
  });
59984
60463
 
59985
- // Joins all installed widgets with their declared and granted permission
59986
- // blocks. Returns one row per widget that has a declared manifest OR a
59987
- // grant — this surfaces both "freshly installed, awaiting consent" and
59988
- // "previously granted, now reviewable" cases in Settings.
60464
+ // Joins all installed widgets with their declared manifests + persisted
60465
+ // grants. Returns ONE row per installed widget regardless of whether it
60466
+ // has a manifest or grant — that's how the Settings panel can offer
60467
+ // "Grant manually" for unmanifested widgets. Plus orphan-grant rows for
60468
+ // granted-but-uninstalled cases. Logic delegated to
60469
+ // widgetMcpGrantsListing.buildGrantsListing for unit-testability.
59989
60470
  ipcMain$1.handle("widget-mcp:list-all", () => {
59990
60471
  const grantsByWidget = new Map();
59991
60472
  for (const { widgetId, granted } of listAllGrants()) {
59992
60473
  grantsByWidget.set(widgetId, granted);
59993
60474
  }
59994
60475
 
59995
- const rows = [];
59996
- const seen = new Set();
59997
-
59998
60476
  let installedWidgets = [];
59999
60477
  try {
60000
60478
  installedWidgets = getWidgetRegistry().getWidgets() || [];
@@ -60002,25 +60480,19 @@ function setupWidgetMcpGrantsHandlers() {
60002
60480
  // Registry not initialized yet; fall back to grants-only listing.
60003
60481
  }
60004
60482
 
60483
+ const declaredByWidget = new Map();
60005
60484
  for (const w of installedWidgets) {
60006
60485
  const widgetId = w?.name;
60007
- if (!widgetId || seen.has(widgetId)) continue;
60008
- seen.add(widgetId);
60486
+ if (!widgetId) continue;
60009
60487
  const declared = getWidgetMcpPermissions(widgetId);
60010
- const granted = grantsByWidget.get(widgetId) || null;
60011
- // Skip widgets with neither — they have nothing to show.
60012
- if (!declared && !granted) continue;
60013
- rows.push({ widgetId, declared, granted });
60488
+ if (declared) declaredByWidget.set(widgetId, declared);
60014
60489
  }
60015
60490
 
60016
- // Surface any grants whose widget is no longer installed (rare, but
60017
- // possible if uninstall didn't revoke). The user can still revoke them.
60018
- for (const [widgetId, granted] of grantsByWidget) {
60019
- if (seen.has(widgetId)) continue;
60020
- rows.push({ widgetId, declared: null, granted });
60021
- }
60022
-
60023
- return rows;
60491
+ return buildGrantsListing(
60492
+ installedWidgets,
60493
+ grantsByWidget,
60494
+ declaredByWidget,
60495
+ );
60024
60496
  });
60025
60497
  }
60026
60498
 
@@ -62253,14 +62725,25 @@ const mcpApi$2 = {
62253
62725
  * @param {object} credentials decrypted credentials object
62254
62726
  * @param {string|null} workspaceId active workspace id (Slice 3a) —
62255
62727
  * server processes are keyed per workspace.
62728
+ * @param {object|null} pathScope (Slice 3b) — when provided, the
62729
+ * workspace's union of granted paths overrides the server's
62730
+ * path-style credentials at spawn time. Shape:
62731
+ * `{ readPaths, writePaths, allowedPaths }`.
62256
62732
  * @returns {Promise<{ success, serverName, tools, status } | { error, message }>}
62257
62733
  */
62258
- startServer: (serverName, mcpConfig, credentials, workspaceId = null) =>
62734
+ startServer: (
62735
+ serverName,
62736
+ mcpConfig,
62737
+ credentials,
62738
+ workspaceId = null,
62739
+ pathScope = null,
62740
+ ) =>
62259
62741
  ipcRenderer$i.invoke(MCP_START_SERVER, {
62260
62742
  serverName,
62261
62743
  mcpConfig,
62262
62744
  credentials,
62263
62745
  workspaceId,
62746
+ pathScope,
62264
62747
  }),
62265
62748
 
62266
62749
  /**