@trops/dash-core 0.1.489 → 0.1.490

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.
@@ -1107,7 +1107,7 @@ var secureStoreController$1 = {
1107
1107
  getData: getData$1,
1108
1108
  };
1109
1109
 
1110
- const path$l = require$$1$2;
1110
+ const path$m = 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$l.dirname(filePath);
1128
+ var dirname = path$m.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$l.join(directory, file), (err) => {
1243
+ unlinkSync(path$m.join(directory, file), (err) => {
1244
1244
  if (err) throw err;
1245
1245
  });
1246
1246
  }
@@ -1257,8 +1257,8 @@ var file = {
1257
1257
  checkDirectory: checkDirectory$1,
1258
1258
  };
1259
1259
 
1260
- const { app: app$e } = require$$0$1;
1261
- const path$k = require$$1$2;
1260
+ const { app: app$f } = require$$0$1;
1261
+ const path$l = require$$1$2;
1262
1262
  const { writeFileSync: writeFileSync$3 } = require$$0$2;
1263
1263
  const { getFileContents: getFileContents$7 } = file;
1264
1264
 
@@ -1305,8 +1305,8 @@ 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$k.join(
1309
- app$e.getPath("userData"),
1308
+ const filename = path$l.join(
1309
+ app$f.getPath("userData"),
1310
1310
  appName$7,
1311
1311
  appId,
1312
1312
  configFilename$5,
@@ -1354,8 +1354,8 @@ const workspaceController$3 = {
1354
1354
  saveMenuItemsForApplication: (win, appId, menuItems) => {
1355
1355
  try {
1356
1356
  // filename to the workspaces file
1357
- const filename = path$k.join(
1358
- app$e.getPath("userData"),
1357
+ const filename = path$l.join(
1358
+ app$f.getPath("userData"),
1359
1359
  appName$7,
1360
1360
  appId,
1361
1361
  configFilename$5,
@@ -1403,8 +1403,8 @@ const workspaceController$3 = {
1403
1403
  */
1404
1404
  deleteWorkspaceForApplication: (win, appId, workspaceId) => {
1405
1405
  try {
1406
- const filename = path$k.join(
1407
- app$e.getPath("userData"),
1406
+ const filename = path$l.join(
1407
+ app$f.getPath("userData"),
1408
1408
  appName$7,
1409
1409
  appId,
1410
1410
  configFilename$5,
@@ -1437,8 +1437,8 @@ const workspaceController$3 = {
1437
1437
 
1438
1438
  listWorkspacesForApplication: (win, appId) => {
1439
1439
  try {
1440
- const filename = path$k.join(
1441
- app$e.getPath("userData"),
1440
+ const filename = path$l.join(
1441
+ app$f.getPath("userData"),
1442
1442
  appName$7,
1443
1443
  appId,
1444
1444
  configFilename$5,
@@ -1465,8 +1465,8 @@ const workspaceController$3 = {
1465
1465
 
1466
1466
  listMenuItemsForApplication: (win, appId) => {
1467
1467
  try {
1468
- const filename = path$k.join(
1469
- app$e.getPath("userData"),
1468
+ const filename = path$l.join(
1469
+ app$f.getPath("userData"),
1470
1470
  appName$7,
1471
1471
  appId,
1472
1472
  configFilename$5,
@@ -1509,8 +1509,8 @@ const workspaceController$3 = {
1509
1509
 
1510
1510
  var workspaceController_1 = workspaceController$3;
1511
1511
 
1512
- const { app: app$d } = require$$0$1;
1513
- const path$j = require$$1$2;
1512
+ const { app: app$e } = require$$0$1;
1513
+ const path$k = require$$1$2;
1514
1514
  const { writeFileSync: writeFileSync$2 } = require$$0$2;
1515
1515
  const { getFileContents: getFileContents$6 } = file;
1516
1516
 
@@ -1530,8 +1530,8 @@ 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$j.join(
1534
- app$d.getPath("userData"),
1533
+ const filename = path$k.join(
1534
+ app$e.getPath("userData"),
1535
1535
  appName$6,
1536
1536
  appId,
1537
1537
  configFilename$4,
@@ -1576,8 +1576,8 @@ const themeController$5 = {
1576
1576
  */
1577
1577
  listThemesForApplication: (win, appId) => {
1578
1578
  try {
1579
- const filename = path$j.join(
1580
- app$d.getPath("userData"),
1579
+ const filename = path$k.join(
1580
+ app$e.getPath("userData"),
1581
1581
  appName$6,
1582
1582
  appId,
1583
1583
  configFilename$4,
@@ -1618,8 +1618,8 @@ const themeController$5 = {
1618
1618
  */
1619
1619
  deleteThemeForApplication: (win, appId, themeKey) => {
1620
1620
  try {
1621
- const filename = path$j.join(
1622
- app$d.getPath("userData"),
1621
+ const filename = path$k.join(
1622
+ app$e.getPath("userData"),
1623
1623
  appName$6,
1624
1624
  appId,
1625
1625
  configFilename$4,
@@ -1695,9 +1695,9 @@ var themeController_1 = themeController$5;
1695
1695
  * `/data/`.
1696
1696
  */
1697
1697
 
1698
- const path$i = require$$1$2;
1699
- const fs$d = require$$0$2;
1700
- const { app: app$c } = require$$0$1;
1698
+ const path$j = require$$1$2;
1699
+ const fs$e = require$$0$2;
1700
+ const { app: app$d } = require$$0$1;
1701
1701
 
1702
1702
  const APP_NAME = "Dashboard";
1703
1703
 
@@ -1706,10 +1706,10 @@ const APP_NAME = "Dashboard";
1706
1706
  * @returns {string[]} ordered allowed roots for that category
1707
1707
  */
1708
1708
  function getAllowedRoots$2(category) {
1709
- const userData = app$c.getPath("userData");
1709
+ const userData = app$d.getPath("userData");
1710
1710
  switch (category) {
1711
1711
  case "data": {
1712
- const def = path$i.join(userData, APP_NAME, "data");
1712
+ const def = path$j.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,13 +1719,13 @@ function getAllowedRoots$2(category) {
1719
1719
  return override ? [def, override] : [def];
1720
1720
  }
1721
1721
  case "themes":
1722
- return [path$i.join(userData, APP_NAME, "themes")];
1722
+ return [path$j.join(userData, APP_NAME, "themes")];
1723
1723
  case "widgets":
1724
- return [path$i.join(userData, "widgets")];
1724
+ return [path$j.join(userData, "widgets")];
1725
1725
  case "plugins":
1726
- return [path$i.join(userData, "plugins")];
1726
+ return [path$j.join(userData, "plugins")];
1727
1727
  case "downloads":
1728
- return [app$c.getPath("downloads")];
1728
+ return [app$d.getPath("downloads")];
1729
1729
  default:
1730
1730
  throw new Error("safePath: unknown allowed-roots category: " + category);
1731
1731
  }
@@ -1740,13 +1740,13 @@ function getAllowedRoots$2(category) {
1740
1740
  */
1741
1741
  function readDataDirectoryFromSettings() {
1742
1742
  try {
1743
- const settingsPath = path$i.join(
1744
- app$c.getPath("userData"),
1743
+ const settingsPath = path$j.join(
1744
+ app$d.getPath("userData"),
1745
1745
  APP_NAME,
1746
1746
  "settings.json",
1747
1747
  );
1748
- if (!fs$d.existsSync(settingsPath)) return undefined;
1749
- const raw = fs$d.readFileSync(settingsPath, "utf8");
1748
+ if (!fs$e.existsSync(settingsPath)) return undefined;
1749
+ const raw = fs$e.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$i.resolve(requested);
1775
+ const resolved = path$j.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$d.realpathSync(resolved);
1782
+ real = fs$e.realpathSync(resolved);
1783
1783
  } catch (_e) {
1784
1784
  try {
1785
- const parent = fs$d.realpathSync(path$i.dirname(resolved));
1786
- real = path$i.join(parent, path$i.basename(resolved));
1785
+ const parent = fs$e.realpathSync(path$j.dirname(resolved));
1786
+ real = path$j.join(parent, path$j.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$d.existsSync(root)) realRoot = fs$d.realpathSync(root);
1798
+ if (fs$e.existsSync(root)) realRoot = fs$e.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$i.sep)) {
1805
+ if (real === realRoot || real.startsWith(realRoot + path$j.sep)) {
1806
1806
  return real;
1807
1807
  }
1808
1808
  }
@@ -2060,15 +2060,15 @@ var safeJsExecutor$1 = {
2060
2060
  * - CSV -> JSON
2061
2061
  */
2062
2062
 
2063
- var fs$c = require$$0$2;
2063
+ var fs$d = 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$h = require$$1$2;
2071
- const { app: app$b } = require$$0$1;
2070
+ const path$i = require$$1$2;
2071
+ const { app: app$c } = require$$0$1;
2072
2072
  const { ensureDirectoryExistence: ensureDirectoryExistence$1 } = file;
2073
2073
  const safeJsExecutor = safeJsExecutor$1;
2074
2074
 
@@ -2122,7 +2122,7 @@ let Transform$1 = class Transform {
2122
2122
  let lineObject = [];
2123
2123
 
2124
2124
  const readInterface = readline.createInterface({
2125
- input: fs$c.createReadStream(filepath),
2125
+ input: fs$d.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$c.createReadStream(file).pipe(parser);
2160
+ const readStream = fs$d.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$c.createReadStream(filepath);
2213
+ const xmlFileReadStream = fs$d.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$c.createWriteStream(outpath);
2224
+ const writeStream = fs$d.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$c
2276
+ const readStream = fs$d
2277
2277
  .createReadStream(filepath)
2278
2278
  .pipe(csv({ separator: delimiter }));
2279
- const writeStream = fs$c.createWriteStream(outpath);
2279
+ const writeStream = fs$d.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$h.join(app$b.getPath("userData"), TRANSFORM_APP_NAME);
2366
- const resolvedFilepath = path$h.resolve(filepath);
2367
- const resolvedOutFilepath = path$h.resolve(outFilepath);
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);
2368
2368
 
2369
- if (!resolvedFilepath.startsWith(appDataDir + path$h.sep)) {
2369
+ if (!resolvedFilepath.startsWith(appDataDir + path$i.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$h.sep)) {
2376
+ if (!resolvedOutFilepath.startsWith(appDataDir + path$i.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$c.existsSync(resolvedFilepath)) {
2387
+ if (!fs$d.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$c.createReadStream(resolvedFilepath).pipe(parser);
2392
+ var readStream = fs$d.createReadStream(resolvedFilepath).pipe(parser);
2393
2393
 
2394
2394
  ensureDirectoryExistence$1(resolvedOutFilepath);
2395
2395
 
2396
- var writeStream = fs$c.createWriteStream(resolvedOutFilepath);
2396
+ var writeStream = fs$d.createWriteStream(resolvedOutFilepath);
2397
2397
 
2398
2398
  let sep = "";
2399
2399
  let count = 0;
@@ -2505,9 +2505,9 @@ let Transform$1 = class Transform {
2505
2505
 
2506
2506
  var transform = Transform$1;
2507
2507
 
2508
- const { app: app$a } = require$$0$1;
2509
- var fs$b = require$$0$2;
2510
- const path$g = require$$1$2;
2508
+ const { app: app$b } = require$$0$1;
2509
+ var fs$c = require$$0$2;
2510
+ const path$h = 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,8 +2532,8 @@ 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$g.join(
2536
- app$a.getPath("userData"),
2535
+ const candidate = path$h.join(
2536
+ app$b.getPath("userData"),
2537
2537
  appName$5,
2538
2538
  appId,
2539
2539
  "data",
@@ -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$b.createWriteStream(resolvedFilepath);
2694
+ const writeStream = fs$c.createWriteStream(resolvedFilepath);
2695
2695
 
2696
2696
  https$3
2697
2697
  .get(url, (resp) => {
@@ -2855,8 +2855,8 @@ 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$g.join(
2859
- app$a.getPath("userData"),
2858
+ const candidate = path$h.join(
2859
+ app$b.getPath("userData"),
2860
2860
  appName$5,
2861
2861
  "data",
2862
2862
  filename,
@@ -2948,8 +2948,8 @@ const dataController$1 = {
2948
2948
  try {
2949
2949
  if (filename) {
2950
2950
  // filename to the pages file (live pages)
2951
- const fromFilename = path$g.join(
2952
- app$a.getPath("userData"),
2951
+ const fromFilename = path$h.join(
2952
+ app$b.getPath("userData"),
2953
2953
  appName$5,
2954
2954
  "data",
2955
2955
  filename,
@@ -3029,9 +3029,9 @@ var dataController_1 = dataController$1;
3029
3029
  * settingsController
3030
3030
  */
3031
3031
 
3032
- const { app: app$9 } = require$$0$1;
3033
- const path$f = require$$1$2;
3034
- const fs$a = require$$0$2;
3032
+ const { app: app$a } = require$$0$1;
3033
+ const path$g = require$$1$2;
3034
+ const fs$b = 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$a.existsSync(destination)) {
3043
- fs$a.mkdirSync(destination, { recursive: true });
3042
+ if (!fs$b.existsSync(destination)) {
3043
+ fs$b.mkdirSync(destination, { recursive: true });
3044
3044
  }
3045
3045
 
3046
- const files = fs$a.readdirSync(source);
3046
+ const files = fs$b.readdirSync(source);
3047
3047
  for (const file of files) {
3048
- const srcPath = path$f.join(source, file);
3049
- const destPath = path$f.join(destination, file);
3050
- const stat = fs$a.lstatSync(srcPath);
3048
+ const srcPath = path$g.join(source, file);
3049
+ const destPath = path$g.join(destination, file);
3050
+ const stat = fs$b.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$a.copyFileSync(srcPath, destPath);
3061
+ fs$b.copyFileSync(srcPath, destPath);
3062
3062
  }
3063
3063
  }
3064
3064
  }
@@ -3074,8 +3074,8 @@ const settingsController$4 = {
3074
3074
  try {
3075
3075
  if (data) {
3076
3076
  // <appId>/settings.json
3077
- const filename = path$f.join(
3078
- app$9.getPath("userData"),
3077
+ const filename = path$g.join(
3078
+ app$a.getPath("userData"),
3079
3079
  appName$4,
3080
3080
  configFilename$3,
3081
3081
  );
@@ -3110,8 +3110,8 @@ const settingsController$4 = {
3110
3110
  getSettingsForApplication: (win) => {
3111
3111
  try {
3112
3112
  // <appId>/settings.json
3113
- const filename = path$f.join(
3114
- app$9.getPath("userData"),
3113
+ const filename = path$g.join(
3114
+ app$a.getPath("userData"),
3115
3115
  appName$4,
3116
3116
  configFilename$3,
3117
3117
  );
@@ -3141,15 +3141,15 @@ const settingsController$4 = {
3141
3141
  */
3142
3142
  getDataDirectory: (win) => {
3143
3143
  try {
3144
- const settingsPath = path$f.join(
3145
- app$9.getPath("userData"),
3144
+ const settingsPath = path$g.join(
3145
+ app$a.getPath("userData"),
3146
3146
  appName$4,
3147
3147
  configFilename$3,
3148
3148
  );
3149
3149
  const settings = getFileContents$4(settingsPath, {});
3150
3150
  const userDataDir =
3151
3151
  settings.userDataDirectory ||
3152
- path$f.join(app$9.getPath("userData"), appName$4);
3152
+ path$g.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,18 +3176,18 @@ const settingsController$4 = {
3176
3176
  setDataDirectory: (win, newPath) => {
3177
3177
  try {
3178
3178
  // Validate the path exists and is a directory
3179
- if (!fs$a.existsSync(newPath)) {
3180
- fs$a.mkdirSync(newPath, { recursive: true });
3179
+ if (!fs$b.existsSync(newPath)) {
3180
+ fs$b.mkdirSync(newPath, { recursive: true });
3181
3181
  }
3182
3182
 
3183
- const stats = fs$a.statSync(newPath);
3183
+ const stats = fs$b.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$f.join(
3190
- app$9.getPath("userData"),
3189
+ const settingsPath = path$g.join(
3190
+ app$a.getPath("userData"),
3191
3191
  appName$4,
3192
3192
  configFilename$3,
3193
3193
  );
@@ -3220,20 +3220,20 @@ const settingsController$4 = {
3220
3220
  migrateDataDirectory: (win, oldPath, newPath) => {
3221
3221
  try {
3222
3222
  // Resolve paths to prevent traversal
3223
- const resolvedOldPath = path$f.resolve(oldPath);
3224
- const resolvedNewPath = path$f.resolve(newPath);
3223
+ const resolvedOldPath = path$g.resolve(oldPath);
3224
+ const resolvedNewPath = path$g.resolve(newPath);
3225
3225
 
3226
3226
  // Validate oldPath is the current configured data directory
3227
- const settingsCheckPath = path$f.join(
3228
- app$9.getPath("userData"),
3227
+ const settingsCheckPath = path$g.join(
3228
+ app$a.getPath("userData"),
3229
3229
  appName$4,
3230
3230
  configFilename$3,
3231
3231
  );
3232
3232
  const currentSettings = getFileContents$4(settingsCheckPath, {});
3233
3233
  const currentDataDir =
3234
3234
  currentSettings.userDataDirectory ||
3235
- path$f.join(app$9.getPath("userData"), appName$4);
3236
- if (resolvedOldPath !== path$f.resolve(currentDataDir)) {
3235
+ path$g.join(app$a.getPath("userData"), appName$4);
3236
+ if (resolvedOldPath !== path$g.resolve(currentDataDir)) {
3237
3237
  throw new Error("Source path must be the current data directory");
3238
3238
  }
3239
3239
 
@@ -3257,20 +3257,20 @@ const settingsController$4 = {
3257
3257
  }
3258
3258
 
3259
3259
  // Validate paths
3260
- if (!fs$a.existsSync(resolvedOldPath)) {
3260
+ if (!fs$b.existsSync(resolvedOldPath)) {
3261
3261
  throw new Error("Source directory does not exist");
3262
3262
  }
3263
3263
 
3264
- if (!fs$a.existsSync(resolvedNewPath)) {
3265
- fs$a.mkdirSync(resolvedNewPath, { recursive: true });
3264
+ if (!fs$b.existsSync(resolvedNewPath)) {
3265
+ fs$b.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$f.join(
3273
- app$9.getPath("userData"),
3272
+ const settingsPath = path$g.join(
3273
+ app$a.getPath("userData"),
3274
3274
  appName$4,
3275
3275
  configFilename$3,
3276
3276
  );
@@ -3934,8 +3934,8 @@ function requireProviderController () {
3934
3934
  return providerController_1;
3935
3935
  }
3936
3936
 
3937
- const { app: app$8 } = require$$0$1;
3938
- const path$e = require$$1$2;
3937
+ const { app: app$9 } = require$$0$1;
3938
+ const path$f = 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,8 +3955,8 @@ 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$e.join(
3959
- app$8.getPath("userData"),
3958
+ const filename = path$f.join(
3959
+ app$9.getPath("userData"),
3960
3960
  appName$3,
3961
3961
  appId,
3962
3962
  configFilename$2,
@@ -3988,8 +3988,8 @@ const layoutController$1 = {
3988
3988
  */
3989
3989
  listLayoutsForApplication: (win, appId) => {
3990
3990
  try {
3991
- const filename = path$e.join(
3992
- app$8.getPath("userData"),
3991
+ const filename = path$f.join(
3992
+ app$9.getPath("userData"),
3993
3993
  appName$3,
3994
3994
  appId,
3995
3995
  configFilename$2,
@@ -21153,154 +21153,211 @@ let StreamableHTTPClientTransport$1 = class StreamableHTTPClientTransport {
21153
21153
  streamableHttp$1.StreamableHTTPClientTransport = StreamableHTTPClientTransport$1;
21154
21154
 
21155
21155
  /**
21156
- * widgetPermissions.js
21156
+ * grantedPermissions.js
21157
21157
  *
21158
- * Read and parse the `dash.permissions.mcp` block from an installed
21159
- * widget's package.json.
21158
+ * Stores the user's actual MCP permission grants per widget. This is the
21159
+ * Slice-2 enforcement source of truth — separate from the widget's declared
21160
+ * `dash.permissions.mcp` block (which is just a request).
21160
21161
  *
21161
- * Manifest format (declared by widget authors in their package.json):
21162
+ * The runtime gate (permissionGate.gateToolCall) reads from here only.
21163
+ * A widget with a declared manifest but no grant entry has no access:
21164
+ * fail-closed. The user grants permissions at install time (consent modal)
21165
+ * or later in Settings → Privacy & Security.
21166
+ *
21167
+ * Storage: userData/widgetMcpGrants.json. Atomic writes via tmp + rename.
21162
21168
  *
21169
+ * Shape on disk:
21163
21170
  * {
21164
- * "name": "@trops/notes-summarizer",
21165
- * "dash": {
21166
- * "permissions": {
21167
- * "mcp": {
21168
- * "filesystem": {
21169
- * "tools": ["read_file", "list_directory"],
21170
- * "readPaths": ["~/Documents/notes"],
21171
- * "writePaths": []
21172
- * },
21173
- * "github": {
21174
- * "tools": ["search_repositories", "get_file_contents"]
21175
- * }
21171
+ * "@trops/notes-summarizer": {
21172
+ * "servers": {
21173
+ * "filesystem": {
21174
+ * "tools": ["read_file"],
21175
+ * "readPaths": ["/Users/jane/Documents/notes"],
21176
+ * "writePaths": []
21176
21177
  * }
21177
21178
  * }
21178
21179
  * }
21179
21180
  * }
21180
21181
  *
21181
- * Path strings beginning with `~` are expanded to the user's home
21182
- * directory at parse time. Tool-only servers (no path I/O, e.g.
21183
- * github) omit the `readPaths`/`writePaths` keys.
21182
+ * Note: paths are stored as-is (already tilde-expanded by the manifest
21183
+ * parser before grants are written). Tests can re-expand via
21184
+ * widgetPermissions.expandHome if they store ~ literals.
21184
21185
  *
21185
21186
  * Public API:
21186
- *
21187
- * getWidgetMcpPermissions(widgetId) → permissions | null
21188
- * Returns the parsed permissions for a widget, or null if the
21189
- * widget is unmanifested. Cached per process.
21190
- *
21191
- * parseManifestPermissions(packageJson) → permissions | null
21192
- * Pure function — exposed for tests.
21193
- *
21194
- * clearCache() → void
21195
- * Test-only. Drops the in-process cache so tests can re-read.
21187
+ * getGrant(widgetId) → grant | null
21188
+ * setGrant(widgetId, perms) → boolean
21189
+ * revokeGrant(widgetId) boolean
21190
+ * revokeServer(widgetId, serverName) boolean
21191
+ * listAllGrants() → [{ widgetId, granted }]
21192
+ * clearCache() → void // test-only
21196
21193
  */
21197
21194
 
21198
- const fs$9 = require$$0$2;
21199
- const path$d = require$$1$2;
21200
- const os$2 = require$$2$1;
21201
- const { app: app$7 } = require$$0$1;
21195
+ const fs$a = require$$0$2;
21196
+ const path$e = require$$1$2;
21197
+ const { app: app$8 } = require$$0$1;
21202
21198
 
21203
- // Cache: widgetId → permissions | null. Populated lazily on first
21204
- // lookup; invalidated when a widget is installed/uninstalled (the
21205
- // install/uninstall paths call clearCache()).
21206
- const _cache = new Map();
21199
+ const FILE_NAME = "widgetMcpGrants.json";
21207
21200
 
21208
- /**
21209
- * Expand a leading "~" to the user's home directory. Other paths are
21210
- * returned as-is.
21211
- */
21212
- function expandHome(p) {
21213
- if (typeof p !== "string" || !p) return p;
21214
- if (p === "~") return os$2.homedir();
21215
- if (p.startsWith("~/") || p.startsWith("~\\")) {
21216
- return path$d.join(os$2.homedir(), p.slice(2));
21201
+ // In-process cache of the entire grants file. Lazily loaded; invalidated
21202
+ // on every write.
21203
+ let _cache$1 = null;
21204
+
21205
+ function grantsFilePath() {
21206
+ return path$e.join(app$8.getPath("userData"), FILE_NAME);
21207
+ }
21208
+
21209
+ function loadFromDisk() {
21210
+ const p = grantsFilePath();
21211
+ if (!fs$a.existsSync(p)) return {};
21212
+ try {
21213
+ const raw = fs$a.readFileSync(p, "utf8");
21214
+ const parsed = JSON.parse(raw);
21215
+ if (!parsed || typeof parsed !== "object") return {};
21216
+ return parsed;
21217
+ } catch (e) {
21218
+ console.warn("[grantedPermissions] failed to read " + p + ": " + e.message);
21219
+ return {};
21217
21220
  }
21218
- return p;
21221
+ }
21222
+
21223
+ function ensureCache() {
21224
+ if (_cache$1 === null) _cache$1 = loadFromDisk();
21225
+ return _cache$1;
21226
+ }
21227
+
21228
+ function writeToDisk(data) {
21229
+ const p = grantsFilePath();
21230
+ const tmp = p + ".tmp";
21231
+ // Ensure parent dir exists (userData should already, but be defensive
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);
21219
21236
  }
21220
21237
 
21221
21238
  /**
21222
- * Parse a widget's package.json contents into a normalized permissions
21223
- * object. Returns null if no `dash.permissions.mcp` block exists.
21239
+ * Sanitize a perms object before persisting. Drops unknown keys, coerces
21240
+ * arrays of strings, and silently ignores malformed servers. Mirrors the
21241
+ * shape produced by parseManifestPermissions so the gate reads either
21242
+ * declared or granted with the same code path.
21224
21243
  */
21225
- function parseManifestPermissions(packageJson) {
21226
- if (!packageJson || typeof packageJson !== "object") return null;
21227
- const mcp = packageJson?.dash?.permissions?.mcp;
21228
- if (!mcp || typeof mcp !== "object") return null;
21244
+ function sanitizePerms(perms) {
21245
+ if (!perms || typeof perms !== "object") return null;
21246
+ const rawServers =
21247
+ perms.servers && typeof perms.servers === "object" ? perms.servers : null;
21248
+ if (!rawServers) return null;
21229
21249
 
21230
21250
  const servers = {};
21231
- for (const [serverName, raw] of Object.entries(mcp)) {
21251
+ for (const [name, raw] of Object.entries(rawServers)) {
21232
21252
  if (!raw || typeof raw !== "object") continue;
21233
- const tools = Array.isArray(raw.tools)
21234
- ? raw.tools.filter((t) => typeof t === "string")
21235
- : [];
21236
- const readPaths = Array.isArray(raw.readPaths)
21237
- ? raw.readPaths.filter((p) => typeof p === "string").map(expandHome)
21238
- : [];
21239
- const writePaths = Array.isArray(raw.writePaths)
21240
- ? raw.writePaths.filter((p) => typeof p === "string").map(expandHome)
21241
- : [];
21242
- servers[serverName] = { tools, readPaths, writePaths };
21253
+ servers[name] = {
21254
+ tools: Array.isArray(raw.tools)
21255
+ ? raw.tools.filter((t) => typeof t === "string")
21256
+ : [],
21257
+ readPaths: Array.isArray(raw.readPaths)
21258
+ ? raw.readPaths.filter((p) => typeof p === "string")
21259
+ : [],
21260
+ writePaths: Array.isArray(raw.writePaths)
21261
+ ? raw.writePaths.filter((p) => typeof p === "string")
21262
+ : [],
21263
+ };
21243
21264
  }
21244
-
21245
21265
  return { servers };
21246
21266
  }
21247
21267
 
21248
- /**
21249
- * Find a widget's installed package.json on disk. Widgets live under
21250
- * userData/widgets/<scope>/<name>/ for scoped packages or
21251
- * userData/widgets/<name>/ for unscoped. The widgetId is the npm
21252
- * package name (e.g. "@trops/notes-summarizer" or "notes-summarizer").
21253
- */
21254
- function resolveWidgetPackagePath(widgetId) {
21268
+ function getGrant$2(widgetId) {
21255
21269
  if (typeof widgetId !== "string" || !widgetId) return null;
21256
- const widgetsRoot = path$d.join(app$7.getPath("userData"), "widgets");
21257
- // Split scope from name for "@scope/name" form.
21258
- const parts = widgetId.startsWith("@") ? widgetId.split("/") : [widgetId];
21259
- return path$d.join(widgetsRoot, ...parts, "package.json");
21270
+ const all = ensureCache();
21271
+ return all[widgetId] || null;
21260
21272
  }
21261
21273
 
21262
- /**
21263
- * Read and parse a widget's MCP permissions. Returns null if:
21264
- * - the widget directory doesn't exist
21265
- * - package.json is unreadable / malformed
21266
- * - the widget hasn't declared dash.permissions.mcp
21267
- *
21268
- * Result is cached per widgetId for the lifetime of this process.
21269
- */
21270
- function getWidgetMcpPermissions$1(widgetId) {
21271
- if (_cache.has(widgetId)) return _cache.get(widgetId);
21272
- const pkgPath = resolveWidgetPackagePath(widgetId);
21273
- if (!pkgPath || !fs$9.existsSync(pkgPath)) {
21274
- _cache.set(widgetId, null);
21275
- return null;
21274
+ function setGrant$1(widgetId, perms) {
21275
+ if (typeof widgetId !== "string" || !widgetId) return false;
21276
+ const sanitized = sanitizePerms(perms);
21277
+ if (!sanitized) return false;
21278
+ const all = ensureCache();
21279
+ all[widgetId] = sanitized;
21280
+ try {
21281
+ writeToDisk(all);
21282
+ return true;
21283
+ } catch (e) {
21284
+ console.warn(
21285
+ "[grantedPermissions] failed to write grant for " +
21286
+ widgetId +
21287
+ ": " +
21288
+ e.message,
21289
+ );
21290
+ // Roll back the cache entry so memory matches disk.
21291
+ _cache$1 = loadFromDisk();
21292
+ return false;
21276
21293
  }
21294
+ }
21295
+
21296
+ function revokeGrant$1(widgetId) {
21297
+ if (typeof widgetId !== "string" || !widgetId) return false;
21298
+ const all = ensureCache();
21299
+ if (!Object.prototype.hasOwnProperty.call(all, widgetId)) return false;
21300
+ delete all[widgetId];
21277
21301
  try {
21278
- const raw = fs$9.readFileSync(pkgPath, "utf8");
21279
- const pkg = JSON.parse(raw);
21280
- const perms = parseManifestPermissions(pkg);
21281
- _cache.set(widgetId, perms);
21282
- return perms;
21302
+ writeToDisk(all);
21303
+ return true;
21283
21304
  } catch (e) {
21284
21305
  console.warn(
21285
- "[widgetPermissions] failed to read package.json for " +
21306
+ "[grantedPermissions] failed to revoke grant for " +
21286
21307
  widgetId +
21287
21308
  ": " +
21288
21309
  e.message,
21289
21310
  );
21290
- _cache.set(widgetId, null);
21291
- return null;
21311
+ _cache$1 = loadFromDisk();
21312
+ return false;
21292
21313
  }
21293
21314
  }
21294
21315
 
21295
- function clearCache() {
21296
- _cache.clear();
21316
+ function revokeServer$1(widgetId, serverName) {
21317
+ if (typeof widgetId !== "string" || !widgetId) return false;
21318
+ if (typeof serverName !== "string" || !serverName) return false;
21319
+ const all = ensureCache();
21320
+ const widgetEntry = all[widgetId];
21321
+ if (!widgetEntry || !widgetEntry.servers) return false;
21322
+ if (!Object.prototype.hasOwnProperty.call(widgetEntry.servers, serverName))
21323
+ return false;
21324
+ delete widgetEntry.servers[serverName];
21325
+ try {
21326
+ writeToDisk(all);
21327
+ return true;
21328
+ } catch (e) {
21329
+ console.warn(
21330
+ "[grantedPermissions] failed to revoke server " +
21331
+ serverName +
21332
+ " for " +
21333
+ widgetId +
21334
+ ": " +
21335
+ e.message,
21336
+ );
21337
+ _cache$1 = loadFromDisk();
21338
+ return false;
21339
+ }
21297
21340
  }
21298
21341
 
21299
- var widgetPermissions = {
21300
- getWidgetMcpPermissions: getWidgetMcpPermissions$1,
21301
- parseManifestPermissions,
21302
- expandHome,
21303
- clearCache,
21342
+ function listAllGrants$1() {
21343
+ const all = ensureCache();
21344
+ return Object.entries(all).map(([widgetId, granted]) => ({
21345
+ widgetId,
21346
+ granted,
21347
+ }));
21348
+ }
21349
+
21350
+ function clearCache$1() {
21351
+ _cache$1 = null;
21352
+ }
21353
+
21354
+ var grantedPermissions = {
21355
+ getGrant: getGrant$2,
21356
+ setGrant: setGrant$1,
21357
+ revokeGrant: revokeGrant$1,
21358
+ revokeServer: revokeServer$1,
21359
+ listAllGrants: listAllGrants$1,
21360
+ clearCache: clearCache$1,
21304
21361
  };
21305
21362
 
21306
21363
  /**
@@ -21309,32 +21366,40 @@ var widgetPermissions = {
21309
21366
  * Per-widget gating for MCP tool calls.
21310
21367
  *
21311
21368
  * When `gateToolCall` is invoked with a widget identity, server name,
21312
- * tool name, and tool arguments, it consults the widget's installed
21313
- * permission manifest (electron/mcp/widgetPermissions.js) and either
21314
- * permits the call or returns a clear denial reason.
21369
+ * tool name, and tool arguments, it consults the widget's GRANTED
21370
+ * permissions (electron/mcp/grantedPermissions.js) and either permits
21371
+ * the call or returns a clear denial reason.
21372
+ *
21373
+ * **Granted vs declared (Slice 2):** the widget's package.json
21374
+ * `dash.permissions.mcp` block is the *request* — read by
21375
+ * widgetPermissions.js and shown to the user at install time. The
21376
+ * *grant* is what the user actually approved. The runtime gate reads
21377
+ * grants only. A widget with a declared manifest but no grant entry
21378
+ * has no MCP access — fail-closed. The user grants permissions via
21379
+ * the install consent modal or Settings → Privacy & Security.
21315
21380
  *
21316
21381
  * Two layers:
21317
21382
  *
21318
- * 1. **Tool-name allowlist** — the manifest's `tools[]` array for the
21383
+ * 1. **Tool-name allowlist** — the granted `tools[]` array for the
21319
21384
  * target server determines which tool names this widget may
21320
21385
  * invoke. Anything outside the list is rejected.
21321
21386
  *
21322
21387
  * 2. **Path-argument containment** — for tools whose arguments
21323
21388
  * include a path-shaped key (`path`, `uri`, `filepath`, `file`,
21324
21389
  * `directory`), the supplied path is validated with safePath()
21325
- * against the widget's declared `readPaths` or `writePaths` for
21390
+ * against the widget's granted `readPaths` or `writePaths` for
21326
21391
  * the target server. The read/write distinction is heuristic
21327
21392
  * based on the tool name (e.g. `write_file` is treated as a
21328
21393
  * write).
21329
21394
  *
21330
- * This is the runtime enforcement layer. Install-time consent UI and
21331
- * per-dashboard MCP-server scope reconfiguration are separate plans
21332
- * (Slices 2 and 3). When the feature flag is OFF (default), this gate
21333
- * is bypassed entirely; mcpController behaves as before. When ON,
21334
- * every callTool dispatch goes through this gate.
21395
+ * This is the runtime enforcement layer. Install-time consent UI is
21396
+ * Slice 2. Per-dashboard MCP-server scope reconfiguration is Slice 3.
21397
+ * When the feature flag is OFF (default), this gate is bypassed
21398
+ * entirely; mcpController behaves as before. When ON, every callTool
21399
+ * dispatch goes through this gate.
21335
21400
  */
21336
21401
 
21337
- const { getWidgetMcpPermissions } = widgetPermissions;
21402
+ const { getGrant: getGrant$1 } = grantedPermissions;
21338
21403
  const { safePath: safePath$1 } = safePath_1;
21339
21404
 
21340
21405
  // Argument keys that look like paths. Different MCP servers use
@@ -21365,18 +21430,18 @@ function gateToolCall$1({ widgetId, serverName, toolName, args }) {
21365
21430
  };
21366
21431
  }
21367
21432
 
21368
- const perms = getWidgetMcpPermissions(widgetId);
21433
+ const perms = getGrant$1(widgetId);
21369
21434
  if (!perms) {
21370
21435
  return {
21371
21436
  allow: false,
21372
21437
  reason:
21373
21438
  "widget '" +
21374
21439
  widgetId +
21375
- "' has no MCP permission manifest; declare dash.permissions.mcp in its package.json to grant access",
21440
+ "' has no MCP permissions granted; user must approve at install time or in Settings Privacy & Security",
21376
21441
  };
21377
21442
  }
21378
21443
 
21379
- const serverPerms = perms.servers[serverName];
21444
+ const serverPerms = perms.servers && perms.servers[serverName];
21380
21445
  if (!serverPerms) {
21381
21446
  return {
21382
21447
  allow: false,
@@ -21477,12 +21542,12 @@ const {
21477
21542
  const {
21478
21543
  StreamableHTTPClientTransport,
21479
21544
  } = streamableHttp$1;
21480
- const path$c = require$$1$2;
21481
- const fs$8 = require$$0$2;
21482
- const os$1 = require$$2$1;
21545
+ const path$d = require$$1$2;
21546
+ const fs$9 = require$$0$2;
21547
+ const os$2 = require$$2$1;
21483
21548
  const responseCache$2 = responseCache_1;
21484
21549
  const { gateToolCall } = permissionGate;
21485
- const { app: app$6 } = require$$0$1;
21550
+ const { app: app$7 } = require$$0$1;
21486
21551
 
21487
21552
  // Read the widget-MCP-enforcement feature flag from settings.json.
21488
21553
  // Default is OFF — flipping ON activates per-widget gating in
@@ -21490,13 +21555,13 @@ const { app: app$6 } = require$$0$1;
21490
21555
  // and electron/mcp/permissionGate.js for context.
21491
21556
  function isWidgetPermissionEnforcementEnabled() {
21492
21557
  try {
21493
- const settingsPath = path$c.join(
21494
- app$6.getPath("userData"),
21558
+ const settingsPath = path$d.join(
21559
+ app$7.getPath("userData"),
21495
21560
  "Dashboard",
21496
21561
  "settings.json",
21497
21562
  );
21498
- if (!fs$8.existsSync(settingsPath)) return false;
21499
- const raw = fs$8.readFileSync(settingsPath, "utf8");
21563
+ if (!fs$9.existsSync(settingsPath)) return false;
21564
+ const raw = fs$9.readFileSync(settingsPath, "utf8");
21500
21565
  const settings = JSON.parse(raw);
21501
21566
  return Boolean(settings?.security?.enforceWidgetMcpPermissions);
21502
21567
  } catch (_e) {
@@ -21642,14 +21707,14 @@ function getShellPath$1() {
21642
21707
  const fallbackDirs = ["/usr/local/bin", "/opt/homebrew/bin"];
21643
21708
 
21644
21709
  // Scan nvm versions, tracking both latest and best compatible version
21645
- const home = os$1.homedir();
21710
+ const home = os$2.homedir();
21646
21711
  let compatibleNvmBin = null;
21647
21712
  if (home) {
21648
21713
  fallbackDirs.push(`${home}/.volta/bin`);
21649
21714
  fallbackDirs.push(`${home}/.nodenv/shims`);
21650
21715
  try {
21651
21716
  const nvmDir = `${home}/.nvm/versions/node`;
21652
- const versions = fs$8.readdirSync(nvmDir).sort();
21717
+ const versions = fs$9.readdirSync(nvmDir).sort();
21653
21718
  if (versions.length > 0) {
21654
21719
  // Find the highest compatible version (v18/v20/v22)
21655
21720
  for (let i = versions.length - 1; i >= 0; i--) {
@@ -21781,19 +21846,19 @@ function interpolate$1(template, credentials) {
21781
21846
  * @param {object} tokenRefresh { credentialsPath, oauthKeysPath }
21782
21847
  */
21783
21848
  async function refreshGoogleOAuthToken(tokenRefresh) {
21784
- const home = os$1.homedir();
21849
+ const home = os$2.homedir();
21785
21850
  const credPath = tokenRefresh.credentialsPath.replace(/^~/, home);
21786
21851
  const keysPath = tokenRefresh.oauthKeysPath.replace(/^~/, home);
21787
21852
 
21788
- if (!fs$8.existsSync(credPath) || !fs$8.existsSync(keysPath)) {
21853
+ if (!fs$9.existsSync(credPath) || !fs$9.existsSync(keysPath)) {
21789
21854
  console.log(
21790
21855
  "[mcpController] Token refresh skipped: credential files not found",
21791
21856
  );
21792
21857
  return;
21793
21858
  }
21794
21859
 
21795
- const credentials = JSON.parse(fs$8.readFileSync(credPath, "utf8"));
21796
- const keysFile = JSON.parse(fs$8.readFileSync(keysPath, "utf8"));
21860
+ const credentials = JSON.parse(fs$9.readFileSync(credPath, "utf8"));
21861
+ const keysFile = JSON.parse(fs$9.readFileSync(keysPath, "utf8"));
21797
21862
  const keyData = keysFile.installed || keysFile.web;
21798
21863
 
21799
21864
  if (
@@ -21862,7 +21927,7 @@ async function refreshGoogleOAuthToken(tokenRefresh) {
21862
21927
  credentials.refresh_token = body.refresh_token;
21863
21928
  }
21864
21929
 
21865
- fs$8.writeFileSync(credPath, JSON.stringify(credentials, null, 2));
21930
+ fs$9.writeFileSync(credPath, JSON.stringify(credentials, null, 2));
21866
21931
  console.log("[mcpController] Google OAuth token refreshed successfully");
21867
21932
  }
21868
21933
 
@@ -21977,7 +22042,7 @@ const mcpController$3 = {
21977
22042
  // Merge static env vars from mcpConfig (with ~ expansion)
21978
22043
  if (mcpConfig.staticEnv) {
21979
22044
  Object.entries(mcpConfig.staticEnv).forEach(([envVar, value]) => {
21980
- env[envVar] = value.replace(/^~/, os$1.homedir());
22045
+ env[envVar] = value.replace(/^~/, os$2.homedir());
21981
22046
  });
21982
22047
  }
21983
22048
 
@@ -22016,7 +22081,7 @@ const mcpController$3 = {
22016
22081
  }
22017
22082
 
22018
22083
  // Interpolate {{MCP_DIR}} in args to resolve local MCP server scripts
22019
- const mcpDir = path$c.join(__dirname, "..", "mcp");
22084
+ const mcpDir = path$d.join(__dirname, "..", "mcp");
22020
22085
  for (let i = 0; i < args.length; i++) {
22021
22086
  if (
22022
22087
  typeof args[i] === "string" &&
@@ -22433,20 +22498,20 @@ const mcpController$3 = {
22433
22498
  */
22434
22499
  getCatalog: (win) => {
22435
22500
  try {
22436
- const catalogPath = path$c.join(
22501
+ const catalogPath = path$d.join(
22437
22502
  __dirname,
22438
22503
  "..",
22439
22504
  "mcp",
22440
22505
  "mcpServerCatalog.json",
22441
22506
  );
22442
22507
 
22443
- if (!fs$8.existsSync(catalogPath)) {
22508
+ if (!fs$9.existsSync(catalogPath)) {
22444
22509
  return {
22445
22510
  catalog: [],
22446
22511
  };
22447
22512
  }
22448
22513
 
22449
- const catalogData = fs$8.readFileSync(catalogPath, "utf8");
22514
+ const catalogData = fs$9.readFileSync(catalogPath, "utf8");
22450
22515
  const catalog = JSON.parse(catalogData);
22451
22516
 
22452
22517
  return {
@@ -22474,18 +22539,18 @@ const mcpController$3 = {
22474
22539
  */
22475
22540
  getKnownExternalCatalog: () => {
22476
22541
  try {
22477
- const catalogPath = path$c.join(
22542
+ const catalogPath = path$d.join(
22478
22543
  __dirname,
22479
22544
  "..",
22480
22545
  "mcp",
22481
22546
  "knownExternalMcpServers.json",
22482
22547
  );
22483
22548
 
22484
- if (!fs$8.existsSync(catalogPath)) {
22549
+ if (!fs$9.existsSync(catalogPath)) {
22485
22550
  return { success: true, servers: [] };
22486
22551
  }
22487
22552
 
22488
- const catalogData = fs$8.readFileSync(catalogPath, "utf8");
22553
+ const catalogData = fs$9.readFileSync(catalogPath, "utf8");
22489
22554
  const catalog = JSON.parse(catalogData);
22490
22555
 
22491
22556
  return {
@@ -22547,11 +22612,11 @@ const mcpController$3 = {
22547
22612
  const { from, to } = authCommand.setup.copyCredential;
22548
22613
  const sourcePath = credentials?.[from];
22549
22614
  if (sourcePath) {
22550
- const destPath = to.replace(/^~/, os$1.homedir());
22615
+ const destPath = to.replace(/^~/, os$2.homedir());
22551
22616
  const destDir = require$$1$2.dirname(destPath);
22552
22617
  try {
22553
- fs$8.mkdirSync(destDir, { recursive: true });
22554
- fs$8.copyFileSync(sourcePath, destPath);
22618
+ fs$9.mkdirSync(destDir, { recursive: true });
22619
+ fs$9.copyFileSync(sourcePath, destPath);
22555
22620
  } catch (err) {
22556
22621
  return {
22557
22622
  error: true,
@@ -22582,12 +22647,12 @@ const mcpController$3 = {
22582
22647
  // Merge static env vars from authCommand with ~ expansion
22583
22648
  if (authCommand.staticEnv) {
22584
22649
  Object.entries(authCommand.staticEnv).forEach(([key, value]) => {
22585
- env[key] = value.replace(/^~/, os$1.homedir());
22650
+ env[key] = value.replace(/^~/, os$2.homedir());
22586
22651
  });
22587
22652
  }
22588
22653
 
22589
22654
  // Interpolate {{MCP_DIR}} in authCommand args (same as startServer)
22590
- const mcpDir = path$c.join(__dirname, "..", "mcp");
22655
+ const mcpDir = path$d.join(__dirname, "..", "mcp");
22591
22656
  const resolvedArgs = (authCommand.args || []).map((arg) =>
22592
22657
  typeof arg === "string" && arg.includes("{{MCP_DIR}}")
22593
22658
  ? arg.replace(/\{\{MCP_DIR\}\}/g, mcpDir)
@@ -23108,8 +23173,8 @@ function commonjsRequire(path) {
23108
23173
  * Runs in the Electron main process at widget install time.
23109
23174
  */
23110
23175
 
23111
- const fs$7 = require$$0$2;
23112
- const path$b = require$$1$2;
23176
+ const fs$8 = require$$0$2;
23177
+ const path$c = require$$1$2;
23113
23178
 
23114
23179
  /**
23115
23180
  * Structured error thrown by compileWidget() when the underlying
@@ -23144,7 +23209,7 @@ function getEsbuildDiagnostics() {
23144
23209
 
23145
23210
  try {
23146
23211
  const pkgJsonPath = require.resolve("esbuild/package.json");
23147
- diagnostics.esbuildPackageDir = path$b.dirname(pkgJsonPath);
23212
+ diagnostics.esbuildPackageDir = path$c.dirname(pkgJsonPath);
23148
23213
  diagnostics.esbuildVersion = commonjsRequire(pkgJsonPath).version;
23149
23214
  } catch (err) {
23150
23215
  diagnostics.esbuildResolveError = err.message;
@@ -23154,15 +23219,15 @@ function getEsbuildDiagnostics() {
23154
23219
  const archPkgJson = require.resolve(
23155
23220
  `${diagnostics.archPackage}/package.json`,
23156
23221
  );
23157
- const archDir = path$b.dirname(archPkgJson);
23222
+ const archDir = path$c.dirname(archPkgJson);
23158
23223
  // esbuild's native binary on macOS/Linux is bin/esbuild;
23159
23224
  // on Windows it's esbuild.exe at the package root.
23160
23225
  const candidate =
23161
23226
  process.platform === "win32"
23162
- ? path$b.join(archDir, "esbuild.exe")
23163
- : path$b.join(archDir, "bin", "esbuild");
23227
+ ? path$c.join(archDir, "esbuild.exe")
23228
+ : path$c.join(archDir, "bin", "esbuild");
23164
23229
  diagnostics.nativeBinaryPath = candidate;
23165
- diagnostics.nativeBinaryExists = fs$7.existsSync(candidate);
23230
+ diagnostics.nativeBinaryExists = fs$8.existsSync(candidate);
23166
23231
  } catch (err) {
23167
23232
  diagnostics.archResolveError = err.message;
23168
23233
  }
@@ -23208,26 +23273,26 @@ async function healthCheck() {
23208
23273
  * @returns {string|null} Path to the widgets/ directory, or null
23209
23274
  */
23210
23275
  function findWidgetsDir$2(widgetPath) {
23211
- const direct = path$b.join(widgetPath, "widgets");
23212
- if (fs$7.existsSync(direct)) {
23276
+ const direct = path$c.join(widgetPath, "widgets");
23277
+ if (fs$8.existsSync(direct)) {
23213
23278
  return direct;
23214
23279
  }
23215
23280
 
23216
23281
  // Check configs/widgets/ (packageZip.js nests .dash.js files here)
23217
- const configsWidgets = path$b.join(widgetPath, "configs", "widgets");
23218
- if (fs$7.existsSync(configsWidgets)) {
23282
+ const configsWidgets = path$c.join(widgetPath, "configs", "widgets");
23283
+ if (fs$8.existsSync(configsWidgets)) {
23219
23284
  return configsWidgets;
23220
23285
  }
23221
23286
 
23222
23287
  // Check configs/ directory (used by packageZip.js for distributed widgets)
23223
- const configs = path$b.join(widgetPath, "configs");
23224
- if (fs$7.existsSync(configs)) {
23288
+ const configs = path$c.join(widgetPath, "configs");
23289
+ if (fs$8.existsSync(configs)) {
23225
23290
  return configs;
23226
23291
  }
23227
23292
 
23228
23293
  // Check one level deeper for nested ZIP extraction
23229
23294
  try {
23230
- const entries = fs$7.readdirSync(widgetPath, { withFileTypes: true });
23295
+ const entries = fs$8.readdirSync(widgetPath, { withFileTypes: true });
23231
23296
  const subdirs = entries.filter(
23232
23297
  (e) =>
23233
23298
  e.isDirectory() &&
@@ -23237,8 +23302,8 @@ function findWidgetsDir$2(widgetPath) {
23237
23302
  );
23238
23303
 
23239
23304
  for (const subdir of subdirs) {
23240
- const nested = path$b.join(widgetPath, subdir.name, "widgets");
23241
- if (fs$7.existsSync(nested)) {
23305
+ const nested = path$c.join(widgetPath, subdir.name, "widgets");
23306
+ if (fs$8.existsSync(nested)) {
23242
23307
  console.log(`[WidgetCompiler] Found nested widgets/ at ${nested}`);
23243
23308
  return nested;
23244
23309
  }
@@ -23272,7 +23337,7 @@ async function compileWidget$1(widgetPath) {
23272
23337
  }
23273
23338
 
23274
23339
  // Discover .dash.js config files
23275
- const files = fs$7.readdirSync(widgetsDir);
23340
+ const files = fs$8.readdirSync(widgetsDir);
23276
23341
  const dashFiles = files.filter((f) => f.endsWith(".dash.js"));
23277
23342
 
23278
23343
  if (dashFiles.length === 0) {
@@ -23286,15 +23351,15 @@ async function compileWidget$1(widgetPath) {
23286
23351
  // Compute relative path from the entry file (in widgetPath) to widgetsDir,
23287
23352
  // since widgetsDir may be nested (e.g., ./weather-widget/widgets/).
23288
23353
  const relWidgetsDir =
23289
- "./" + path$b.relative(widgetPath, widgetsDir).split(path$b.sep).join("/");
23354
+ "./" + path$c.relative(widgetPath, widgetsDir).split(path$c.sep).join("/");
23290
23355
  const imports = [];
23291
23356
  const exportParts = [];
23292
23357
 
23293
23358
  for (const dashFile of dashFiles) {
23294
23359
  const componentName = dashFile.replace(".dash.js", "");
23295
23360
  const componentFile = `${componentName}.js`;
23296
- const componentFilePath = path$b.join(widgetsDir, componentFile);
23297
- const hasComponent = fs$7.existsSync(componentFilePath);
23361
+ const componentFilePath = path$c.join(widgetsDir, componentFile);
23362
+ const hasComponent = fs$8.existsSync(componentFilePath);
23298
23363
 
23299
23364
  // Import the config (always)
23300
23365
  imports.push(
@@ -23326,17 +23391,17 @@ async function compileWidget$1(widgetPath) {
23326
23391
  const entryContent = [...imports, "", ...exportParts, ""].join("\n");
23327
23392
 
23328
23393
  // Write temporary entry file in the widget root
23329
- const entryPath = path$b.join(widgetPath, "__compile_entry.js");
23330
- const distDir = path$b.join(widgetPath, "dist");
23331
- const outPath = path$b.join(distDir, "index.cjs.js");
23394
+ const entryPath = path$c.join(widgetPath, "__compile_entry.js");
23395
+ const distDir = path$c.join(widgetPath, "dist");
23396
+ const outPath = path$c.join(distDir, "index.cjs.js");
23332
23397
 
23333
23398
  try {
23334
23399
  // Ensure dist/ directory exists
23335
- if (!fs$7.existsSync(distDir)) {
23336
- fs$7.mkdirSync(distDir, { recursive: true });
23400
+ if (!fs$8.existsSync(distDir)) {
23401
+ fs$8.mkdirSync(distDir, { recursive: true });
23337
23402
  }
23338
23403
 
23339
- fs$7.writeFileSync(entryPath, entryContent, "utf8");
23404
+ fs$8.writeFileSync(entryPath, entryContent, "utf8");
23340
23405
 
23341
23406
  console.log(
23342
23407
  `[WidgetCompiler] Compiling ${dashFiles.length} component(s) from ${widgetPath}`,
@@ -23392,8 +23457,8 @@ async function compileWidget$1(widgetPath) {
23392
23457
  } finally {
23393
23458
  // Clean up temporary entry file
23394
23459
  try {
23395
- if (fs$7.existsSync(entryPath)) {
23396
- fs$7.unlinkSync(entryPath);
23460
+ if (fs$8.existsSync(entryPath)) {
23461
+ fs$8.unlinkSync(entryPath);
23397
23462
  }
23398
23463
  } catch (cleanupError) {
23399
23464
  // Non-fatal
@@ -23425,8 +23490,8 @@ var widgetCompiler$1 = {
23425
23490
  * Integrates with ComponentManager for automatic registration
23426
23491
  */
23427
23492
 
23428
- const fs$6 = require$$0$2;
23429
- const path$a = require$$1$2;
23493
+ const fs$7 = require$$0$2;
23494
+ const path$b = require$$1$2;
23430
23495
  const vm = require$$2$2;
23431
23496
  const { findWidgetsDir: findWidgetsDir$1 } = widgetCompiler$1;
23432
23497
 
@@ -23514,14 +23579,14 @@ class DynamicWidgetLoader {
23514
23579
  );
23515
23580
 
23516
23581
  const widgetsDir =
23517
- findWidgetsDir$1(widgetPath) || path$a.join(widgetPath, "widgets");
23518
- const componentPath = path$a.join(widgetsDir, `${componentName}.js`);
23519
- const configPath = path$a.join(widgetsDir, `${componentName}.dash.js`);
23582
+ findWidgetsDir$1(widgetPath) || path$b.join(widgetPath, "widgets");
23583
+ const componentPath = path$b.join(widgetsDir, `${componentName}.js`);
23584
+ const configPath = path$b.join(widgetsDir, `${componentName}.dash.js`);
23520
23585
 
23521
- if (!fs$6.existsSync(componentPath)) {
23586
+ if (!fs$7.existsSync(componentPath)) {
23522
23587
  throw new Error(`Component file not found: ${componentPath}`);
23523
23588
  }
23524
- if (!fs$6.existsSync(configPath)) {
23589
+ if (!fs$7.existsSync(configPath)) {
23525
23590
  throw new Error(`Config file not found: ${configPath}`);
23526
23591
  }
23527
23592
 
@@ -23572,7 +23637,7 @@ class DynamicWidgetLoader {
23572
23637
  */
23573
23638
  async loadConfigFile(configPath) {
23574
23639
  try {
23575
- const source = fs$6.readFileSync(configPath, "utf8");
23640
+ const source = fs$7.readFileSync(configPath, "utf8");
23576
23641
 
23577
23642
  let exportMatch = source.match(/export\s+default\s+({[\s\S]*});?\s*$/);
23578
23643
 
@@ -23640,7 +23705,7 @@ class DynamicWidgetLoader {
23640
23705
  return [];
23641
23706
  }
23642
23707
 
23643
- const files = fs$6.readdirSync(widgetsDir);
23708
+ const files = fs$7.readdirSync(widgetsDir);
23644
23709
  const widgets = new Set();
23645
23710
 
23646
23711
  files.forEach((file) => {
@@ -23673,6 +23738,157 @@ dynamicWidgetLoader$3.exports.dynamicWidgetLoader = dynamicWidgetLoader$2;
23673
23738
 
23674
23739
  var dynamicWidgetLoaderExports = dynamicWidgetLoader$3.exports;
23675
23740
 
23741
+ /**
23742
+ * widgetPermissions.js
23743
+ *
23744
+ * Read and parse the `dash.permissions.mcp` block from an installed
23745
+ * widget's package.json.
23746
+ *
23747
+ * Manifest format (declared by widget authors in their package.json):
23748
+ *
23749
+ * {
23750
+ * "name": "@trops/notes-summarizer",
23751
+ * "dash": {
23752
+ * "permissions": {
23753
+ * "mcp": {
23754
+ * "filesystem": {
23755
+ * "tools": ["read_file", "list_directory"],
23756
+ * "readPaths": ["~/Documents/notes"],
23757
+ * "writePaths": []
23758
+ * },
23759
+ * "github": {
23760
+ * "tools": ["search_repositories", "get_file_contents"]
23761
+ * }
23762
+ * }
23763
+ * }
23764
+ * }
23765
+ * }
23766
+ *
23767
+ * Path strings beginning with `~` are expanded to the user's home
23768
+ * directory at parse time. Tool-only servers (no path I/O, e.g.
23769
+ * github) omit the `readPaths`/`writePaths` keys.
23770
+ *
23771
+ * Public API:
23772
+ *
23773
+ * getWidgetMcpPermissions(widgetId) → permissions | null
23774
+ * Returns the parsed permissions for a widget, or null if the
23775
+ * widget is unmanifested. Cached per process.
23776
+ *
23777
+ * parseManifestPermissions(packageJson) → permissions | null
23778
+ * Pure function — exposed for tests.
23779
+ *
23780
+ * clearCache() → void
23781
+ * Test-only. Drops the in-process cache so tests can re-read.
23782
+ */
23783
+
23784
+ const fs$6 = require$$0$2;
23785
+ const path$a = require$$1$2;
23786
+ const os$1 = require$$2$1;
23787
+ const { app: app$6 } = require$$0$1;
23788
+
23789
+ // Cache: widgetId → permissions | null. Populated lazily on first
23790
+ // lookup; invalidated when a widget is installed/uninstalled (the
23791
+ // install/uninstall paths call clearCache()).
23792
+ const _cache = new Map();
23793
+
23794
+ /**
23795
+ * Expand a leading "~" to the user's home directory. Other paths are
23796
+ * returned as-is.
23797
+ */
23798
+ function expandHome(p) {
23799
+ if (typeof p !== "string" || !p) return p;
23800
+ if (p === "~") return os$1.homedir();
23801
+ if (p.startsWith("~/") || p.startsWith("~\\")) {
23802
+ return path$a.join(os$1.homedir(), p.slice(2));
23803
+ }
23804
+ return p;
23805
+ }
23806
+
23807
+ /**
23808
+ * Parse a widget's package.json contents into a normalized permissions
23809
+ * object. Returns null if no `dash.permissions.mcp` block exists.
23810
+ */
23811
+ function parseManifestPermissions(packageJson) {
23812
+ if (!packageJson || typeof packageJson !== "object") return null;
23813
+ const mcp = packageJson?.dash?.permissions?.mcp;
23814
+ if (!mcp || typeof mcp !== "object") return null;
23815
+
23816
+ const servers = {};
23817
+ for (const [serverName, raw] of Object.entries(mcp)) {
23818
+ if (!raw || typeof raw !== "object") continue;
23819
+ const tools = Array.isArray(raw.tools)
23820
+ ? raw.tools.filter((t) => typeof t === "string")
23821
+ : [];
23822
+ const readPaths = Array.isArray(raw.readPaths)
23823
+ ? raw.readPaths.filter((p) => typeof p === "string").map(expandHome)
23824
+ : [];
23825
+ const writePaths = Array.isArray(raw.writePaths)
23826
+ ? raw.writePaths.filter((p) => typeof p === "string").map(expandHome)
23827
+ : [];
23828
+ servers[serverName] = { tools, readPaths, writePaths };
23829
+ }
23830
+
23831
+ return { servers };
23832
+ }
23833
+
23834
+ /**
23835
+ * Find a widget's installed package.json on disk. Widgets live under
23836
+ * userData/widgets/<scope>/<name>/ for scoped packages or
23837
+ * userData/widgets/<name>/ for unscoped. The widgetId is the npm
23838
+ * package name (e.g. "@trops/notes-summarizer" or "notes-summarizer").
23839
+ */
23840
+ function resolveWidgetPackagePath(widgetId) {
23841
+ if (typeof widgetId !== "string" || !widgetId) return null;
23842
+ const widgetsRoot = path$a.join(app$6.getPath("userData"), "widgets");
23843
+ // Split scope from name for "@scope/name" form.
23844
+ const parts = widgetId.startsWith("@") ? widgetId.split("/") : [widgetId];
23845
+ return path$a.join(widgetsRoot, ...parts, "package.json");
23846
+ }
23847
+
23848
+ /**
23849
+ * Read and parse a widget's MCP permissions. Returns null if:
23850
+ * - the widget directory doesn't exist
23851
+ * - package.json is unreadable / malformed
23852
+ * - the widget hasn't declared dash.permissions.mcp
23853
+ *
23854
+ * Result is cached per widgetId for the lifetime of this process.
23855
+ */
23856
+ function getWidgetMcpPermissions$1(widgetId) {
23857
+ if (_cache.has(widgetId)) return _cache.get(widgetId);
23858
+ const pkgPath = resolveWidgetPackagePath(widgetId);
23859
+ if (!pkgPath || !fs$6.existsSync(pkgPath)) {
23860
+ _cache.set(widgetId, null);
23861
+ return null;
23862
+ }
23863
+ try {
23864
+ const raw = fs$6.readFileSync(pkgPath, "utf8");
23865
+ const pkg = JSON.parse(raw);
23866
+ const perms = parseManifestPermissions(pkg);
23867
+ _cache.set(widgetId, perms);
23868
+ return perms;
23869
+ } catch (e) {
23870
+ console.warn(
23871
+ "[widgetPermissions] failed to read package.json for " +
23872
+ widgetId +
23873
+ ": " +
23874
+ e.message,
23875
+ );
23876
+ _cache.set(widgetId, null);
23877
+ return null;
23878
+ }
23879
+ }
23880
+
23881
+ function clearCache() {
23882
+ _cache.clear();
23883
+ }
23884
+
23885
+ var widgetPermissions = {
23886
+ getWidgetMcpPermissions: getWidgetMcpPermissions$1,
23887
+ parseManifestPermissions,
23888
+ expandHome,
23889
+ clearCache,
23890
+ };
23891
+
23676
23892
  /**
23677
23893
  * schedulerController.js
23678
23894
  *
@@ -24213,6 +24429,10 @@ var schedulerController_1 = schedulerController$2;
24213
24429
  const { compileWidget, findWidgetsDir } = widgetCompiler$1;
24214
24430
  const { toPackageId, parsePackageId } = packageId;
24215
24431
  const { getStoredToken } = registryAuthController$2;
24432
+ const {
24433
+ getWidgetMcpPermissions,
24434
+ clearCache: clearWidgetPermsCache,
24435
+ } = widgetPermissions;
24216
24436
 
24217
24437
  let WIDGETS_CACHE_DIR = null;
24218
24438
  let REGISTRY_CONFIG_FILE = null;
@@ -25287,6 +25507,38 @@ var schedulerController_1 = schedulerController$2;
25287
25507
  return null;
25288
25508
  }
25289
25509
 
25510
+ /**
25511
+ * If a freshly-installed widget declares dash.permissions.mcp, ping the
25512
+ * renderer so the consent modal can pop up. The renderer subscribes via
25513
+ * `widget:mcp-consent-required` and either calls widget-mcp:set-grant
25514
+ * with the user's selections (Slice 2) or quietly drops the message
25515
+ * (older renderer pre-Slice-2 — the gate still fail-closes).
25516
+ *
25517
+ * Cache invalidation: widgetPermissions caches per-process, so an upgrade
25518
+ * over a stale cached entry would otherwise keep the old manifest. Drop
25519
+ * the whole cache here — cheap, infrequent.
25520
+ */
25521
+ function maybeEmitMcpConsentRequired(widgetName) {
25522
+ try {
25523
+ clearWidgetPermsCache();
25524
+ const declared = getWidgetMcpPermissions(widgetName);
25525
+ if (!declared) return;
25526
+ BrowserWindow.getAllWindows().forEach((win) => {
25527
+ win.webContents.send("widget:mcp-consent-required", {
25528
+ widgetId: widgetName,
25529
+ declared,
25530
+ });
25531
+ });
25532
+ } catch (e) {
25533
+ console.warn(
25534
+ "[widgetRegistry] mcp consent emit failed for " +
25535
+ widgetName +
25536
+ ": " +
25537
+ e.message,
25538
+ );
25539
+ }
25540
+ }
25541
+
25290
25542
  /**
25291
25543
  * Setup IPC handlers for widget management (use in main.js)
25292
25544
  */
@@ -25313,6 +25565,8 @@ var schedulerController_1 = schedulerController$2;
25313
25565
  });
25314
25566
  });
25315
25567
 
25568
+ maybeEmitMcpConsentRequired(widgetName);
25569
+
25316
25570
  return config;
25317
25571
  },
25318
25572
  );
@@ -25334,6 +25588,8 @@ var schedulerController_1 = schedulerController$2;
25334
25588
  });
25335
25589
  });
25336
25590
 
25591
+ maybeEmitMcpConsentRequired(widgetName);
25592
+
25337
25593
  return config;
25338
25594
  },
25339
25595
  );
@@ -50796,7 +51052,7 @@ async function handleGetAppStats$1() {
50796
51052
 
50797
51053
  const registryController$2 = registryController$3;
50798
51054
  const registryAuthController$1 = registryAuthController$2;
50799
- const { getWidgetRegistry } = widgetRegistryExports;
51055
+ const { getWidgetRegistry: getWidgetRegistry$1 } = widgetRegistryExports;
50800
51056
 
50801
51057
  /**
50802
51058
  * Helper: find a workspace by ID or return the first (active) one.
@@ -51324,7 +51580,7 @@ async function handleConfigureWidget$1({ dashboardId, widgetId, config }) {
51324
51580
  function getInstalledPackageNames() {
51325
51581
  const installed = new Set();
51326
51582
  try {
51327
- const registry = getWidgetRegistry();
51583
+ const registry = getWidgetRegistry$1();
51328
51584
  const widgets = registry.getWidgets();
51329
51585
  for (const w of widgets) {
51330
51586
  const name = w.name || w.packageId || "";
@@ -51605,7 +51861,7 @@ async function handleInstallWidget$1({ packageName }) {
51605
51861
  }
51606
51862
 
51607
51863
  // Download and install
51608
- const registry = getWidgetRegistry();
51864
+ const registry = getWidgetRegistry$1();
51609
51865
  const config = await registry.downloadWidget(pkg.name, pkg.downloadUrl);
51610
51866
 
51611
51867
  // Notify all renderer windows
@@ -59564,6 +59820,93 @@ const webSocketController$1 = {
59564
59820
 
59565
59821
  var webSocketController_1 = webSocketController$1;
59566
59822
 
59823
+ /**
59824
+ * widgetMcpGrantsController.js
59825
+ *
59826
+ * IPC handlers for the Slice-2 user-grant store. The renderer reads/writes
59827
+ * grants via these channels: install consent modal calls `widget-mcp:set-grant`
59828
+ * with the user's selections, Settings → Privacy & Security calls
59829
+ * `widget-mcp:list-all` to render the grant audit, and either entry point can
59830
+ * call `widget-mcp:revoke` / `widget-mcp:revoke-server`.
59831
+ *
59832
+ * The list-all handler joins each grant with its declared manifest so the UI
59833
+ * can show declared-vs-granted diffs (declared paths the user did NOT grant
59834
+ * are rendered struck through, etc.). Widgets that have a manifest but no
59835
+ * grant are also surfaced — those are the install-consent retroactive prompts.
59836
+ */
59837
+
59838
+ const { ipcMain: ipcMain$1 } = require$$0$1;
59839
+ const {
59840
+ getGrant,
59841
+ setGrant,
59842
+ revokeGrant,
59843
+ revokeServer,
59844
+ listAllGrants,
59845
+ } = grantedPermissions;
59846
+ const { getWidgetMcpPermissions } = widgetPermissions;
59847
+ const { getWidgetRegistry } = widgetRegistryExports;
59848
+
59849
+ function setupWidgetMcpGrantsHandlers() {
59850
+ ipcMain$1.handle("widget-mcp:get-grant", (event, widgetId) => {
59851
+ return getGrant(widgetId);
59852
+ });
59853
+
59854
+ ipcMain$1.handle("widget-mcp:set-grant", (event, widgetId, perms) => {
59855
+ return setGrant(widgetId, perms);
59856
+ });
59857
+
59858
+ ipcMain$1.handle("widget-mcp:revoke", (event, widgetId) => {
59859
+ return revokeGrant(widgetId);
59860
+ });
59861
+
59862
+ ipcMain$1.handle("widget-mcp:revoke-server", (event, widgetId, serverName) => {
59863
+ return revokeServer(widgetId, serverName);
59864
+ });
59865
+
59866
+ // Joins all installed widgets with their declared and granted permission
59867
+ // blocks. Returns one row per widget that has a declared manifest OR a
59868
+ // grant — this surfaces both "freshly installed, awaiting consent" and
59869
+ // "previously granted, now reviewable" cases in Settings.
59870
+ ipcMain$1.handle("widget-mcp:list-all", () => {
59871
+ const grantsByWidget = new Map();
59872
+ for (const { widgetId, granted } of listAllGrants()) {
59873
+ grantsByWidget.set(widgetId, granted);
59874
+ }
59875
+
59876
+ const rows = [];
59877
+ const seen = new Set();
59878
+
59879
+ let installedWidgets = [];
59880
+ try {
59881
+ installedWidgets = getWidgetRegistry().getWidgets() || [];
59882
+ } catch (_e) {
59883
+ // Registry not initialized yet; fall back to grants-only listing.
59884
+ }
59885
+
59886
+ for (const w of installedWidgets) {
59887
+ const widgetId = w?.name;
59888
+ if (!widgetId || seen.has(widgetId)) continue;
59889
+ seen.add(widgetId);
59890
+ const declared = getWidgetMcpPermissions(widgetId);
59891
+ const granted = grantsByWidget.get(widgetId) || null;
59892
+ // Skip widgets with neither — they have nothing to show.
59893
+ if (!declared && !granted) continue;
59894
+ rows.push({ widgetId, declared, granted });
59895
+ }
59896
+
59897
+ // Surface any grants whose widget is no longer installed (rare, but
59898
+ // possible if uninstall didn't revoke). The user can still revoke them.
59899
+ for (const [widgetId, granted] of grantsByWidget) {
59900
+ if (seen.has(widgetId)) continue;
59901
+ rows.push({ widgetId, declared: null, granted });
59902
+ }
59903
+
59904
+ return rows;
59905
+ });
59906
+ }
59907
+
59908
+ var widgetMcpGrantsController$1 = { setupWidgetMcpGrantsHandlers };
59909
+
59567
59910
  /**
59568
59911
  * clientFactories.js
59569
59912
  *
@@ -64613,6 +64956,7 @@ const paletteToThemeMapper = paletteToThemeMapper_1;
64613
64956
  const webSocketController = webSocketController_1;
64614
64957
  const extractionCacheController = extractionCacheController_1;
64615
64958
  const mcpDashServerController = mcpDashServerController_1;
64959
+ const widgetMcpGrantsController = widgetMcpGrantsController$1;
64616
64960
 
64617
64961
  // --- Errors ---
64618
64962
  const themeFromUrlErrors = themeFromUrlErrors$1;
@@ -64717,6 +65061,7 @@ var electron = {
64717
65061
  webSocketController,
64718
65062
  extractionCacheController,
64719
65063
  mcpDashServerController,
65064
+ widgetMcpGrantsController,
64720
65065
 
64721
65066
  // Controller functions (flat) — spread for convenient destructuring
64722
65067
  ...controllers,