claude-code-session-manager 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/dist/assets/{TiptapBody-D0tfDVZb.js → TiptapBody-CFh7PFkf.js} +1 -1
  2. package/dist/assets/{cssMode-C3tZkaJ9.js → cssMode-Cll4QpTW.js} +1 -1
  3. package/dist/assets/{freemarker2-DEh8tC5X.js → freemarker2-lo-fLEl5.js} +1 -1
  4. package/dist/assets/{handlebars-D_6KmsOK.js → handlebars-CMA5gb0k.js} +1 -1
  5. package/dist/assets/{html-DF1KVjzv.js → html-BQ2X1VHF.js} +1 -1
  6. package/dist/assets/{htmlMode-DEakPokt.js → htmlMode-Da8nU8Ys.js} +1 -1
  7. package/dist/assets/{index-D-kX3T0V.css → index-6uZy0Pbe.css} +1 -1
  8. package/dist/assets/{index-Zg61GP50.js → index-Bs_D2jQM.js} +1392 -1329
  9. package/dist/assets/{javascript-Du7a359D.js → javascript-BfjktvBd.js} +1 -1
  10. package/dist/assets/{jsonMode-W3BJwbUD.js → jsonMode-Duc74W4E.js} +1 -1
  11. package/dist/assets/{liquid-DDj1fqca.js → liquid-CONgoaDI.js} +1 -1
  12. package/dist/assets/{lspLanguageFeatures-uyEbiR-d.js → lspLanguageFeatures-DdHwUVu5.js} +1 -1
  13. package/dist/assets/{mdx-DUqSETvC.js → mdx-DIQgvOto.js} +1 -1
  14. package/dist/assets/{python-D7S2lUAn.js → python-BcGmrT3q.js} +1 -1
  15. package/dist/assets/{razor-19nfZNaZ.js → razor-B2RyqYc0.js} +1 -1
  16. package/dist/assets/{tsMode-CQke5zpL.js → tsMode-CRnbNLOY.js} +1 -1
  17. package/dist/assets/{typescript-D4ge0PUF.js → typescript-CeTpx2hJ.js} +1 -1
  18. package/dist/assets/{xml-D42QDS7q.js → xml-DIN_EFpG.js} +1 -1
  19. package/dist/assets/{yaml-CEiz9NyU.js → yaml-DpCxufnr.js} +1 -1
  20. package/dist/index.html +2 -2
  21. package/package.json +3 -1
  22. package/src/main/index.cjs +78 -4
@@ -1,4 +1,4 @@
1
- const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard } = require('electron');
1
+ const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard, powerSaveBlocker, protocol } = require('electron');
2
2
  const { spawn, execFile, execFileSync } = require('node:child_process');
3
3
  const path = require('node:path');
4
4
  const fs = require('node:fs');
@@ -35,11 +35,14 @@ const searchIpc = require('./search.cjs');
35
35
  const repoAnalyzer = require('./repoAnalyzer.cjs');
36
36
  const hivesIpc = require('./hives.cjs');
37
37
  const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
38
- const { checkInsideHome } = require('./lib/insideHome.cjs');
38
+ const { checkInsideHome, assertInsideHome } = require('./lib/insideHome.cjs');
39
39
  const { openInEditor, openFileInEditor, openInFinder, openInTerminal } = require('./lib/openExternalApp.cjs');
40
40
 
41
41
  let mainWindow = null;
42
42
  let rebooting = false;
43
+ // powerSaveBlocker handle — keeps the system from suspending while the app runs
44
+ // so the scheduler's polling and jobs aren't frozen. -1 = not held.
45
+ let powerBlockerId = -1;
43
46
 
44
47
  // Boot diagnostics — populated at app.whenReady so the renderer can poll their
45
48
  // state via IPC and surface toasts on the failure paths. The first-paint
@@ -594,6 +597,29 @@ if (process.env.SM_E2E === '1') {
594
597
  try { app.disableHardwareAcceleration(); } catch { /* */ }
595
598
  }
596
599
 
600
+ // smfile:// — privileged scheme that serves home-scoped files to the in-app
601
+ // Editor's HTML preview iframe. Loading the user's HTML via a custom scheme
602
+ // (rather than srcdoc) gives the iframe document its OWN origin + empty CSP, so
603
+ // the page's own visualization scripts run — while the iframe is still
604
+ // sandboxed without allow-same-origin (opaque origin), keeping it walled off
605
+ // from the host app, its IPC bridge, and the user's filesystem. Must be
606
+ // registered before app.whenReady() (Electron rejects late scheme privileges).
607
+ protocol.registerSchemesAsPrivileged([
608
+ { scheme: 'smfile', privileges: { standard: true, secure: true, supportFetchAPI: true, stream: true } },
609
+ ]);
610
+
611
+ // Common content types for previewed assets. Anything unknown is served as
612
+ // octet-stream so the browser sniffs / downloads rather than mis-rendering.
613
+ const SMFILE_MIME = {
614
+ '.html': 'text/html', '.htm': 'text/html',
615
+ '.css': 'text/css', '.js': 'text/javascript', '.mjs': 'text/javascript',
616
+ '.json': 'application/json', '.svg': 'image/svg+xml',
617
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
618
+ '.gif': 'image/gif', '.webp': 'image/webp', '.ico': 'image/x-icon',
619
+ '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf',
620
+ '.txt': 'text/plain', '.csv': 'text/csv', '.xml': 'application/xml',
621
+ };
622
+
597
623
  // Single-instance lock (PRD F1 v2 §requestSingleInstanceLock).
598
624
  // In dev mode we skip the lock so two-dev-instance workflows still work.
599
625
  // E2E tests also skip so playwright.electron.launch can run multiple specs
@@ -685,7 +711,7 @@ app.whenReady().then(async () => {
685
711
  // — adds ~3 packages and ~2MB to the renderer build; the network fetch
686
712
  // happens once per cold launch and is cached by Electron's HTTP cache.
687
713
  "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
688
- "img-src 'self' data: blob:",
714
+ "img-src 'self' data: blob: smfile:",
689
715
  "font-src 'self' data: https://fonts.gstatic.com",
690
716
  // schemastore.org is used by Monaco for JSON schema validation
691
717
  // (settings.json, keybindings.json — see App.tsx::installMonacoSchemas).
@@ -694,10 +720,18 @@ app.whenReady().then(async () => {
694
720
  "connect-src 'self' https://api.anthropic.com https://registry.npmjs.org https://json.schemastore.org https://www.schemastore.org",
695
721
  "media-src 'self' blob:",
696
722
  "worker-src 'self' blob:",
697
- "frame-src 'none'",
723
+ // smfile: powers the Editor's sandboxed HTML preview iframe. The iframe is
724
+ // sandboxed WITHOUT allow-same-origin, so its document has an opaque origin
725
+ // and cannot reach the host even though it may run its own scripts.
726
+ "frame-src smfile:",
698
727
  "frame-ancestors 'none'",
699
728
  ].join('; ') + ';';
700
729
  session.defaultSession.webRequest.onHeadersReceived((details, cb) => {
730
+ // smfile:// serves the Editor's HTML-preview documents. They must NOT
731
+ // inherit the app CSP — `frame-ancestors 'none'` would forbid framing them,
732
+ // and `script-src 'self'` would block the page's own visualization scripts.
733
+ // Isolation comes from the iframe sandbox (opaque origin), not from CSP.
734
+ if (details.url.startsWith('smfile:')) { cb({}); return; }
701
735
  cb({
702
736
  responseHeaders: {
703
737
  ...details.responseHeaders,
@@ -706,6 +740,29 @@ app.whenReady().then(async () => {
706
740
  });
707
741
  });
708
742
 
743
+ // smfile:// handler — serves a single home-scoped file for the Editor's HTML
744
+ // preview iframe (and its relative assets: ./chart.js, ./data.json, images).
745
+ // URL shape: smfile://local/<absolute-path>. The pathname IS the absolute
746
+ // path; assertInsideHome enforces containment + rejects symlink escapes. The
747
+ // sandboxed iframe's relative requests resolve against this same scheme, so
748
+ // co-located assets load while everything stays inside home.
749
+ protocol.handle('smfile', async (request) => {
750
+ try {
751
+ const url = new URL(request.url);
752
+ const abs = decodeURIComponent(url.pathname);
753
+ const { realPath } = assertInsideHome(abs); // throws if outside home
754
+ const st = await fs.promises.stat(realPath);
755
+ if (st.isDirectory()) return new Response('Not a file', { status: 404 });
756
+ if (st.size > 25 * 1024 * 1024) return new Response('Too large', { status: 413 });
757
+ const buf = await fs.promises.readFile(realPath);
758
+ const ext = path.extname(realPath).toLowerCase();
759
+ const type = SMFILE_MIME[ext] || 'application/octet-stream';
760
+ return new Response(buf, { headers: { 'content-type': type } });
761
+ } catch (err) {
762
+ return new Response(`smfile error: ${err && err.message}`, { status: 404 });
763
+ }
764
+ });
765
+
709
766
  // Grant microphone / media permissions only for trusted origins.
710
767
  const MEDIA_PERMS = new Set(['media', 'audioCapture', 'microphone']);
711
768
  const isTrustedOrigin = (url) =>
@@ -792,6 +849,19 @@ app.whenReady().then(async () => {
792
849
  logs.writeLine({ scope: 'scheduler', level: 'error', message: 'init failed', meta: { error: e?.message } });
793
850
  });
794
851
 
852
+ // Keep the machine awake while the app is open. The scheduler polls billing
853
+ // usage every 2 min and runs `claude -p` jobs that must survive an idle
854
+ // laptop — a system suspend (GNOME/Pop!_OS idle or lid timeout) would freeze
855
+ // both. `prevent-app-suspension` stops suspend but still lets the display
856
+ // dim/sleep, so battery impact is limited to keeping the CPU resumable.
857
+ // On Linux this routes through the org.freedesktop.login1 inhibitor.
858
+ try {
859
+ powerBlockerId = powerSaveBlocker.start('prevent-app-suspension');
860
+ logs.writeLine({ scope: 'main', level: 'info', message: 'powerSaveBlocker started', meta: { id: powerBlockerId } });
861
+ } catch (e) {
862
+ logs.writeLine({ scope: 'main', level: 'warn', message: 'powerSaveBlocker failed', meta: { error: e?.message } });
863
+ }
864
+
795
865
  // OTEL: load persisted config and start the exporter only if `enabled`.
796
866
  // Failures are non-fatal — the app must keep working without telemetry.
797
867
  otelSettings.load()
@@ -810,6 +880,10 @@ app.on('will-quit', () => {
810
880
  // PRD F1 v2 §IPC plumbing: must unregisterAll on will-quit.
811
881
  try { globalShortcut.unregisterAll(); } catch { /* */ }
812
882
  voiceHotkey.disposeOnQuit();
883
+ if (powerBlockerId !== -1) {
884
+ try { powerSaveBlocker.stop(powerBlockerId); } catch { /* */ }
885
+ powerBlockerId = -1;
886
+ }
813
887
  });
814
888
 
815
889
  app.on('window-all-closed', () => {