pi-mega-compact 0.4.8 → 0.4.10

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.
@@ -24,7 +24,7 @@
24
24
  * before_agent_start systemPrompt prepend (PREVENT-PI-003).
25
25
  */
26
26
  import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
27
- import { join, dirname } from "node:path";
27
+ import { join, dirname, sep } from "node:path";
28
28
  import { fileURLToPath } from "node:url";
29
29
  import { STATE_DIR_DEFAULT } from "../src/config.js";
30
30
  import { VectorStore } from "../src/vectorStore.js";
@@ -651,46 +651,93 @@ export default function (pi) {
651
651
  // ---- Dashboard server commands ----------------------------------------
652
652
  const portFile = join(currentStateDir, "port.pid");
653
653
  const runnerFile = join(currentStateDir, "_dashboard-runner.mjs");
654
+ const launchLog = join(currentStateDir, "_dashboard-launch.log");
655
+ // Whether the runner must be spawned with --experimental-strip-types (true only
656
+ // when we fall back to the .ts source outside node_modules; false when using
657
+ // the shipped compiled dist/extensions/dashboard-server.js).
658
+ let dashboardNeedsStrip = false;
659
+ // The dashboard server binds 9320–9329 (TARGET_PORT..TARGET_PORT+PORT_RANGE-1
660
+ // in dashboard-server.js). Probe each for a live /api/snapshot so we can detect
661
+ // readiness even when port.pid landed in a different state dir than we poll.
662
+ async function findLivePort() {
663
+ for (let port = 9320; port <= 9329; port++) {
664
+ try {
665
+ const res = await fetch(`http://localhost:${port}/api/snapshot`, { signal: AbortSignal.timeout(800) }); // guardrails-allow PREVENT-PI-004: localhost liveness probe of the dashboard server this extension spawned
666
+ if (res.ok)
667
+ return port;
668
+ }
669
+ catch { /* not on this port — try next */ }
670
+ }
671
+ return null;
672
+ }
654
673
  /** Try to reach a running dashboard server. Returns { port, url } or null. */
655
674
  async function isServerRunning() {
656
- if (!existsSync(portFile))
675
+ const port = await findLivePort();
676
+ if (!port) {
677
+ // Stale marker with no live server behind it — clean up.
678
+ if (existsSync(portFile)) {
679
+ try {
680
+ unlinkSync(portFile);
681
+ }
682
+ catch { /* ignore */ }
683
+ }
657
684
  return null;
658
- try {
659
- const info = JSON.parse(readFileSync(portFile, "utf-8"));
660
- if (!info?.port)
661
- return null;
662
- const url = `http://localhost:${info.port}`; // guardrails-allow PREVENT-PI-004: localhost URL of the dashboard server this extension spawned
663
- // Quick liveness probe
664
- const res = await fetch(`${url}/api/snapshot`, { signal: AbortSignal.timeout(1500) }); // guardrails-allow PREVENT-PI-004: localhost probe to the dashboard server this extension spawned
665
- if (res.ok)
666
- return { port: info.port, url };
667
685
  }
668
- catch {
669
- // stale or unreachable — clean up
670
- try {
671
- unlinkSync(portFile);
672
- }
673
- catch { /* ignore */ }
686
+ return { port, url: `http://localhost:${port}` }; // guardrails-allow PREVENT-PI-004: localhost URL of the dashboard server this extension spawned
687
+ }
688
+ /**
689
+ * Resolve the launchable dashboard-server module.
690
+ *
691
+ * CRITICAL: Node's `--experimental-strip-types` REFUSES to strip .ts files that
692
+ * live under `node_modules` (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). Since
693
+ * the published package installs under node_modules, importing the .ts source
694
+ * fails in every real install (it only worked from a source checkout). So we
695
+ * prefer the COMPILED dist/extensions/dashboard-server.js (which the package
696
+ * ships from v0.4.6 — it imports only Node built-ins, so it runs standalone),
697
+ * and only fall back to the .ts source (with strip-types) when the compiled
698
+ * file is absent AND we're not under node_modules (dev checkout without a build).
699
+ *
700
+ * Returns { entry, needsStripTypes }.
701
+ */
702
+ function resolveDashboardEntry() {
703
+ const here = dirname(fileURLToPath(import.meta.url)); // .../extensions
704
+ const candidates = [
705
+ // 1. Compiled sibling when running from dist/ (import.meta is dist/extensions/…js)
706
+ { entry: join(here, "dashboard-server.js"), strip: false },
707
+ // 2. Compiled under the package's dist/ when running from source extensions/…ts
708
+ { entry: join(here, "..", "dist", "extensions", "dashboard-server.js"), strip: false },
709
+ // 3. Last resort: the .ts source (only strippable OUTSIDE node_modules)
710
+ { entry: join(here, "dashboard-server.ts"), strip: true },
711
+ ];
712
+ for (const c of candidates) {
713
+ if (!existsSync(c.entry))
714
+ continue;
715
+ if (c.strip && c.entry.includes(`${sep}node_modules${sep}`))
716
+ continue; // unstrippable
717
+ return { entry: c.entry, needsStripTypes: c.strip };
674
718
  }
675
719
  return null;
676
720
  }
677
721
  /** Write a small ESM runner script that imports and launches the dashboard server. */
678
722
  function writeRunnerScript() {
679
- // The npm package ships SOURCE only (no dist/), so the launchable entry is
680
- // the .ts file — not dashboard-server.js (which only exists after a local
681
- // build). dashboard-server.ts imports only Node built-ins, so it loads under
682
- // node's --experimental-strip-types without any other compiled code. The
683
- // child is spawned with that flag (see the spawn below) so the import below
684
- // resolves from the source install path.
685
- const sourceServer = join(dirname(fileURLToPath(import.meta.url)), "dashboard-server.ts");
723
+ const resolved = resolveDashboardEntry();
724
+ if (!resolved)
725
+ return false;
726
+ dashboardNeedsStrip = resolved.needsStripTypes;
686
727
  const script = [
687
- `import { launchDashboardServer } from ${JSON.stringify(sourceServer)};`,
688
- `launchDashboardServer(${JSON.stringify(currentStateDir)}).catch(err => {`,
689
- ` console.error("[mega-compact] dashboard failed:", err);`,
728
+ `import { appendFileSync } from "node:fs";`,
729
+ `const __log = ${JSON.stringify(launchLog)};`,
730
+ `function __fail(err) {`,
731
+ ` const msg = "[mega-compact] dashboard failed: " + (err && err.stack ? err.stack : String(err));`,
732
+ ` try { appendFileSync(__log, msg + "\\n"); } catch { /* ignore */ }`,
733
+ ` console.error(msg);`,
690
734
  ` process.exit(1);`,
691
- `});`,
735
+ `}`,
736
+ `import { launchDashboardServer } from ${JSON.stringify(resolved.entry)};`,
737
+ `launchDashboardServer(${JSON.stringify(currentStateDir)}).catch(__fail);`,
692
738
  ].join("\n");
693
739
  writeFileSync(runnerFile, script);
740
+ return true;
694
741
  }
695
742
  /** Open a URL in the default browser. Platform-aware. Uses spawn (not exec) to avoid shell injection. */
696
743
  function openBrowser(url) {
@@ -718,30 +765,36 @@ export default function (pi) {
718
765
  }
719
766
  // Start the server
720
767
  ctx.ui.notify("[mega-compact] starting dashboard server…");
721
- writeRunnerScript();
722
- const child = spawn(process.execPath, ["--experimental-strip-types", runnerFile], {
768
+ if (!writeRunnerScript()) {
769
+ ctx.ui.notify("[mega-compact] dashboard entry not found check logs.");
770
+ return;
771
+ }
772
+ const args = dashboardNeedsStrip ? ["--experimental-strip-types", runnerFile] : [runnerFile];
773
+ const child = spawn(process.execPath, args, {
723
774
  detached: true,
724
775
  stdio: "ignore",
725
776
  });
726
777
  child.unref();
727
- // Poll for port.pid (up to 5 seconds)
728
- const deadline = Date.now() + 5_000;
729
- let port;
778
+ // Poll for a live server (port 9320–9329) instead of relying solely on the
779
+ // port.pid marker, which can land in a different state dir than the one we
780
+ // poll when a prior compact left currentStateDir pointing elsewhere.
781
+ const deadline = Date.now() + 6_000;
782
+ let port = null;
730
783
  while (Date.now() < deadline) {
731
- await new Promise((r) => setTimeout(r, 300));
732
- if (existsSync(portFile)) {
733
- try {
734
- const raw = JSON.parse(readFileSync(portFile, "utf-8"));
735
- if (raw?.port) {
736
- port = raw.port;
737
- break;
738
- }
739
- }
740
- catch { /* keep polling */ }
741
- }
784
+ await new Promise((resolve) => setTimeout(resolve, 300));
785
+ port = await findLivePort();
786
+ if (port)
787
+ break;
742
788
  }
743
789
  if (!port) {
744
- ctx.ui.notify("[mega-compact] dashboard server failed to start — check logs.");
790
+ let detail = "";
791
+ try {
792
+ const log = readFileSync(launchLog, "utf-8").trim();
793
+ if (log)
794
+ detail = ` — ${log.split("\n").slice(-3).join("; ")}`;
795
+ }
796
+ catch { /* no log yet */ }
797
+ ctx.ui.notify(`[mega-compact] dashboard server failed to start${detail}. See ${launchLog}`);
745
798
  return;
746
799
  }
747
800
  const url = `http://localhost:${port}`; // guardrails-allow PREVENT-PI-004: localhost URL of the dashboard server this extension spawned
@@ -27,7 +27,7 @@
27
27
  import type { ExtensionAPI, ExtensionContext, ContextEvent, SessionBeforeCompactEvent } from "@earendil-works/pi-coding-agent";
28
28
  import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
29
29
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
30
- import { join, dirname } from "node:path";
30
+ import { join, dirname, sep } from "node:path";
31
31
  import { fileURLToPath } from "node:url";
32
32
  import { STATE_DIR_DEFAULT } from "../src/config.js";
33
33
  import { VectorStore } from "../src/vectorStore.js";
@@ -790,41 +790,89 @@ export default function (pi: ExtensionAPI) {
790
790
 
791
791
  const portFile = join(currentStateDir, "port.pid");
792
792
  const runnerFile = join(currentStateDir, "_dashboard-runner.mjs");
793
+ const launchLog = join(currentStateDir, "_dashboard-launch.log");
794
+ // Whether the runner must be spawned with --experimental-strip-types (true only
795
+ // when we fall back to the .ts source outside node_modules; false when using
796
+ // the shipped compiled dist/extensions/dashboard-server.js).
797
+ let dashboardNeedsStrip = false;
798
+
799
+ // The dashboard server binds 9320–9329 (TARGET_PORT..TARGET_PORT+PORT_RANGE-1
800
+ // in dashboard-server.js). Probe each for a live /api/snapshot so we can detect
801
+ // readiness even when port.pid landed in a different state dir than we poll.
802
+ async function findLivePort(): Promise<number | null> {
803
+ for (let port = 9320; port <= 9329; port++) {
804
+ try {
805
+ const res = await fetch(`http://localhost:${port}/api/snapshot`, { signal: AbortSignal.timeout(800) }); // guardrails-allow PREVENT-PI-004: localhost liveness probe of the dashboard server this extension spawned
806
+ if (res.ok) return port;
807
+ } catch { /* not on this port — try next */ }
808
+ }
809
+ return null;
810
+ }
793
811
 
794
812
  /** Try to reach a running dashboard server. Returns { port, url } or null. */
795
813
  async function isServerRunning(): Promise<{ port: number; url: string } | null> {
796
- if (!existsSync(portFile)) return null;
797
- try {
798
- const info = JSON.parse(readFileSync(portFile, "utf-8"));
799
- if (!info?.port) return null;
800
- const url = `http://localhost:${info.port}`; // guardrails-allow PREVENT-PI-004: localhost URL of the dashboard server this extension spawned
801
- // Quick liveness probe
802
- const res = await fetch(`${url}/api/snapshot`, { signal: AbortSignal.timeout(1500) }); // guardrails-allow PREVENT-PI-004: localhost probe to the dashboard server this extension spawned
803
- if (res.ok) return { port: info.port, url };
804
- } catch {
805
- // stale or unreachable — clean up
806
- try { unlinkSync(portFile); } catch { /* ignore */ }
814
+ const port = await findLivePort();
815
+ if (!port) {
816
+ // Stale marker with no live server behind it — clean up.
817
+ if (existsSync(portFile)) {
818
+ try { unlinkSync(portFile); } catch { /* ignore */ }
819
+ }
820
+ return null;
821
+ }
822
+ return { port, url: `http://localhost:${port}` }; // guardrails-allow PREVENT-PI-004: localhost URL of the dashboard server this extension spawned
823
+ }
824
+
825
+ /**
826
+ * Resolve the launchable dashboard-server module.
827
+ *
828
+ * CRITICAL: Node's `--experimental-strip-types` REFUSES to strip .ts files that
829
+ * live under `node_modules` (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). Since
830
+ * the published package installs under node_modules, importing the .ts source
831
+ * fails in every real install (it only worked from a source checkout). So we
832
+ * prefer the COMPILED dist/extensions/dashboard-server.js (which the package
833
+ * ships from v0.4.6 — it imports only Node built-ins, so it runs standalone),
834
+ * and only fall back to the .ts source (with strip-types) when the compiled
835
+ * file is absent AND we're not under node_modules (dev checkout without a build).
836
+ *
837
+ * Returns { entry, needsStripTypes }.
838
+ */
839
+ function resolveDashboardEntry(): { entry: string; needsStripTypes: boolean } | null {
840
+ const here = dirname(fileURLToPath(import.meta.url)); // .../extensions
841
+ const candidates = [
842
+ // 1. Compiled sibling when running from dist/ (import.meta is dist/extensions/…js)
843
+ { entry: join(here, "dashboard-server.js"), strip: false },
844
+ // 2. Compiled under the package's dist/ when running from source extensions/…ts
845
+ { entry: join(here, "..", "dist", "extensions", "dashboard-server.js"), strip: false },
846
+ // 3. Last resort: the .ts source (only strippable OUTSIDE node_modules)
847
+ { entry: join(here, "dashboard-server.ts"), strip: true },
848
+ ];
849
+ for (const c of candidates) {
850
+ if (!existsSync(c.entry)) continue;
851
+ if (c.strip && c.entry.includes(`${sep}node_modules${sep}`)) continue; // unstrippable
852
+ return { entry: c.entry, needsStripTypes: c.strip };
807
853
  }
808
854
  return null;
809
855
  }
810
856
 
811
857
  /** Write a small ESM runner script that imports and launches the dashboard server. */
812
- function writeRunnerScript(): void {
813
- // The npm package ships SOURCE only (no dist/), so the launchable entry is
814
- // the .ts file — not dashboard-server.js (which only exists after a local
815
- // build). dashboard-server.ts imports only Node built-ins, so it loads under
816
- // node's --experimental-strip-types without any other compiled code. The
817
- // child is spawned with that flag (see the spawn below) so the import below
818
- // resolves from the source install path.
819
- const sourceServer = join(dirname(fileURLToPath(import.meta.url)), "dashboard-server.ts");
858
+ function writeRunnerScript(): boolean {
859
+ const resolved = resolveDashboardEntry();
860
+ if (!resolved) return false;
861
+ dashboardNeedsStrip = resolved.needsStripTypes;
820
862
  const script = [
821
- `import { launchDashboardServer } from ${JSON.stringify(sourceServer)};`,
822
- `launchDashboardServer(${JSON.stringify(currentStateDir)}).catch(err => {`,
823
- ` console.error("[mega-compact] dashboard failed:", err);`,
863
+ `import { appendFileSync } from "node:fs";`,
864
+ `const __log = ${JSON.stringify(launchLog)};`,
865
+ `function __fail(err) {`,
866
+ ` const msg = "[mega-compact] dashboard failed: " + (err && err.stack ? err.stack : String(err));`,
867
+ ` try { appendFileSync(__log, msg + "\\n"); } catch { /* ignore */ }`,
868
+ ` console.error(msg);`,
824
869
  ` process.exit(1);`,
825
- `});`,
870
+ `}`,
871
+ `import { launchDashboardServer } from ${JSON.stringify(resolved.entry)};`,
872
+ `launchDashboardServer(${JSON.stringify(currentStateDir)}).catch(__fail);`,
826
873
  ].join("\n");
827
874
  writeFileSync(runnerFile, script);
875
+ return true;
828
876
  }
829
877
 
830
878
  /** Open a URL in the default browser. Platform-aware. Uses spawn (not exec) to avoid shell injection. */
@@ -855,29 +903,36 @@ export default function (pi: ExtensionAPI) {
855
903
 
856
904
  // Start the server
857
905
  ctx.ui.notify("[mega-compact] starting dashboard server…");
858
- writeRunnerScript();
906
+ if (!writeRunnerScript()) {
907
+ ctx.ui.notify("[mega-compact] dashboard entry not found — check logs.");
908
+ return;
909
+ }
859
910
 
860
- const child = spawn(process.execPath, ["--experimental-strip-types", runnerFile], {
911
+ const args = dashboardNeedsStrip ? ["--experimental-strip-types", runnerFile] : [runnerFile];
912
+ const child = spawn(process.execPath, args, {
861
913
  detached: true,
862
914
  stdio: "ignore",
863
915
  });
864
916
  child.unref();
865
917
 
866
- // Poll for port.pid (up to 5 seconds)
867
- const deadline = Date.now() + 5_000;
868
- let port: number | undefined;
918
+ // Poll for a live server (port 9320–9329) instead of relying solely on the
919
+ // port.pid marker, which can land in a different state dir than the one we
920
+ // poll when a prior compact left currentStateDir pointing elsewhere.
921
+ const deadline = Date.now() + 6_000;
922
+ let port: number | null = null;
869
923
  while (Date.now() < deadline) {
870
- await new Promise((r) => setTimeout(r, 300));
871
- if (existsSync(portFile)) {
872
- try {
873
- const raw = JSON.parse(readFileSync(portFile, "utf-8"));
874
- if (raw?.port) { port = raw.port; break; }
875
- } catch { /* keep polling */ }
876
- }
924
+ await new Promise((resolve) => setTimeout(resolve, 300));
925
+ port = await findLivePort();
926
+ if (port) break;
877
927
  }
878
928
 
879
929
  if (!port) {
880
- ctx.ui.notify("[mega-compact] dashboard server failed to start — check logs.");
930
+ let detail = "";
931
+ try {
932
+ const log = readFileSync(launchLog, "utf-8").trim();
933
+ if (log) detail = ` — ${log.split("\n").slice(-3).join("; ")}`;
934
+ } catch { /* no log yet */ }
935
+ ctx.ui.notify(`[mega-compact] dashboard server failed to start${detail}. See ${launchLog}`);
881
936
  return;
882
937
  }
883
938
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-2-Clause",