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/public/sw.js CHANGED
@@ -1,6 +1,7 @@
1
- // pi-inspect service worker — network-first for dynamic data, cache-first for static shell.
2
- const VERSION = 'pi-inspect-v1';
3
- const SHELL = ['/', '/index.html', '/style.css', '/app.js', '/manifest.webmanifest', '/icon.svg'];
1
+ // Network first, so a new server version shows on the next load; the cache only serves the shell offline (GitHub Pages share links).
2
+ const VERSION = 'pi-inspect-v8';
3
+ const BASE = new URL('./', self.registration?.scope || self.location.href).pathname;
4
+ const SHELL = ['', 'index.html', 'style.css', 'app.js', 'share.js', 'fonts/fonts.css', 'manifest.webmanifest', 'icon.svg'].map((p) => BASE + p);
4
5
 
5
6
  self.addEventListener('install', (event) => {
6
7
  event.waitUntil(caches.open(VERSION).then((c) => c.addAll(SHELL)).catch(() => {}));
@@ -18,24 +19,17 @@ self.addEventListener('fetch', (event) => {
18
19
  const req = event.request;
19
20
  if (req.method !== 'GET') return;
20
21
  const url = new URL(req.url);
21
- if (url.origin !== self.location.origin) return;
22
+ if (url.origin !== self.location.origin || url.pathname.startsWith('/api/')) return;
22
23
 
23
- // Never cache API or SSE — always go to network.
24
- if (url.pathname.startsWith('/api/')) return;
25
-
26
- // Static shell: cache-first, fall back to network, then update cache.
27
24
  event.respondWith(
28
- caches.match(req).then((cached) => {
29
- const fetchPromise = fetch(req)
30
- .then((res) => {
31
- if (res && res.ok) {
32
- const copy = res.clone();
33
- caches.open(VERSION).then((c) => c.put(req, copy)).catch(() => {});
34
- }
35
- return res;
36
- })
37
- .catch(() => cached);
38
- return cached || fetchPromise;
39
- }),
25
+ fetch(req)
26
+ .then((res) => {
27
+ if (res.ok) {
28
+ const copy = res.clone();
29
+ caches.open(VERSION).then((c) => c.put(req, copy)).catch(() => {});
30
+ }
31
+ return res;
32
+ })
33
+ .catch(() => caches.match(req, { ignoreSearch: true }).then((hit) => hit || Response.error())),
40
34
  );
41
35
  });
package/server.js CHANGED
@@ -1,206 +1,35 @@
1
1
  #!/usr/bin/env node
2
- const express = require('express');
2
+ 'use strict';
3
3
  const path = require('node:path');
4
- const fs = require('node:fs');
5
- const fsp = require('node:fs/promises');
6
- const os = require('node:os');
7
- const chokidar = require('chokidar');
8
- const open = require('open').default || require('open');
9
- const snapshots = require('./lib/snapshot');
10
- const githubSource = require('./lib/github-source');
11
- const pkg = require('./package.json');
12
-
13
- const PORT = Number(process.env.PORT) || 5462;
14
- const args = process.argv.slice(2);
15
- const shouldOpen = args.includes('--open');
16
- const openSession = (() => {
17
- const i = args.indexOf('--session');
18
- return i >= 0 ? args[i + 1] : null;
19
- })();
20
-
21
- const BUILTIN_THEME_DIR = path.join(__dirname, 'themes');
22
- const USER_THEME_DIR = process.env.INSPECT_THEME_DIR
23
- || path.join(os.homedir(), '.pi', 'agent', 'inspect', 'themes');
24
-
25
- const app = express();
26
- app.disable('x-powered-by');
27
- app.use(express.json({ limit: '2mb' }));
28
-
29
- app.get('/api/version', (_req, res) => {
30
- res.json({ name: pkg.name, version: pkg.version });
31
- });
32
-
33
- app.get('/api/sessions', async (_req, res) => {
34
- try {
35
- const sessions = await snapshots.readIndex();
36
- const sorted = [...sessions].sort((a, b) => (b.capturedAt || 0) - (a.capturedAt || 0));
37
- res.json({ sessions: sorted });
38
- } catch (e) {
39
- res.status(500).json({ error: e.message });
40
- }
41
- });
42
-
43
- app.post('/api/sessions/cleanup', async (req, res) => {
44
- try {
45
- const keep = (req.body && req.body.keep) || req.query.keep || null;
46
- const result = keep
47
- ? await snapshots.keepOnly(String(keep))
48
- : await snapshots.cleanupIndex();
49
- res.json({ ok: true, ...result });
50
- } catch (e) {
51
- res.status(500).json({ ok: false, error: e.message });
52
- }
53
- });
54
-
55
- const githubMemo = new Map(); // sessionId -> Record<root, {url, source}>
56
-
57
- function collectSourceRoots(snap) {
58
- const roots = new Set();
59
- const add = (x) => {
60
- const baseDir = x?.sourceInfo?.baseDir;
61
- if (typeof baseDir === 'string' && baseDir) roots.add(baseDir);
62
- };
63
- for (const t of snap.tools ?? []) add(t);
64
- for (const c of snap.commands ?? []) add(c);
65
- return [...roots];
4
+ const { parseOptions } = require('./lib/args');
5
+ const { inspectDir } = require('./lib/agent-dir');
6
+ const { createApp, SESSION_PATH } = require('./lib/app');
7
+ const { createSnapshotStore } = require('./lib/snapshot');
8
+ const { watchSnapshots } = require('./lib/watch');
9
+
10
+ let options;
11
+ try {
12
+ options = parseOptions(process.argv.slice(2));
13
+ } catch (e) {
14
+ console.error(`pi-inspect: ${e.message}`);
15
+ process.exit(1);
66
16
  }
67
17
 
68
- app.get('/api/introspect', async (req, res) => {
69
- try {
70
- const sid = req.query.session ? String(req.query.session) : null;
71
- const snap = sid ? await snapshots.readSnapshot(sid) : await snapshots.readLatestSnapshot();
72
- if (!snap) return res.status(404).json({ error: 'no snapshot found', sessionId: sid });
73
- const memoKey = snap.sessionId;
74
- let githubSources = memoKey ? githubMemo.get(memoKey) : null;
75
- if (!githubSources) {
76
- githubSources = await githubSource.resolveMany(collectSourceRoots(snap));
77
- if (memoKey) githubMemo.set(memoKey, githubSources);
78
- }
79
- res.json({ ...snap, githubSources });
80
- } catch (e) {
81
- res.status(500).json({ error: e.message });
82
- }
83
- });
18
+ const INSPECT_DIR = inspectDir();
19
+ const store = createSnapshotStore(path.join(INSPECT_DIR, 'snapshots'));
20
+ const { app, broadcast } = createApp({ store, inspectDir: INSPECT_DIR, host: options.host });
84
21
 
85
- const themes = new Map();
86
- async function loadThemes() {
87
- for (const dir of [BUILTIN_THEME_DIR, USER_THEME_DIR]) {
88
- let entries;
89
- try { entries = await fsp.readdir(dir); }
90
- catch (e) {
91
- if (e.code !== 'ENOENT') console.warn(`themes: cannot read ${dir}: ${e.message}`);
92
- continue;
93
- }
94
- for (const f of entries.filter((n) => n.toLowerCase().endsWith('.json'))) {
95
- try {
96
- const raw = await fsp.readFile(path.join(dir, f), 'utf8');
97
- const obj = JSON.parse(raw);
98
- const id = f.replace(/\.json$/i, '');
99
- themes.set(id, obj);
100
- } catch (e) {
101
- console.warn(`themes: skip ${f}: ${e.message}`);
102
- }
103
- }
104
- }
105
- }
22
+ watchSnapshots(store.dir, (files) => broadcast('snapshot', { files }));
106
23
 
107
- app.get('/api/themes', (_req, res) => {
108
- res.json({ themes: [...themes.values()] });
109
- });
110
-
111
- app.post('/api/open', (req, res) => {
112
- const target = (req.body && req.body.path) || '';
113
- if (!target || typeof target !== 'string') return res.status(400).json({ ok: false, error: 'missing path' });
114
- const normalized = path.normalize(target);
115
- if (!fs.existsSync(normalized)) return res.status(404).json({ ok: false, error: `path not found: ${normalized}` });
116
- const editor = process.env.EDITOR || process.env.VISUAL || (process.platform === 'win32' ? 'code' : 'vi');
117
- try {
118
- const { spawn } = require('node:child_process');
119
- const isWin = process.platform === 'win32';
120
- const parts = editor.split(/\s+/);
121
- const cmd = parts.shift();
122
- const args = [...parts, normalized];
123
- const quotedArgs = isWin ? args.map((a) => `"${String(a).replace(/"/g, '\\"')}"`) : args;
124
- const child = spawn(cmd, quotedArgs, {
125
- detached: true,
126
- stdio: 'ignore',
127
- shell: isWin,
128
- windowsVerbatimArguments: isWin,
129
- });
130
- child.on('error', (err) => console.warn(`open failed: ${err.message}`));
131
- child.unref();
132
- res.json({ ok: true, editor, path: normalized });
133
- } catch (e) {
134
- res.status(500).json({ ok: false, error: e.message });
135
- }
136
- });
137
-
138
- const REQ_DIR = path.join(os.homedir(), '.pi', 'agent', 'inspect', 'requests');
139
-
140
- app.post('/api/toggle', async (req, res) => {
141
- try {
142
- const { action, resourceKind, path: target, scope } = req.body || {};
143
- if (action !== 'enable' && action !== 'disable') return res.status(400).json({ ok: false, error: 'action must be enable|disable' });
144
- if (!['skills', 'prompts', 'extensions', 'themes'].includes(resourceKind)) return res.status(400).json({ ok: false, error: 'invalid resourceKind' });
145
- if (!target || typeof target !== 'string') return res.status(400).json({ ok: false, error: 'missing path' });
146
- const useScope = scope === 'project' ? 'project' : 'user';
147
- await fsp.mkdir(REQ_DIR, { recursive: true });
148
- const id = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
149
- const file = path.join(REQ_DIR, `${id}.json`);
150
- const tmp = `${file}.tmp`;
151
- const body = JSON.stringify({ id, ts: Date.now(), action, resourceKind, path: target, scope: useScope });
152
- await fsp.writeFile(tmp, body, 'utf8');
153
- await fsp.rename(tmp, file);
154
- res.json({ ok: true, id });
155
- } catch (e) {
156
- res.status(500).json({ ok: false, error: e.message });
24
+ const server = app.listen(options.port, options.host, async (err) => {
25
+ if (err) {
26
+ console.error(`pi-inspect: cannot listen on port ${options.port}: ${err.message}`);
27
+ process.exit(1);
157
28
  }
29
+ const base = `http://localhost:${server.address().port}`;
30
+ console.log(`pi-inspect listening on ${base}`);
31
+ if (!options.open) return;
32
+ const url = options.session ? base + SESSION_PATH.replace('{id}', encodeURIComponent(options.session)) : base;
33
+ const { default: open } = await import('open');
34
+ open(url).catch(() => {});
158
35
  });
159
-
160
- app.post('/api/focus', (req, res) => {
161
- const sid = (req.body && req.body.session) || req.query.session || null;
162
- const count = sseClients.size;
163
- if (count > 0) broadcast('navigate', { session: sid });
164
- res.json({ delivered: count });
165
- });
166
-
167
- // SSE
168
- const sseClients = new Set();
169
- app.get('/api/events', (req, res) => {
170
- res.set({
171
- 'Content-Type': 'text/event-stream',
172
- 'Cache-Control': 'no-cache',
173
- Connection: 'keep-alive',
174
- });
175
- res.flushHeaders();
176
- res.write(': hello\n\n');
177
- sseClients.add(res);
178
- req.on('close', () => sseClients.delete(res));
179
- });
180
-
181
- function broadcast(event, data) {
182
- const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
183
- for (const c of sseClients) {
184
- try { c.write(payload); } catch {}
185
- }
186
- }
187
-
188
- const SNAP_DIR = snapshots.snapshotDir();
189
- fs.mkdirSync(SNAP_DIR, { recursive: true });
190
- chokidar
191
- .watch(SNAP_DIR, { ignoreInitial: true, depth: 1 })
192
- .on('all', (kind, file) => {
193
- if (!file.endsWith('.json')) return;
194
- broadcast('snapshot', { kind, file: path.basename(file) });
195
- });
196
-
197
- app.use(express.static(path.join(__dirname, 'public')));
198
-
199
- (async () => {
200
- await loadThemes();
201
- app.listen(PORT, () => {
202
- const url = `http://localhost:${PORT}${openSession ? `/?session=${encodeURIComponent(openSession)}` : ''}`;
203
- console.log(`pi-inspect listening on http://localhost:${PORT}`);
204
- if (shouldOpen) open(url).catch(() => {});
205
- });
206
- })();
@@ -1,21 +0,0 @@
1
- {
2
- "name": "pi-dark",
3
- "displayName": "pi.dev Dark",
4
- "mode": "dark",
5
- "colors": {
6
- "bgDeep": "#1a1a1a",
7
- "bgSurface": "#212121",
8
- "bgElevated": "#2a2a2a",
9
- "bgHover": "#333333",
10
- "border": "#3a3a3a",
11
- "textPrimary": "#f0ebe6",
12
- "textSecondary": "#c9c2bb",
13
- "textTertiary": "#8b857e",
14
- "textMuted": "#5f5a55",
15
- "accent": "#e8927c",
16
- "accentText": "#f5b8a8",
17
- "tool": "#6ea8d8",
18
- "command": "#a78bfa",
19
- "injected": "#f0b14d"
20
- }
21
- }
@@ -1,21 +0,0 @@
1
- {
2
- "name": "pi-light",
3
- "displayName": "pi.dev Light",
4
- "mode": "light",
5
- "colors": {
6
- "bgDeep": "#ebe7e4",
7
- "bgSurface": "#f4f1ee",
8
- "bgElevated": "#e0dcd8",
9
- "bgHover": "#d5d1cd",
10
- "border": "#b5b0aa",
11
- "textPrimary": "#0d1116",
12
- "textSecondary": "#212730",
13
- "textTertiary": "#495059",
14
- "textMuted": "#757d89",
15
- "accent": "#2f5f8a",
16
- "accentText": "#1d4870",
17
- "tool": "#2f5f8a",
18
- "command": "#7c3aed",
19
- "injected": "#b45309"
20
- }
21
- }