pi-lean-portal 0.1.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 (55) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +608 -0
  3. package/backends/chromium/index.ts +50 -0
  4. package/backends/chromium-py/bridge.py +67 -0
  5. package/backends/firefox/index.ts +60 -0
  6. package/backends/firefox-py/bridge.py +64 -0
  7. package/backends/playwright-base/playwright-plugin.ts +1294 -0
  8. package/backends/python-adapter.ts +1141 -0
  9. package/backends/python-base/pi_browser_bridge/__init__.py +71 -0
  10. package/backends/python-base/pi_browser_bridge/accessibility.py +408 -0
  11. package/backends/python-base/pi_browser_bridge/bot_detection.py +115 -0
  12. package/backends/python-base/pi_browser_bridge/bridge.py +598 -0
  13. package/backends/python-base/pi_browser_bridge/playwright_base.py +1222 -0
  14. package/backends/python-base/pi_browser_bridge/transport.py +167 -0
  15. package/backends/python-base/pyproject.toml +15 -0
  16. package/browser-cookies.ts +88 -0
  17. package/browser-profile.ts +260 -0
  18. package/browser-status.ts +84 -0
  19. package/browser-toggle.ts +527 -0
  20. package/core/fetch-backend.ts +466 -0
  21. package/core/guides.ts +467 -0
  22. package/core/plugin-api.ts +302 -0
  23. package/core/plugin-config.ts +388 -0
  24. package/core/plugin-registry.ts +263 -0
  25. package/core/router.ts +1186 -0
  26. package/core/shared/accessibility-tree.ts +408 -0
  27. package/core/shared/bot-detection.ts +187 -0
  28. package/core/shared/browser-events.ts +111 -0
  29. package/core/shared/dom-extractor.ts +550 -0
  30. package/core/shared/nav-settle.ts +187 -0
  31. package/core/shared/paths.ts +56 -0
  32. package/core/shared/session-manager.ts +258 -0
  33. package/core/shared/settings-reader.ts +63 -0
  34. package/core/shared/snapshot-cache.ts +231 -0
  35. package/core/shared/storage-state.ts +560 -0
  36. package/core/shared/task-id.ts +77 -0
  37. package/core/shared/url-safety.ts +164 -0
  38. package/index.ts +253 -0
  39. package/package.json +63 -0
  40. package/ship-manifest.test.ts +12 -0
  41. package/tools/browser-back.ts +50 -0
  42. package/tools/browser-click.ts +74 -0
  43. package/tools/browser-console.ts +160 -0
  44. package/tools/browser-inspect.ts +136 -0
  45. package/tools/browser-navigate.ts +254 -0
  46. package/tools/browser-press.ts +80 -0
  47. package/tools/browser-scroll.ts +56 -0
  48. package/tools/browser-snapshot.ts +90 -0
  49. package/tools/browser-type.ts +60 -0
  50. package/tools/index.ts +19 -0
  51. package/tools/utils.ts +157 -0
  52. package/tools/web-fetch.ts +147 -0
  53. package/tools/web-guide.ts +55 -0
  54. package/tools/web-learn.ts +128 -0
  55. package/verify-ship-manifest.ts +126 -0
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Snapshot Disk Cache — Phase 1 of the browser-intelligence plan.
3
+ *
4
+ * When compactSnapshot() truncates a page's accessibility tree, the full
5
+ * tree is written to /tmp/pi-lean-portal/snapshot-*.txt so the agent can read
6
+ * elements past the truncation boundary with the read tool.
7
+ *
8
+ * Design parallels capFetchContent() in fetch-backend.ts:
9
+ * - Pure utility functions, no class or global state beyond a tracking Map
10
+ * - Caches ONLY when truncation occurred (snapshot > 2800 chars)
11
+ * - Graceful I/O degradation (try-catch, no crash on disk errors)
12
+ * - Max 2 files per task (oldest evicted)
13
+ *
14
+ * @module snapshot-cache
15
+ */
16
+
17
+ import { writeFileSync, rmSync } from "node:fs";
18
+ import { createHash } from "node:crypto";
19
+ import { BROWSER_TEMP_DIR, safeTaskId, ensureBrowserTempDir } from "./paths.js";
20
+
21
+ // ─── Constants ──────────────────────────────────────────────────────────
22
+
23
+ /**
24
+ * Snapshot truncation threshold.
25
+ * Snapshots shorter than this are returned as-is (no truncation).
26
+ * Shared with router.ts — both must agree on the cut-off.
27
+ */
28
+ export const SNAPSHOT_TRUNCATE_THRESHOLD = 2800;
29
+
30
+ /** Maximum cached snapshot files per task. */
31
+ const MAX_FILES_PER_TASK = 2;
32
+
33
+ // ─── Types ──────────────────────────────────────────────────────────────
34
+
35
+ /** Result of a cache attempt (null = not cached for any reason). */
36
+ export interface CacheResult {
37
+ /** Absolute path to the cached snapshot file. */
38
+ path: string;
39
+ /** Snapshot fingerprint (for staleness detection). */
40
+ fingerprint: string;
41
+ }
42
+
43
+ /** Internal tracking entry for a cached file. */
44
+ interface CacheEntry {
45
+ path: string;
46
+ fingerprint: string;
47
+ timestamp: number;
48
+ }
49
+
50
+ // ─── Internal state ────────────────────────────────────────────────────
51
+
52
+ /**
53
+ * Tracks active snapshot temp files per task.
54
+ * For each task, entries are kept in insertion order (oldest first).
55
+ */
56
+ const activeSnapshotFiles = new Map<string, CacheEntry[]>();
57
+
58
+ // ─── Helpers ────────────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Compute an 8-char hex fingerprint of a string.
62
+ */
63
+ function sha256Prefix(content: string): string {
64
+ return createHash("sha256").update(content).digest("hex").slice(0, 8);
65
+ }
66
+
67
+ /**
68
+ * Build a snapshot cache file path.
69
+ */
70
+ function buildCacheFilePath(
71
+ taskId: string,
72
+ digest: string,
73
+ index: number,
74
+ ): string {
75
+ const tid = safeTaskId(taskId);
76
+ return `${BROWSER_TEMP_DIR}/snapshot-${tid}-${digest}-${index}.txt`;
77
+ }
78
+
79
+ // ─── Public API ─────────────────────────────────────────────────────────
80
+
81
+ /**
82
+ * Cache a snapshot's full text to a temp file.
83
+ *
84
+ * Only caches when the snapshot is longer than CACHE_TRUNCATE_THRESHOLD
85
+ * (truncation occurred). Gracefully degrades to a no-op on any
86
+ * filesystem error. Evicts the oldest file per task when the count
87
+ * exceeds MAX_FILES_PER_TASK.
88
+ *
89
+ * @param taskId - The task ID (used for file naming and tracking)
90
+ * @param snapshot - The full snapshot text to cache
91
+ * @param fingerprint - A stable hash/fingerprint for the snapshot
92
+ * @returns A CacheResult with path and fingerprint, or null if not cached
93
+ */
94
+ export function cacheSnapshot(
95
+ taskId: string,
96
+ snapshot: string,
97
+ fingerprint: string,
98
+ ): CacheResult | null {
99
+ // Only cache when truncation occurred
100
+ if (snapshot.length <= SNAPSHOT_TRUNCATE_THRESHOLD) {
101
+ return null;
102
+ }
103
+
104
+ try {
105
+ ensureBrowserTempDir();
106
+
107
+ const digest = sha256Prefix(snapshot);
108
+ const existingEntries = activeSnapshotFiles.get(taskId) ?? [];
109
+
110
+ // Determine index: next sequential index based on existing files
111
+ const nextIndex = existingEntries.reduce((max, entry) => {
112
+ const match = entry.path.match(/-(\d+)\.txt$/);
113
+ const idx = match ? parseInt(match[1]!, 10) : -1;
114
+ return Math.max(max, idx + 1);
115
+ }, 0);
116
+
117
+ const filePath = buildCacheFilePath(taskId, digest, nextIndex);
118
+ writeFileSync(filePath, snapshot, "utf-8");
119
+
120
+ // Track the new entry
121
+ const newEntry: CacheEntry = {
122
+ path: filePath,
123
+ fingerprint,
124
+ timestamp: Date.now(),
125
+ };
126
+ existingEntries.push(newEntry);
127
+ activeSnapshotFiles.set(taskId, existingEntries);
128
+
129
+ // Evict oldest if over limit
130
+ if (existingEntries.length > MAX_FILES_PER_TASK) {
131
+ // Sort by timestamp (oldest first) and remove the oldest
132
+ existingEntries.sort((a, b) => a.timestamp - b.timestamp);
133
+ const toRemove = existingEntries.splice(
134
+ 0,
135
+ existingEntries.length - MAX_FILES_PER_TASK,
136
+ );
137
+ for (const entry of toRemove) {
138
+ try {
139
+ rmSync(entry.path, { force: true });
140
+ } catch {
141
+ /* best-effort */
142
+ }
143
+ }
144
+ }
145
+
146
+ return { path: filePath, fingerprint };
147
+ } catch {
148
+ // Graceful degradation: any I/O error → no cache
149
+ return null;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Remove all cached snapshot files for a specific task.
155
+ *
156
+ * @param taskId - The task ID whose cached files should be removed.
157
+ */
158
+ export function removeSnapshotFiles(taskId: string): void {
159
+ const entries = activeSnapshotFiles.get(taskId);
160
+ if (!entries) return;
161
+
162
+ for (const entry of entries) {
163
+ try {
164
+ rmSync(entry.path, { force: true });
165
+ } catch {
166
+ /* best-effort */
167
+ }
168
+ }
169
+ activeSnapshotFiles.delete(taskId);
170
+ }
171
+
172
+ /**
173
+ * Remove ALL cached snapshot files across all tasks.
174
+ * Called during session_shutdown.
175
+ */
176
+ export function removeAllSnapshotFiles(): void {
177
+ for (const [, entries] of activeSnapshotFiles) {
178
+ for (const entry of entries) {
179
+ try {
180
+ rmSync(entry.path, { force: true });
181
+ } catch {
182
+ /* best-effort */
183
+ }
184
+ }
185
+ }
186
+ activeSnapshotFiles.clear();
187
+
188
+ // Also attempt to remove the cache directory itself
189
+ try {
190
+ rmSync(BROWSER_TEMP_DIR, { recursive: true, force: true });
191
+ } catch {
192
+ /* best-effort — dir may not be empty due to other files */
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Build the hint appended to compacted snapshot output.
198
+ *
199
+ * When the snapshot was cached, returns the cache path + action guidance
200
+ * pointing to browser-inspect as a cheaper alternative to loading the full
201
+ * file. When the snapshot was truncated but NOT cached, returns a fallback
202
+ * hint pointing to browser-inspect or full=true. Returns empty string when
203
+ * the snapshot was not truncated.
204
+ *
205
+ * @param cacheResult - The result from cacheSnapshot() (null if not cached)
206
+ * @param snapshotLength - The length of the original (uncached) snapshot
207
+ * @param truncated - Whether the snapshot was truncated by compactSnapshot()
208
+ * @param elementCount - Optional number of interactive elements on the page
209
+ * @returns A hint string (cache notice, fallback hint, or empty string)
210
+ */
211
+ export function formatCacheNotice(
212
+ cacheResult: CacheResult | null,
213
+ snapshotLength: number,
214
+ truncated: boolean,
215
+ elementCount?: number,
216
+ ): string {
217
+ if (truncated && snapshotLength > SNAPSHOT_TRUNCATE_THRESHOLD) {
218
+ if (cacheResult) {
219
+ // Cache was written — show cache path + action guidance
220
+ const guidance =
221
+ elementCount != null && elementCount > 0
222
+ ? ` ${elementCount} elements total — read the cache file for the exact ARIA tree, or use browser-inspect for quick targeted element discovery`
223
+ : ` read the cache file for the exact ARIA tree, or use browser-inspect for quick targeted element discovery`;
224
+ return `\n📄 Full snapshot cached at ${cacheResult.path}\n${guidance}`;
225
+ }
226
+ // Truncated but not cached — fallback hint
227
+ // (replaces old "(use full=true for complete tree)" that was embedded in compactSnapshot())
228
+ return `\n(use browser-inspect role=... name=... to find specific elements, or use browser-snapshot full=true for the complete tree)`;
229
+ }
230
+ return "";
231
+ }