@raingor/pi-web-switch 0.5.0 → 0.6.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.
package/dist/index.html CHANGED
@@ -9,8 +9,8 @@
9
9
  <link rel="manifest" href="./manifest.webmanifest" />
10
10
  <meta name="theme-color" content="#05090d" />
11
11
  <meta name="description" content="Web UI for pi coding agent — configuration management, session browser, and usage dashboard" />
12
- <script type="module" crossorigin src="./assets/main-rMGV7BZ7.js"></script>
13
- <link rel="stylesheet" crossorigin href="./assets/main-BtntHFgX.css">
12
+ <script type="module" crossorigin src="./assets/main-CqkZxauO.js"></script>
13
+ <link rel="stylesheet" crossorigin href="./assets/main-DiEs78Xc.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@raingor/pi-web-switch",
3
3
  "private": false,
4
- "version": "0.5.0",
4
+ "version": "0.6.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.html",
7
7
  "description": "Web UI for pi coding agent — live configuration management, session browser, and memory viewer",
@@ -1633,6 +1633,268 @@ export function readSessionPreview(filePath: string, limit = 20): { messages: Se
1633
1633
 
1634
1634
  const MEMORY_FILENAMES = ["MEMORY.md", "USER.md", "failures.md"];
1635
1635
 
1636
+ // ─── Hermes Memory Config (auto-write model + optimize) ──
1637
+ // The pi-hermes-memory extension reads ~/.pi/agent/hermes-memory-config.json
1638
+ // to decide which model performs automatic memory writes and consolidation
1639
+ // (`llmModelOverride`, `llmThinkingOverride`) and how long a consolidation run
1640
+ // may take (`consolidationTimeoutMs`). We read/merge-write just those keys so
1641
+ // unrelated fields the extension may add later are preserved.
1642
+ const HERMES_MEMORY_CONFIG_PATH = join(PI_DIR, "hermes-memory-config.json");
1643
+ const HERMES_EXTENSION_ENTRY = join(
1644
+ PI_DIR, "npm", "node_modules", "pi-hermes-memory", "src", "index.ts"
1645
+ );
1646
+ const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
1647
+ type ThinkingLevel = (typeof THINKING_LEVELS)[number];
1648
+ const OVERFLOW_STRATEGIES = ["auto-consolidate", "reject", "fifo-evict"] as const;
1649
+ type OverflowStrategy = (typeof OVERFLOW_STRATEGIES)[number];
1650
+
1651
+ // Mirror pi-hermes-memory's own defaults (src/constants.ts) so the panel shows
1652
+ // the same effective limits the extension enforces when the keys are unset.
1653
+ const DEFAULT_MEMORY_CHAR_LIMIT = 5000;
1654
+ const DEFAULT_USER_CHAR_LIMIT = 5000;
1655
+
1656
+ export interface HermesMemoryConfig {
1657
+ llmModelOverride?: string;
1658
+ llmThinkingOverride?: ThinkingLevel;
1659
+ consolidationTimeoutMs?: number;
1660
+ memoryCharLimit?: number;
1661
+ userCharLimit?: number;
1662
+ memoryOverflowStrategy?: OverflowStrategy;
1663
+ }
1664
+
1665
+ export function readHermesMemoryConfig(): HermesMemoryConfig {
1666
+ try {
1667
+ if (!existsSync(HERMES_MEMORY_CONFIG_PATH)) return {};
1668
+ const parsed = JSON.parse(readFileSync(HERMES_MEMORY_CONFIG_PATH, "utf-8")) as Record<string, unknown>;
1669
+ const out: HermesMemoryConfig = {};
1670
+ if (typeof parsed.llmModelOverride === "string" && parsed.llmModelOverride.trim()) {
1671
+ out.llmModelOverride = parsed.llmModelOverride.trim();
1672
+ }
1673
+ if (typeof parsed.llmThinkingOverride === "string" && (THINKING_LEVELS as readonly string[]).includes(parsed.llmThinkingOverride)) {
1674
+ out.llmThinkingOverride = parsed.llmThinkingOverride as ThinkingLevel;
1675
+ }
1676
+ if (typeof parsed.consolidationTimeoutMs === "number" && Number.isFinite(parsed.consolidationTimeoutMs)) {
1677
+ out.consolidationTimeoutMs = parsed.consolidationTimeoutMs;
1678
+ }
1679
+ if (typeof parsed.memoryCharLimit === "number" && Number.isFinite(parsed.memoryCharLimit) && parsed.memoryCharLimit > 0) {
1680
+ out.memoryCharLimit = parsed.memoryCharLimit;
1681
+ }
1682
+ if (typeof parsed.userCharLimit === "number" && Number.isFinite(parsed.userCharLimit) && parsed.userCharLimit > 0) {
1683
+ out.userCharLimit = parsed.userCharLimit;
1684
+ }
1685
+ if (typeof parsed.memoryOverflowStrategy === "string" && (OVERFLOW_STRATEGIES as readonly string[]).includes(parsed.memoryOverflowStrategy)) {
1686
+ out.memoryOverflowStrategy = parsed.memoryOverflowStrategy as OverflowStrategy;
1687
+ }
1688
+ return out;
1689
+ } catch {
1690
+ return {};
1691
+ }
1692
+ }
1693
+
1694
+ /** Merge-write the auto-write model settings, preserving any other keys. */
1695
+ export function writeHermesMemoryConfig(patch: HermesMemoryConfig): boolean {
1696
+ try {
1697
+ let existing: Record<string, unknown> = {};
1698
+ if (existsSync(HERMES_MEMORY_CONFIG_PATH)) {
1699
+ try {
1700
+ existing = JSON.parse(readFileSync(HERMES_MEMORY_CONFIG_PATH, "utf-8")) as Record<string, unknown>;
1701
+ } catch {
1702
+ existing = {};
1703
+ }
1704
+ }
1705
+ // Empty string clears the override so the extension falls back to defaults.
1706
+ if (patch.llmModelOverride !== undefined) {
1707
+ const v = patch.llmModelOverride.trim();
1708
+ if (v) existing.llmModelOverride = v;
1709
+ else delete existing.llmModelOverride;
1710
+ }
1711
+ if (patch.llmThinkingOverride !== undefined) {
1712
+ if ((THINKING_LEVELS as readonly string[]).includes(patch.llmThinkingOverride)) {
1713
+ existing.llmThinkingOverride = patch.llmThinkingOverride;
1714
+ }
1715
+ }
1716
+ if (patch.consolidationTimeoutMs !== undefined && Number.isFinite(patch.consolidationTimeoutMs)) {
1717
+ existing.consolidationTimeoutMs = patch.consolidationTimeoutMs;
1718
+ }
1719
+ if (patch.memoryCharLimit !== undefined && Number.isFinite(patch.memoryCharLimit) && patch.memoryCharLimit > 0) {
1720
+ existing.memoryCharLimit = patch.memoryCharLimit;
1721
+ }
1722
+ if (patch.userCharLimit !== undefined && Number.isFinite(patch.userCharLimit) && patch.userCharLimit > 0) {
1723
+ existing.userCharLimit = patch.userCharLimit;
1724
+ }
1725
+ if (patch.memoryOverflowStrategy !== undefined && (OVERFLOW_STRATEGIES as readonly string[]).includes(patch.memoryOverflowStrategy)) {
1726
+ existing.memoryOverflowStrategy = patch.memoryOverflowStrategy;
1727
+ }
1728
+ writeFileSync(HERMES_MEMORY_CONFIG_PATH, JSON.stringify(existing, null, 2), "utf-8");
1729
+ return true;
1730
+ } catch {
1731
+ return false;
1732
+ }
1733
+ }
1734
+
1735
+ /**
1736
+ * Lightweight capacity snapshot for the memory page. Reports per-target usage
1737
+ * against the effective char limits the extension enforces (failure gets 2x the
1738
+ * memory limit). The extension measures capacity in CHARACTERS, not bytes, so
1739
+ * we read each file and use its character length — byte size (statSync) would
1740
+ * over-count ~3x for CJK text and show false "over limit" bars.
1741
+ */
1742
+ export function readMemoryStatus(): {
1743
+ targets: { filename: string; target: "memory" | "user" | "failure"; chars: number; limit: number }[];
1744
+ } {
1745
+ const cfg = readHermesMemoryConfig();
1746
+ const memLimit = cfg.memoryCharLimit ?? DEFAULT_MEMORY_CHAR_LIMIT;
1747
+ const userLimit = cfg.userCharLimit ?? DEFAULT_USER_CHAR_LIMIT;
1748
+ const map: { filename: string; target: "memory" | "user" | "failure"; limit: number }[] = [
1749
+ { filename: "MEMORY.md", target: "memory", limit: memLimit },
1750
+ { filename: "USER.md", target: "user", limit: userLimit },
1751
+ { filename: "failures.md", target: "failure", limit: memLimit * 2 },
1752
+ ];
1753
+ return {
1754
+ targets: map.map(({ filename, target, limit }) => {
1755
+ const p = join(HERMES_DIR, filename);
1756
+ let chars = 0;
1757
+ try {
1758
+ chars = existsSync(p) ? readFileSync(p, "utf-8").length : 0;
1759
+ } catch {
1760
+ chars = 0;
1761
+ }
1762
+ return { filename, target, chars, limit };
1763
+ }),
1764
+ };
1765
+ }
1766
+
1767
+ /** Resolve a usable `pi` binary: PI_BINARY env → PATH → known install dirs. */
1768
+ function resolvePiBin(): string | null {
1769
+ const home = homedir();
1770
+ const candidates = [
1771
+ process.env.PI_BINARY,
1772
+ "pi",
1773
+ `${home}/.npm-global/bin/pi`,
1774
+ `${home}/.local/share/pnpm/pi`,
1775
+ ].filter(Boolean) as string[];
1776
+ for (const bin of candidates) {
1777
+ try {
1778
+ const out = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 15000 });
1779
+ if (out.status === 0 && out.stdout.trim()) return bin;
1780
+ } catch {
1781
+ // try next
1782
+ }
1783
+ }
1784
+ // Fall back to `which pi`
1785
+ try {
1786
+ const which = spawnSync("which", ["pi"], { encoding: "utf8", timeout: 5000 });
1787
+ if (which.status === 0 && which.stdout.trim()) return which.stdout.trim();
1788
+ } catch {
1789
+ // ignore
1790
+ }
1791
+ return null;
1792
+ }
1793
+
1794
+ function memoryFileSizes(): Record<string, number> {
1795
+ const sizes: Record<string, number> = {};
1796
+ for (const name of MEMORY_FILENAMES) {
1797
+ const p = join(HERMES_DIR, name);
1798
+ try {
1799
+ sizes[name] = existsSync(p) ? statSync(p).size : 0;
1800
+ } catch {
1801
+ sizes[name] = 0;
1802
+ }
1803
+ }
1804
+ return sizes;
1805
+ }
1806
+
1807
+ export interface OptimizeMemoryResult {
1808
+ success: boolean;
1809
+ before: Record<string, number>;
1810
+ after: Record<string, number>;
1811
+ freedBytes: number;
1812
+ message?: string;
1813
+ }
1814
+
1815
+ /**
1816
+ * One-click memory optimization. Runs pi's `/memory-consolidate` command in a
1817
+ * headless child process with the pi-hermes-memory extension loaded, using the
1818
+ * configured auto-write model override. Reports the byte delta across the three
1819
+ * memory files. Long-running (the command spawns an LLM turn), so the caller
1820
+ * should surface a spinner.
1821
+ */
1822
+ export async function optimizeMemory(): Promise<OptimizeMemoryResult> {
1823
+ const before = memoryFileSizes();
1824
+ const bin = resolvePiBin();
1825
+ if (!bin) {
1826
+ return { success: false, before, after: before, freedBytes: 0, message: "pi binary not found" };
1827
+ }
1828
+ if (!existsSync(HERMES_EXTENSION_ENTRY)) {
1829
+ return { success: false, before, after: before, freedBytes: 0, message: "pi-hermes-memory extension not found" };
1830
+ }
1831
+
1832
+ const cfg = readHermesMemoryConfig();
1833
+ const args = ["-p", "--no-session", "-e", HERMES_EXTENSION_ENTRY];
1834
+ if (cfg.llmModelOverride) args.push("--model", cfg.llmModelOverride);
1835
+ args.push("--thinking", cfg.llmThinkingOverride ?? "off");
1836
+
1837
+ // Compute per-target capacity so the prompt can name the over-limit targets
1838
+ // explicitly. Weak/free models are conservative and skip merging when told
1839
+ // only "merge duplicates"; giving them a concrete goal ("USER is at 105%,
1840
+ // get it under the limit") is what actually makes them shrink memory.
1841
+ const status = readMemoryStatus();
1842
+ const capacityLines = status.targets.map((tg) => {
1843
+ const pct = tg.limit > 0 ? Math.round((tg.chars / tg.limit) * 100) : 0;
1844
+ const over = tg.chars > tg.limit;
1845
+ return `- target "${tg.target}" (${tg.filename}): ${tg.chars}/${tg.limit} chars (${pct}%)${over ? " — OVER LIMIT, must shrink below the limit" : ""}`;
1846
+ });
1847
+ const overTargets = status.targets.filter((tg) => tg.chars > tg.limit).map((tg) => `"${tg.target}"`);
1848
+
1849
+ // Direct consolidation prompt rather than the /memory-consolidate slash
1850
+ // command: the slash command spawns a *nested* child pi process per target
1851
+ // (memory/user/failure/project), which routinely runs for 10+ minutes.
1852
+ // Driving the memory tools directly in this single child is far faster.
1853
+ args.push(
1854
+ [
1855
+ "Consolidate my long-term memory to reduce redundancy WITHOUT losing important facts.",
1856
+ "Current capacity per target:",
1857
+ ...capacityLines,
1858
+ overTargets.length > 0
1859
+ ? `The following targets are OVER their limit and MUST be reduced below it: ${overTargets.join(", ")}. This is the primary goal — you must actually remove or merge entries so each over-limit target ends up under its char limit.`
1860
+ : "No target is over its limit; still merge any obvious duplicates and drop clearly stale entries.",
1861
+ "For EACH target: call memory_search (with an empty or broad query) to list its current entries, then use memory_remove to drop outdated/superseded/duplicate entries and memory_replace/memory_add to merge related entries into fewer, more concise ones.",
1862
+ "Always preserve user preferences and explicit corrections (highest priority). Prefer merging several similar entries into one tight entry over deleting unique facts.",
1863
+ "Do not stop until every over-limit target is under its limit. When finished, reply with a one-line summary of what changed per target.",
1864
+ ].join("\n")
1865
+ );
1866
+
1867
+ const timeoutMs = cfg.consolidationTimeoutMs && cfg.consolidationTimeoutMs > 0
1868
+ ? cfg.consolidationTimeoutMs
1869
+ : 600000;
1870
+
1871
+ try {
1872
+ const out = spawnSync(bin, args, {
1873
+ encoding: "utf8",
1874
+ timeout: timeoutMs,
1875
+ maxBuffer: 1024 * 1024 * 8,
1876
+ });
1877
+ const after = memoryFileSizes();
1878
+ const freedBytes = Object.values(before).reduce((a, b) => a + b, 0)
1879
+ - Object.values(after).reduce((a, b) => a + b, 0);
1880
+ if (out.error) {
1881
+ const killed = (out.error as NodeJS.ErrnoException).code === "ETIMEDOUT";
1882
+ return {
1883
+ success: false,
1884
+ before,
1885
+ after,
1886
+ freedBytes,
1887
+ message: killed ? `consolidation timed out after ${Math.round(timeoutMs / 1000)}s` : String(out.error.message),
1888
+ };
1889
+ }
1890
+ return { success: out.status === 0, before, after, freedBytes, message: out.status === 0 ? undefined : `exit ${out.status}` };
1891
+ } catch (e) {
1892
+ const after = memoryFileSizes();
1893
+ return { success: false, before, after, freedBytes: 0, message: e instanceof Error ? e.message : String(e) };
1894
+ }
1895
+ }
1896
+
1897
+
1636
1898
  /** Strip the trailing `<!-- created=..., last=... -->` marker from a § section. */
1637
1899
  function sectionText(section: string): string {
1638
1900
  return section.replace(/<!--\s*created\s*=[^>]*-->\s*$/, "").trim();
@@ -16,6 +16,8 @@ import {
16
16
  Trash2,
17
17
  ChevronDown,
18
18
  ChevronRight,
19
+ Sparkles,
20
+ Settings2,
19
21
  } from "lucide-react";
20
22
 
21
23
  interface MemoryFile {
@@ -370,7 +372,7 @@ function MemoryFileSection({
370
372
 
371
373
  export function MemoryPage() {
372
374
  const { t } = useTranslation();
373
- const { initialized } = useConfigStore();
375
+ const { initialized, allModels } = useConfigStore();
374
376
  const [files, setFiles] = useState<MemoryFile[]>([]);
375
377
  const [loading, setLoading] = useState(true);
376
378
  const [refreshing, setRefreshing] = useState(false);
@@ -379,6 +381,28 @@ export function MemoryPage() {
379
381
  const [deleteTarget, setDeleteTarget] = useState<{ filename: string; entry: MemoryEntry } | null>(null);
380
382
  const [deleting, setDeleting] = useState(false);
381
383
 
384
+ // One-click optimize (memory consolidation)
385
+ const [optimizing, setOptimizing] = useState(false);
386
+ const [optimizeMsg, setOptimizeMsg] = useState<{ ok: boolean; text: string } | null>(null);
387
+ const [optimizeElapsed, setOptimizeElapsed] = useState(0);
388
+
389
+ // Auto-write memory model config
390
+ const [showConfig, setShowConfig] = useState(false);
391
+ const [cfg, setCfg] = useState<{ llmModelOverride: string; llmThinkingOverride: string; consolidationTimeoutMs: number; memoryCharLimit: number; userCharLimit: number; memoryOverflowStrategy: string }>(
392
+ { llmModelOverride: "", llmThinkingOverride: "off", consolidationTimeoutMs: 600000, memoryCharLimit: 5000, userCharLimit: 5000, memoryOverflowStrategy: "auto-consolidate" }
393
+ );
394
+ const [cfgSaving, setCfgSaving] = useState(false);
395
+ const [cfgMsg, setCfgMsg] = useState<{ ok: boolean; text: string } | null>(null);
396
+
397
+ // Capacity status (per-target chars vs limit) for progress bars
398
+ const [status, setStatus] = useState<{ filename: string; target: string; chars: number; limit: number }[]>([]);
399
+ const loadStatus = useCallback(() => {
400
+ fetch("/api/pi/memory/status")
401
+ .then((r) => r.json())
402
+ .then((s: { targets?: { filename: string; target: string; chars: number; limit: number }[] }) => setStatus(s.targets ?? []))
403
+ .catch(() => {});
404
+ }, []);
405
+
382
406
  const loadAll = useCallback(() => {
383
407
  if (!initialized) return;
384
408
  setRefreshing(true);
@@ -396,6 +420,86 @@ export function MemoryPage() {
396
420
  }, [initialized]);
397
421
 
398
422
  useEffect(() => { loadAll(); }, [loadAll]);
423
+ useEffect(() => { loadStatus(); }, [loadStatus]);
424
+
425
+ // Load the hermes auto-write model config once.
426
+ useEffect(() => {
427
+ fetch("/api/pi/memory/config")
428
+ .then((r) => r.json())
429
+ .then((c: { llmModelOverride?: string; llmThinkingOverride?: string; consolidationTimeoutMs?: number; memoryCharLimit?: number; userCharLimit?: number; memoryOverflowStrategy?: string }) =>
430
+ setCfg({
431
+ llmModelOverride: c.llmModelOverride ?? "",
432
+ llmThinkingOverride: c.llmThinkingOverride ?? "off",
433
+ consolidationTimeoutMs: c.consolidationTimeoutMs ?? 600000,
434
+ memoryCharLimit: c.memoryCharLimit ?? 5000,
435
+ userCharLimit: c.userCharLimit ?? 5000,
436
+ memoryOverflowStrategy: c.memoryOverflowStrategy ?? "auto-consolidate",
437
+ })
438
+ )
439
+ .catch(() => {});
440
+ }, []);
441
+
442
+ const formatBytes = (n: number) => {
443
+ const abs = Math.abs(n);
444
+ if (abs >= 1024) return `${(n / 1024).toFixed(1)} KB`;
445
+ return `${n} B`;
446
+ };
447
+
448
+ const handleOptimize = async () => {
449
+ setOptimizing(true);
450
+ setOptimizeMsg(null);
451
+ setOptimizeElapsed(0);
452
+ // While the consolidation child runs, poll capacity + elapsed so the user
453
+ // sees the bars move and a running timer instead of a frozen spinner.
454
+ const startedAt = Date.now();
455
+ const timer = window.setInterval(() => {
456
+ setOptimizeElapsed(Math.round((Date.now() - startedAt) / 1000));
457
+ loadStatus();
458
+ }, 2000);
459
+ try {
460
+ const res = await fetch("/api/pi/memory/optimize", { method: "POST" });
461
+ const result = (await res.json()) as { success: boolean; freedBytes?: number; message?: string };
462
+ if (result.success) {
463
+ const freed = result.freedBytes ?? 0;
464
+ setOptimizeMsg({ ok: true, text: freed > 0 ? t("memory.optimize_done", formatBytes(freed)) : t("memory.optimize_none") });
465
+ loadAll();
466
+ loadStatus();
467
+ } else {
468
+ setOptimizeMsg({ ok: false, text: t("memory.optimize_failed", result.message ?? "") });
469
+ }
470
+ } catch (e) {
471
+ setOptimizeMsg({ ok: false, text: t("memory.optimize_failed", e instanceof Error ? e.message : "") });
472
+ } finally {
473
+ window.clearInterval(timer);
474
+ setOptimizing(false);
475
+ }
476
+ };
477
+
478
+ const handleSaveConfig = async () => {
479
+ setCfgSaving(true);
480
+ setCfgMsg(null);
481
+ try {
482
+ const res = await fetch("/api/pi/memory/config", {
483
+ method: "POST",
484
+ headers: { "Content-Type": "application/json" },
485
+ body: JSON.stringify({
486
+ llmModelOverride: cfg.llmModelOverride.trim(),
487
+ llmThinkingOverride: cfg.llmThinkingOverride,
488
+ consolidationTimeoutMs: cfg.consolidationTimeoutMs,
489
+ memoryCharLimit: cfg.memoryCharLimit,
490
+ userCharLimit: cfg.userCharLimit,
491
+ memoryOverflowStrategy: cfg.memoryOverflowStrategy,
492
+ }),
493
+ });
494
+ const { success } = (await res.json()) as { success: boolean };
495
+ setCfgMsg({ ok: !!success, text: success ? t("memory.config_saved") : t("memory.config_save_failed") });
496
+ if (success) loadStatus();
497
+ } catch {
498
+ setCfgMsg({ ok: false, text: t("memory.config_save_failed") });
499
+ } finally {
500
+ setCfgSaving(false);
501
+ }
502
+ };
399
503
 
400
504
  const handleDelete = async () => {
401
505
  if (!deleteTarget) return;
@@ -472,17 +576,78 @@ export function MemoryPage() {
472
576
  {t("memory.summary", String(totalEntries), String(parsed.length))}
473
577
  </p>
474
578
  </div>
475
- <button
476
- onClick={loadAll}
477
- className="flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors"
478
- style={{ borderColor: "var(--card-border)", color: "var(--muted-text)", backgroundColor: "var(--card-bg)" }}
479
- title={t("memory.refresh")}
480
- >
481
- <RefreshCw className={refreshing ? "h-3.5 w-3.5 animate-spin" : "h-3.5 w-3.5"} />
482
- {t("memory.refresh")}
483
- </button>
579
+ <div className="flex items-center gap-2">
580
+ <button
581
+ onClick={handleOptimize}
582
+ disabled={optimizing || parsed.length === 0}
583
+ className="flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors disabled:opacity-50"
584
+ style={{ borderColor: "var(--card-border)", color: "var(--muted-text)", backgroundColor: "var(--card-bg)" }}
585
+ title={t("memory.optimize_hint")}
586
+ >
587
+ <Sparkles className={optimizing ? "h-3.5 w-3.5 animate-pulse" : "h-3.5 w-3.5"} />
588
+ {optimizing ? t("memory.optimizing") : t("memory.optimize")}
589
+ </button>
590
+ <button
591
+ onClick={() => { setCfgMsg(null); setShowConfig(true); }}
592
+ className="flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors"
593
+ style={{ borderColor: "var(--card-border)", color: "var(--muted-text)", backgroundColor: "var(--card-bg)" }}
594
+ title={t("memory.config")}
595
+ >
596
+ <Settings2 className="h-3.5 w-3.5" />
597
+ {t("memory.config")}
598
+ </button>
599
+ <button
600
+ onClick={loadAll}
601
+ className="flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors"
602
+ style={{ borderColor: "var(--card-border)", color: "var(--muted-text)", backgroundColor: "var(--card-bg)" }}
603
+ title={t("memory.refresh")}
604
+ >
605
+ <RefreshCw className={refreshing ? "h-3.5 w-3.5 animate-spin" : "h-3.5 w-3.5"} />
606
+ {t("memory.refresh")}
607
+ </button>
608
+ </div>
484
609
  </div>
485
610
 
611
+ {(optimizing || optimizeMsg || status.length > 0) && (
612
+ <div className="tech-panel space-y-3 rounded-xl border px-4 py-3" style={{ borderColor: "var(--card-border)", backgroundColor: "var(--card-bg)" }}>
613
+ {/* Per-target capacity bars */}
614
+ <div className="space-y-2">
615
+ {status.map((s) => {
616
+ const pct = s.limit > 0 ? Math.min(100, Math.round((s.chars / s.limit) * 100)) : 0;
617
+ const over = s.chars > s.limit;
618
+ const barColor = over ? "#ef4444" : pct > 80 ? "#f59e0b" : "#10b981";
619
+ const label = FILE_LABEL_KEYS[s.filename] ? t(FILE_LABEL_KEYS[s.filename]!) : s.filename;
620
+ return (
621
+ <div key={s.filename}>
622
+ <div className="mb-1 flex items-center justify-between text-[11px]" style={{ color: "var(--muted-text)" }}>
623
+ <span>{label} <span style={{ color: "var(--subtle-text)" }}>· {s.filename}</span></span>
624
+ <span style={{ color: over ? "#ef4444" : "var(--muted-text)" }}>
625
+ {s.chars} / {s.limit} ({pct}%)
626
+ </span>
627
+ </div>
628
+ <div className="h-2 w-full overflow-hidden rounded-full" style={{ backgroundColor: "var(--page-bg)" }}>
629
+ <div className="h-full rounded-full transition-all duration-500" style={{ width: `${pct}%`, backgroundColor: barColor }} />
630
+ </div>
631
+ </div>
632
+ );
633
+ })}
634
+ </div>
635
+
636
+ {/* Optimize progress / result line */}
637
+ {optimizing && (
638
+ <div className="flex items-center gap-2 text-xs" style={{ color: "var(--muted-text)" }}>
639
+ <div className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-gray-500 border-t-transparent" />
640
+ <span>{t("memory.optimizing")} · {optimizeElapsed}s</span>
641
+ </div>
642
+ )}
643
+ {!optimizing && optimizeMsg && (
644
+ <div className="text-xs" style={{ color: optimizeMsg.ok ? "#10b981" : "#ef4444" }}>
645
+ {optimizeMsg.text}
646
+ </div>
647
+ )}
648
+ </div>
649
+ )}
650
+
486
651
  {parsed.length === 0 ? (
487
652
  <div className="memory-empty-state">
488
653
  <div className="memory-empty-radar"><Brain className="h-7 w-7" /></div>
@@ -548,6 +713,123 @@ export function MemoryPage() {
548
713
  </>
549
714
  )}
550
715
 
716
+ {/* Auto-write memory model config */}
717
+ <Modal
718
+ open={showConfig}
719
+ onClose={() => !cfgSaving && setShowConfig(false)}
720
+ title={t("memory.config_title")}
721
+ size="md"
722
+ >
723
+ <div className="space-y-4">
724
+ <p className="text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_desc")}</p>
725
+ <div>
726
+ <label className="mb-1 block text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_model")}</label>
727
+ <input
728
+ type="text"
729
+ list="memory-model-options"
730
+ value={cfg.llmModelOverride}
731
+ onChange={(e) => setCfg({ ...cfg, llmModelOverride: e.target.value })}
732
+ placeholder={t("memory.config_model_default")}
733
+ className="w-full rounded-lg border px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-blue-500"
734
+ style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }}
735
+ />
736
+ <datalist id="memory-model-options">
737
+ {allModels.map((m) => (
738
+ <option key={`${m.providerId}/${m.id}`} value={`${m.providerId}/${m.id}`}>{m.providerName} · {m.name ?? m.id}</option>
739
+ ))}
740
+ </datalist>
741
+ </div>
742
+ <div className="flex gap-3">
743
+ <div className="flex-1">
744
+ <label className="mb-1 block text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_thinking")}</label>
745
+ <select
746
+ value={cfg.llmThinkingOverride}
747
+ onChange={(e) => setCfg({ ...cfg, llmThinkingOverride: e.target.value })}
748
+ className="w-full rounded-lg border px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-blue-500"
749
+ style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }}
750
+ >
751
+ {["off", "minimal", "low", "medium", "high", "xhigh"].map((lvl) => (
752
+ <option key={lvl} value={lvl}>{lvl}</option>
753
+ ))}
754
+ </select>
755
+ </div>
756
+ <div className="flex-1">
757
+ <label className="mb-1 block text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_timeout")}</label>
758
+ <input
759
+ type="number"
760
+ min={60}
761
+ value={Math.round(cfg.consolidationTimeoutMs / 1000)}
762
+ onChange={(e) => setCfg({ ...cfg, consolidationTimeoutMs: Math.max(60, Number(e.target.value) || 0) * 1000 })}
763
+ className="w-full rounded-lg border px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-blue-500"
764
+ style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }}
765
+ />
766
+ </div>
767
+ </div>
768
+ {/* Capacity limits */}
769
+ <div className="flex gap-3">
770
+ <div className="flex-1">
771
+ <label className="mb-1 block text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_mem_limit")}</label>
772
+ <input
773
+ type="number"
774
+ min={1000}
775
+ step={500}
776
+ value={cfg.memoryCharLimit}
777
+ onChange={(e) => setCfg({ ...cfg, memoryCharLimit: Math.max(1000, Number(e.target.value) || 0) })}
778
+ className="w-full rounded-lg border px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-blue-500"
779
+ style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }}
780
+ />
781
+ <p className="mt-1 text-[10px]" style={{ color: "var(--subtle-text)" }}>{t("memory.config_mem_limit_hint", String(cfg.memoryCharLimit * 2))}</p>
782
+ </div>
783
+ <div className="flex-1">
784
+ <label className="mb-1 block text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_user_limit")}</label>
785
+ <input
786
+ type="number"
787
+ min={1000}
788
+ step={500}
789
+ value={cfg.userCharLimit}
790
+ onChange={(e) => setCfg({ ...cfg, userCharLimit: Math.max(1000, Number(e.target.value) || 0) })}
791
+ className="w-full rounded-lg border px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-blue-500"
792
+ style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }}
793
+ />
794
+ </div>
795
+ </div>
796
+ <div>
797
+ <label className="mb-1 block text-xs" style={{ color: "var(--muted-text)" }}>{t("memory.config_overflow")}</label>
798
+ <select
799
+ value={cfg.memoryOverflowStrategy}
800
+ onChange={(e) => setCfg({ ...cfg, memoryOverflowStrategy: e.target.value })}
801
+ className="w-full rounded-lg border px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-blue-500"
802
+ style={{ backgroundColor: "var(--card-bg)", borderColor: "var(--card-border)", color: "var(--page-text)" }}
803
+ >
804
+ <option value="auto-consolidate">{t("memory.config_overflow_auto")}</option>
805
+ <option value="reject">{t("memory.config_overflow_reject")}</option>
806
+ <option value="fifo-evict">{t("memory.config_overflow_fifo")}</option>
807
+ </select>
808
+ </div>
809
+ <div className="flex items-center justify-end gap-3">
810
+ {cfgMsg && (
811
+ <span className="text-xs" style={{ color: cfgMsg.ok ? "#10b981" : "#ef4444" }}>{cfgMsg.text}</span>
812
+ )}
813
+ <button
814
+ onClick={() => setShowConfig(false)}
815
+ disabled={cfgSaving}
816
+ className="rounded-lg border px-4 py-2 text-sm"
817
+ style={{ borderColor: "var(--card-border)", color: "var(--muted-text)" }}
818
+ >
819
+ {t("memory.config_cancel")}
820
+ </button>
821
+ <button
822
+ onClick={handleSaveConfig}
823
+ disabled={cfgSaving}
824
+ className="rounded-lg px-4 py-2 text-sm font-medium text-white disabled:opacity-50"
825
+ style={{ backgroundColor: "#2563eb" }}
826
+ >
827
+ {cfgSaving ? t("memory.config_saving") : t("memory.config_save")}
828
+ </button>
829
+ </div>
830
+ </div>
831
+ </Modal>
832
+
551
833
  {/* Delete entry confirm */}
552
834
  <Modal
553
835
  open={deleteTarget !== null}
@@ -27,7 +27,6 @@ import {
27
27
  CloudDownload,
28
28
  RefreshCw,
29
29
  ZoomIn,
30
- KeyRound,
31
30
  } from "lucide-react";
32
31
 
33
32
  type SettingsTab = "appearance" | "models" | "advanced";
@@ -117,37 +116,6 @@ export function SettingsPage() {
117
116
  const [updateError, setUpdateError] = useState(false);
118
117
  const [applying, setApplying] = useState(false);
119
118
  const [applyMessage, setApplyMessage] = useState<{ ok: number; failNames: string[] } | null>(null);
120
- // GitHub Copilot usage API config (username + classic PAT).
121
- const [copilotCfg, setCopilotCfg] = useState<{ username: string; token: string }>({ username: "", token: "" });
122
- const [copilotSaving, setCopilotSaving] = useState(false);
123
- const [copilotMsg, setCopilotMsg] = useState<{ ok: boolean; text: string } | null>(null);
124
-
125
- useEffect(() => {
126
- fetch("/api/pi/copilot-config")
127
- .then((r) => r.json())
128
- .then((cfg: { username?: string; token?: string }) =>
129
- setCopilotCfg({ username: cfg.username ?? "", token: cfg.token ?? "" })
130
- )
131
- .catch(() => {});
132
- }, []);
133
-
134
- const handleSaveCopilot = async () => {
135
- setCopilotSaving(true);
136
- setCopilotMsg(null);
137
- try {
138
- const res = await fetch("/api/pi/copilot-config", {
139
- method: "POST",
140
- headers: { "Content-Type": "application/json" },
141
- body: JSON.stringify(copilotCfg),
142
- });
143
- const { success } = (await res.json()) as { success: boolean };
144
- setCopilotMsg({ ok: !!success, text: success ? t("settings.copilot_saved") : t("settings.copilot_save_failed") });
145
- } catch {
146
- setCopilotMsg({ ok: false, text: t("settings.copilot_save_failed") });
147
- } finally {
148
- setCopilotSaving(false);
149
- }
150
- };
151
119
 
152
120
  const handleCheckUpdates = async () => {
153
121
  setCheckingUpdates(true);
@@ -600,46 +568,6 @@ export function SettingsPage() {
600
568
  })()}
601
569
  </Card>
602
570
 
603
- <Card icon={KeyRound} title={t("settings.copilot_title")} desc={t("settings.copilot_desc")}>
604
- <div className="max-w-md space-y-3">
605
- <div>
606
- <label className="mb-1 block text-xs text-gray-500">{t("settings.copilot_username")}</label>
607
- <input
608
- type="text"
609
- value={copilotCfg.username}
610
- onChange={(e) => setCopilotCfg({ ...copilotCfg, username: e.target.value })}
611
- placeholder="octocat"
612
- className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-white placeholder-gray-600"
613
- />
614
- </div>
615
- <div>
616
- <label className="mb-1 block text-xs text-gray-500">{t("settings.copilot_token")}</label>
617
- <input
618
- type="password"
619
- value={copilotCfg.token}
620
- onChange={(e) => setCopilotCfg({ ...copilotCfg, token: e.target.value })}
621
- placeholder="ghp_…"
622
- className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-white placeholder-gray-600"
623
- />
624
- </div>
625
- <div className="flex items-center gap-3">
626
- <button
627
- onClick={handleSaveCopilot}
628
- disabled={copilotSaving}
629
- className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-4 py-1.5 text-sm text-gray-300 hover:bg-gray-700 disabled:opacity-60"
630
- >
631
- {copilotSaving ? t("settings.copilot_saving") : t("settings.copilot_save")}
632
- </button>
633
- {copilotMsg && (
634
- <span className={cn("text-xs", copilotMsg.ok ? "text-emerald-400" : "text-red-400")}>
635
- {copilotMsg.text}
636
- </span>
637
- )}
638
- </div>
639
- <p className="text-[11px] leading-relaxed text-gray-500">{t("settings.copilot_token_hint")}</p>
640
- </div>
641
- </Card>
642
-
643
571
  <Card icon={Package} title={t("settings.packages")}>
644
572
  {(settings?.packages ?? []).length > 0 && (
645
573
  <div className="mb-4 flex flex-wrap gap-1.5">
@@ -365,6 +365,31 @@ const en: Record<string, string> = {
365
365
  "memory.channels": "Memory Channels",
366
366
  "memory.knowledge_synced": "Knowledge Base // Synced",
367
367
  "memory.files_indexed_detail": "{0} files · {1} memories",
368
+ "memory.optimize": "Optimize Memory",
369
+ "memory.optimizing": "Optimizing…",
370
+ "memory.optimize_done": "Optimized · freed {0}",
371
+ "memory.optimize_none": "Optimized · nothing to trim",
372
+ "memory.optimize_failed": "Optimize failed: {0}",
373
+ "memory.optimize_hint": "Runs pi's memory consolidation to merge duplicates and drop stale entries",
374
+ "memory.config": "Memory Model",
375
+ "memory.config_title": "Model for automatic memory writes",
376
+ "memory.config_desc": "Automatic memory writes and consolidation run on this model (blank = default)",
377
+ "memory.config_model": "Model",
378
+ "memory.config_model_default": "(default / unset)",
379
+ "memory.config_thinking": "Thinking level",
380
+ "memory.config_timeout": "Consolidation timeout (s)",
381
+ "memory.config_save": "Save",
382
+ "memory.config_saving": "Saving…",
383
+ "memory.config_saved": "Saved",
384
+ "memory.config_save_failed": "Save failed",
385
+ "memory.config_cancel": "Cancel",
386
+ "memory.config_mem_limit": "Project memory limit (chars)",
387
+ "memory.config_mem_limit_hint": "Failure records get 2x: {0}",
388
+ "memory.config_user_limit": "User profile limit (chars)",
389
+ "memory.config_overflow": "Overflow strategy",
390
+ "memory.config_overflow_auto": "Auto-consolidate (recommended)",
391
+ "memory.config_overflow_reject": "Reject writes",
392
+ "memory.config_overflow_fifo": "Evict oldest",
368
393
 
369
394
  "common.today": "Today",
370
395
  "common.yesterday": "Yesterday",
@@ -358,6 +358,31 @@ const ja: Record<string, string> = {
358
358
  "memory.channels": "メモリーチャンネル",
359
359
  "memory.knowledge_synced": "ナレッジベース // 同期済み",
360
360
  "memory.files_indexed_detail": "{0} ファイル · {1} メモリー",
361
+ "memory.optimize": "メモリを最適化",
362
+ "memory.optimizing": "最適化中…",
363
+ "memory.optimize_done": "最適化完了 · {0} 解放",
364
+ "memory.optimize_none": "最適化完了 · 整理不要",
365
+ "memory.optimize_failed": "最適化失敗:{0}",
366
+ "memory.optimize_hint": "pi のメモリ統合を実行し、重複の統合と古い項目の削除を行います",
367
+ "memory.config": "メモリモデル設定",
368
+ "memory.config_title": "自動メモリ書き込みに使うモデル",
369
+ "memory.config_desc": "自動メモリ書き込みと統合はこのモデルで実行されます(空欄なら既定)",
370
+ "memory.config_model": "モデル",
371
+ "memory.config_model_default": "(既定 / 未指定)",
372
+ "memory.config_thinking": "思考レベル",
373
+ "memory.config_timeout": "統合タイムアウト(秒)",
374
+ "memory.config_save": "保存",
375
+ "memory.config_saving": "保存中…",
376
+ "memory.config_saved": "保存しました",
377
+ "memory.config_save_failed": "保存に失敗",
378
+ "memory.config_cancel": "キャンセル",
379
+ "memory.config_mem_limit": "プロジェクト記憶上限(文字)",
380
+ "memory.config_mem_limit_hint": "障害記録は2倍:{0}",
381
+ "memory.config_user_limit": "ユーザープロファイル上限(文字)",
382
+ "memory.config_overflow": "上限超過時の戦略",
383
+ "memory.config_overflow_auto": "自動統合(推奨)",
384
+ "memory.config_overflow_reject": "書き込み拒否",
385
+ "memory.config_overflow_fifo": "最古を削除",
361
386
 
362
387
  "common.today": "今日",
363
388
  "common.yesterday": "昨日",
@@ -358,6 +358,31 @@ const zhCN: Record<string, string> = {
358
358
  "memory.channels": "记忆频道",
359
359
  "memory.knowledge_synced": "知识库 // 已同步",
360
360
  "memory.files_indexed_detail": "{0} 个文件 · {1} 条记忆",
361
+ "memory.optimize": "一键优化记忆",
362
+ "memory.optimizing": "优化中…",
363
+ "memory.optimize_done": "优化完成 · 释放 {0}",
364
+ "memory.optimize_none": "优化完成 · 无需精简",
365
+ "memory.optimize_failed": "优化失败:{0}",
366
+ "memory.optimize_hint": "调用 pi 的记忆整合,合并重复条目、清理过时记忆",
367
+ "memory.config": "记忆模型配置",
368
+ "memory.config_title": "自动写入记忆所用模型",
369
+ "memory.config_desc": "自动记忆写入与整合由该模型执行(留空则用默认)",
370
+ "memory.config_model": "模型",
371
+ "memory.config_model_default": "(默认 / 不指定)",
372
+ "memory.config_thinking": "思考等级",
373
+ "memory.config_timeout": "整合超时(秒)",
374
+ "memory.config_save": "保存",
375
+ "memory.config_saving": "保存中…",
376
+ "memory.config_saved": "已保存",
377
+ "memory.config_save_failed": "保存失败",
378
+ "memory.config_cancel": "取消",
379
+ "memory.config_mem_limit": "项目记忆上限(字符)",
380
+ "memory.config_mem_limit_hint": "故障记录上限为其 2 倍:{0}",
381
+ "memory.config_user_limit": "用户画像上限(字符)",
382
+ "memory.config_overflow": "超限策略",
383
+ "memory.config_overflow_auto": "自动整合(推荐)",
384
+ "memory.config_overflow_reject": "拒绝写入",
385
+ "memory.config_overflow_fifo": "淘汰最旧",
361
386
 
362
387
  "common.today": "今天",
363
388
  "common.yesterday": "昨天",
@@ -357,6 +357,31 @@ const zhTW: Record<string, string> = {
357
357
  "memory.channels": "記憶頻道",
358
358
  "memory.knowledge_synced": "知識庫 // 已同步",
359
359
  "memory.files_indexed_detail": "{0} 個檔案 · {1} 條記憶",
360
+ "memory.optimize": "一鍵優化記憶",
361
+ "memory.optimizing": "優化中…",
362
+ "memory.optimize_done": "優化完成 · 釋放 {0}",
363
+ "memory.optimize_none": "優化完成 · 無需精簡",
364
+ "memory.optimize_failed": "優化失敗:{0}",
365
+ "memory.optimize_hint": "呼叫 pi 的記憶整合,合併重複條目、清理過時記憶",
366
+ "memory.config": "記憶模型設定",
367
+ "memory.config_title": "自動寫入記憶所用模型",
368
+ "memory.config_desc": "自動記憶寫入與整合由該模型執行(留空則用預設)",
369
+ "memory.config_model": "模型",
370
+ "memory.config_model_default": "(預設 / 不指定)",
371
+ "memory.config_thinking": "思考等級",
372
+ "memory.config_timeout": "整合逾時(秒)",
373
+ "memory.config_save": "儲存",
374
+ "memory.config_saving": "儲存中…",
375
+ "memory.config_saved": "已儲存",
376
+ "memory.config_save_failed": "儲存失敗",
377
+ "memory.config_cancel": "取消",
378
+ "memory.config_mem_limit": "專案記憶上限(字元)",
379
+ "memory.config_mem_limit_hint": "故障記錄上限為其 2 倍:{0}",
380
+ "memory.config_user_limit": "使用者畫像上限(字元)",
381
+ "memory.config_overflow": "超限策略",
382
+ "memory.config_overflow_auto": "自動整合(推薦)",
383
+ "memory.config_overflow_reject": "拒絕寫入",
384
+ "memory.config_overflow_fifo": "淘汰最舊",
360
385
 
361
386
  "common.today": "今天",
362
387
  "common.yesterday": "昨天",
package/vite.config.ts CHANGED
@@ -148,6 +148,40 @@ function piApiPlugin(): Plugin {
148
148
  res.setHeader("Content-Type", "application/json");
149
149
  res.end(JSON.stringify(data));
150
150
  },
151
+ "GET /api/pi/memory/config"(_, res) {
152
+ res.setHeader("Content-Type", "application/json");
153
+ res.end(JSON.stringify(pi.readHermesMemoryConfig() ?? {}));
154
+ },
155
+ "GET /api/pi/memory/status"(_, res) {
156
+ res.setHeader("Content-Type", "application/json");
157
+ res.end(JSON.stringify(pi.readMemoryStatus()));
158
+ },
159
+ "POST /api/pi/memory/config"(req, res) {
160
+ let body = "";
161
+ req.on("data", (chunk: string) => (body += chunk));
162
+ req.on("end", () => {
163
+ try {
164
+ const patch = JSON.parse(body);
165
+ const ok = pi.writeHermesMemoryConfig(patch);
166
+ res.setHeader("Content-Type", "application/json");
167
+ res.end(JSON.stringify({ success: ok }));
168
+ } catch {
169
+ res.statusCode = 400;
170
+ res.setHeader("Content-Type", "application/json");
171
+ res.end(JSON.stringify({ success: false, error: "Invalid request body" }));
172
+ }
173
+ });
174
+ },
175
+ "POST /api/pi/memory/optimize"(_, res) {
176
+ pi.optimizeMemory().then((result: unknown) => {
177
+ res.setHeader("Content-Type", "application/json");
178
+ res.end(JSON.stringify(result));
179
+ }).catch((error: unknown) => {
180
+ res.statusCode = 500;
181
+ res.setHeader("Content-Type", "application/json");
182
+ res.end(JSON.stringify({ success: false, error: error instanceof Error ? error.message : "Optimize failed" }));
183
+ });
184
+ },
151
185
  "POST /api/pi/memory/delete-entry"(req, res) {
152
186
  let body = "";
153
187
  req.on("data", (chunk: string) => (body += chunk));