pi-inspect 0.4.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +65 -3
  3. package/extensions/inspect.ts +54 -52
  4. package/lib/agent-dir.js +18 -0
  5. package/lib/app.js +144 -0
  6. package/lib/args.js +30 -0
  7. package/lib/editor.js +26 -0
  8. package/lib/snapshot.js +57 -79
  9. package/lib/watch.js +29 -0
  10. package/package.json +9 -8
  11. package/public/app.js +676 -591
  12. package/public/fonts/OFL-IBM-Plex.txt +93 -0
  13. package/public/fonts/OFL-Playfair-Display.txt +93 -0
  14. package/public/fonts/fonts.css +244 -0
  15. package/public/fonts/ibm-plex-mono-cyrillic-400.woff2 +0 -0
  16. package/public/fonts/ibm-plex-mono-cyrillic-500.woff2 +0 -0
  17. package/public/fonts/ibm-plex-mono-cyrillic-600.woff2 +0 -0
  18. package/public/fonts/ibm-plex-mono-cyrillic-ext-400.woff2 +0 -0
  19. package/public/fonts/ibm-plex-mono-cyrillic-ext-500.woff2 +0 -0
  20. package/public/fonts/ibm-plex-mono-cyrillic-ext-600.woff2 +0 -0
  21. package/public/fonts/ibm-plex-mono-latin-400.woff2 +0 -0
  22. package/public/fonts/ibm-plex-mono-latin-500.woff2 +0 -0
  23. package/public/fonts/ibm-plex-mono-latin-600.woff2 +0 -0
  24. package/public/fonts/ibm-plex-mono-latin-ext-400.woff2 +0 -0
  25. package/public/fonts/ibm-plex-mono-latin-ext-500.woff2 +0 -0
  26. package/public/fonts/ibm-plex-mono-latin-ext-600.woff2 +0 -0
  27. package/public/fonts/ibm-plex-mono-vietnamese-400.woff2 +0 -0
  28. package/public/fonts/ibm-plex-mono-vietnamese-500.woff2 +0 -0
  29. package/public/fonts/ibm-plex-mono-vietnamese-600.woff2 +0 -0
  30. package/public/fonts/playfair-display-cyrillic-400.woff2 +0 -0
  31. package/public/fonts/playfair-display-latin-400.woff2 +0 -0
  32. package/public/fonts/playfair-display-latin-ext-400.woff2 +0 -0
  33. package/public/fonts/playfair-display-vietnamese-400.woff2 +0 -0
  34. package/public/index.html +96 -75
  35. package/public/share.js +119 -0
  36. package/public/style.css +416 -1245
  37. package/public/sw.js +14 -20
  38. package/server.js +27 -198
  39. package/themes/pi-dark.json +0 -21
  40. package/themes/pi-light.json +0 -21
package/lib/snapshot.js CHANGED
@@ -1,102 +1,80 @@
1
- const fs = require('node:fs');
1
+ 'use strict';
2
2
  const fsp = require('node:fs/promises');
3
3
  const path = require('node:path');
4
- const os = require('node:os');
5
4
 
6
- const SNAPSHOT_DIR = process.env.INSPECT_SNAPSHOT_DIR
7
- || path.join(os.homedir(), '.pi', 'agent', 'inspect', 'snapshots');
8
- const INDEX_PATH = path.join(SNAPSHOT_DIR, 'index.json');
9
-
10
- function snapshotDir() {
11
- return SNAPSHOT_DIR;
12
- }
13
-
14
- function snapshotPath(sessionId) {
15
- return path.join(SNAPSHOT_DIR, `${sanitize(sessionId)}.json`);
16
- }
5
+ const INDEX_FILE = 'index.json';
17
6
 
7
+ // Same rule as the extension's file names, so an id maps to the file it wrote.
18
8
  function sanitize(id) {
19
9
  return String(id).replace(/[^a-zA-Z0-9._-]/g, '_');
20
10
  }
21
11
 
22
- async function readIndex() {
23
- try {
24
- const raw = await fsp.readFile(INDEX_PATH, 'utf8');
25
- const parsed = JSON.parse(raw);
26
- return Array.isArray(parsed?.sessions) ? parsed.sessions : [];
27
- } catch (e) {
28
- if (e.code !== 'ENOENT') console.warn(`pi-inspect: read index: ${e.message}`);
29
- return [];
30
- }
12
+ async function writeJsonAtomic(file, value) {
13
+ await fsp.mkdir(path.dirname(file), { recursive: true });
14
+ const tmp = `${file}.${process.pid}.tmp`;
15
+ await fsp.writeFile(tmp, JSON.stringify(value, null, 2), 'utf8');
16
+ await fsp.rename(tmp, file);
31
17
  }
32
18
 
33
- async function readSnapshot(sessionId) {
34
- try {
35
- const raw = await fsp.readFile(snapshotPath(sessionId), 'utf8');
36
- return JSON.parse(raw);
37
- } catch (e) {
38
- if (e.code === 'ENOENT') return null;
39
- throw e;
40
- }
41
- }
19
+ const byCapturedDesc = (a, b) => (b.capturedAt || 0) - (a.capturedAt || 0);
42
20
 
43
- async function writeIndex(sessions) {
44
- await fsp.mkdir(SNAPSHOT_DIR, { recursive: true });
45
- await fsp.writeFile(INDEX_PATH, JSON.stringify({ sessions }, null, 2), 'utf8');
46
- }
21
+ function createSnapshotStore(dir) {
22
+ const indexPath = path.join(dir, INDEX_FILE);
23
+ const snapshotPath = (id) => path.join(dir, `${sanitize(id)}.json`);
47
24
 
48
- async function cleanupIndex() {
49
- const sessions = await readIndex();
50
- const kept = [];
51
- const removed = [];
52
- for (const s of sessions) {
25
+ async function readIndex() {
53
26
  try {
54
- await fsp.access(snapshotPath(s.id));
55
- kept.push(s);
56
- } catch {
57
- removed.push(s.id);
27
+ const parsed = JSON.parse(await fsp.readFile(indexPath, 'utf8'));
28
+ return Array.isArray(parsed?.sessions) ? parsed.sessions : [];
29
+ } catch (e) {
30
+ if (e.code !== 'ENOENT') console.warn(`pi-inspect: read index: ${e.message}`);
31
+ return [];
58
32
  }
59
33
  }
60
- if (removed.length) await writeIndex(kept);
61
- return { kept: kept.length, removed };
62
- }
63
34
 
64
- async function keepOnly(sessionId) {
65
- if (!sessionId) throw new Error('sessionId required');
66
- const sessions = await readIndex();
67
- const kept = sessions.filter((s) => s.id === sessionId);
68
- const removed = [];
69
- let files;
70
- try { files = await fsp.readdir(SNAPSHOT_DIR); }
71
- catch { files = []; }
72
- const keepPath = path.basename(snapshotPath(sessionId));
73
- for (const f of files) {
74
- if (!f.endsWith('.json') || f === 'index.json' || f === keepPath) continue;
35
+ function writeIndex(sessions) {
36
+ return writeJsonAtomic(indexPath, { sessions });
37
+ }
38
+
39
+ async function listSessions() {
40
+ return (await readIndex()).sort(byCapturedDesc);
41
+ }
42
+
43
+ async function readSnapshot(id) {
75
44
  try {
76
- await fsp.unlink(path.join(SNAPSHOT_DIR, f));
77
- removed.push(f.replace(/\.json$/, ''));
45
+ return JSON.parse(await fsp.readFile(snapshotPath(id), 'utf8'));
78
46
  } catch (e) {
79
- console.warn(`pi-inspect: unlink ${f}: ${e.message}`);
47
+ if (e.code === 'ENOENT') return null;
48
+ throw e;
80
49
  }
81
50
  }
82
- await writeIndex(kept);
83
- return { kept: kept.length, removed };
84
- }
85
51
 
86
- async function readLatestSnapshot() {
87
- const sessions = await readIndex();
88
- if (!sessions.length) return null;
89
- const sorted = [...sessions].sort((a, b) => (b.capturedAt || 0) - (a.capturedAt || 0));
90
- return readSnapshot(sorted[0].id);
52
+ async function readLatestSnapshot() {
53
+ const [latest] = await listSessions();
54
+ return latest ? readSnapshot(latest.id) : null;
55
+ }
56
+
57
+ async function keepOnly(id) {
58
+ if (!id) throw new Error('session id required');
59
+ const keepFile = path.basename(snapshotPath(id));
60
+ let files;
61
+ try { files = await fsp.readdir(dir); } catch { files = []; }
62
+ const removed = [];
63
+ for (const f of files) {
64
+ if (!f.endsWith('.json') || f === INDEX_FILE || f === keepFile) continue;
65
+ try {
66
+ await fsp.unlink(path.join(dir, f));
67
+ removed.push(f.slice(0, -'.json'.length));
68
+ } catch (e) {
69
+ console.warn(`pi-inspect: unlink ${f}: ${e.message}`);
70
+ }
71
+ }
72
+ const kept = (await readIndex()).filter((s) => s.id === id);
73
+ await writeIndex(kept);
74
+ return { kept: kept.length, removed };
75
+ }
76
+
77
+ return { dir, snapshotPath, writeIndex, listSessions, readSnapshot, readLatestSnapshot, keepOnly };
91
78
  }
92
79
 
93
- module.exports = {
94
- snapshotDir,
95
- snapshotPath,
96
- readIndex,
97
- writeIndex,
98
- cleanupIndex,
99
- keepOnly,
100
- readSnapshot,
101
- readLatestSnapshot,
102
- };
80
+ module.exports = { createSnapshotStore, sanitize, writeJsonAtomic };
package/lib/watch.js ADDED
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+
4
+ // fs.watch fires several events per write, so changes are batched into one call per quiet period.
5
+ function watchSnapshots(dir, onChange, { delayMs = 150 } = {}) {
6
+ fs.mkdirSync(dir, { recursive: true });
7
+ let timer = null;
8
+ const changed = new Set();
9
+ // libuv aborts when it watches a Windows 8.3 short path (C:\Users\NAME~1\...), so watch the long form.
10
+ const watcher = fs.watch(fs.realpathSync.native(dir), (_event, filename) => {
11
+ const name = filename && String(filename);
12
+ if (!name || !name.endsWith('.json')) return;
13
+ changed.add(name);
14
+ clearTimeout(timer);
15
+ timer = setTimeout(() => {
16
+ const files = [...changed];
17
+ changed.clear();
18
+ onChange(files);
19
+ }, delayMs);
20
+ });
21
+ return {
22
+ close() {
23
+ clearTimeout(timer);
24
+ watcher.close();
25
+ },
26
+ };
27
+ }
28
+
29
+ module.exports = { watchSnapshots };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-inspect",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Introspection dashboard for the pi coding agent — tools, slash commands, skills, and the system prompt injected on init.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -29,22 +29,23 @@
29
29
  "lib/",
30
30
  "public/",
31
31
  "extensions/",
32
- "themes/",
33
- "README.md"
32
+ "README.md",
33
+ "CHANGELOG.md"
34
34
  ],
35
35
  "scripts": {
36
36
  "start": "node server.js",
37
37
  "dev": "node server.js --open",
38
+ "test": "node --test test/*.test.js",
38
39
  "typecheck:extensions": "tsc -p extensions/tsconfig.json --noEmit"
39
40
  },
40
41
  "dependencies": {
41
- "chokidar": "^3.5.3",
42
- "express": "^4.19.2",
43
- "open": "^10.1.0"
42
+ "express": "^5.3.0",
43
+ "open": "^11.0.4"
44
44
  },
45
45
  "devDependencies": {
46
- "@types/node": "^25.6.2",
47
- "typescript": "^5.8.2"
46
+ "@earendil-works/pi-coding-agent": "^1.4.0",
47
+ "@types/node": "^25.9.9",
48
+ "typescript": "^5.9.3"
48
49
  },
49
50
  "peerDependencies": {
50
51
  "@earendil-works/pi-coding-agent": "*"