pi-inspect 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.
- package/README.md +42 -0
- package/extensions/inspect.ts +309 -0
- package/extensions/package.json +1 -0
- package/extensions/tsconfig.json +16 -0
- package/lib/snapshot.js +56 -0
- package/package.json +66 -0
- package/public/app.js +616 -0
- package/public/icon-maskable.svg +1 -0
- package/public/icon.svg +1 -0
- package/public/index.html +77 -0
- package/public/manifest.webmanifest +24 -0
- package/public/style.css +1443 -0
- package/public/sw.js +41 -0
- package/server.js +152 -0
- package/themes/pi-dark.json +21 -0
- package/themes/pi-light.json +21 -0
package/public/sw.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
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'];
|
|
4
|
+
|
|
5
|
+
self.addEventListener('install', (event) => {
|
|
6
|
+
event.waitUntil(caches.open(VERSION).then((c) => c.addAll(SHELL)).catch(() => {}));
|
|
7
|
+
self.skipWaiting();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
self.addEventListener('activate', (event) => {
|
|
11
|
+
event.waitUntil(
|
|
12
|
+
caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== VERSION).map((k) => caches.delete(k)))),
|
|
13
|
+
);
|
|
14
|
+
self.clients.claim();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
self.addEventListener('fetch', (event) => {
|
|
18
|
+
const req = event.request;
|
|
19
|
+
if (req.method !== 'GET') return;
|
|
20
|
+
const url = new URL(req.url);
|
|
21
|
+
if (url.origin !== self.location.origin) return;
|
|
22
|
+
|
|
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
|
+
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
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
});
|
package/server.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const express = require('express');
|
|
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 pkg = require('./package.json');
|
|
11
|
+
|
|
12
|
+
const PORT = Number(process.env.PORT) || 5462;
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const shouldOpen = args.includes('--open');
|
|
15
|
+
const openSession = (() => {
|
|
16
|
+
const i = args.indexOf('--session');
|
|
17
|
+
return i >= 0 ? args[i + 1] : null;
|
|
18
|
+
})();
|
|
19
|
+
|
|
20
|
+
const BUILTIN_THEME_DIR = path.join(__dirname, 'themes');
|
|
21
|
+
const USER_THEME_DIR = process.env.INSPECT_THEME_DIR
|
|
22
|
+
|| path.join(os.homedir(), '.pi', 'agent', 'inspect', 'themes');
|
|
23
|
+
|
|
24
|
+
const app = express();
|
|
25
|
+
app.disable('x-powered-by');
|
|
26
|
+
app.use(express.json({ limit: '2mb' }));
|
|
27
|
+
|
|
28
|
+
app.get('/api/version', (_req, res) => {
|
|
29
|
+
res.json({ name: pkg.name, version: pkg.version });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
app.get('/api/sessions', async (_req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const sessions = await snapshots.readIndex();
|
|
35
|
+
const sorted = [...sessions].sort((a, b) => (b.capturedAt || 0) - (a.capturedAt || 0));
|
|
36
|
+
res.json({ sessions: sorted });
|
|
37
|
+
} catch (e) {
|
|
38
|
+
res.status(500).json({ error: e.message });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
app.get('/api/introspect', async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
const sid = req.query.session ? String(req.query.session) : null;
|
|
45
|
+
const snap = sid ? await snapshots.readSnapshot(sid) : await snapshots.readLatestSnapshot();
|
|
46
|
+
if (!snap) return res.status(404).json({ error: 'no snapshot found', sessionId: sid });
|
|
47
|
+
res.json(snap);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
res.status(500).json({ error: e.message });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const themes = new Map();
|
|
54
|
+
async function loadThemes() {
|
|
55
|
+
for (const dir of [BUILTIN_THEME_DIR, USER_THEME_DIR]) {
|
|
56
|
+
let entries;
|
|
57
|
+
try { entries = await fsp.readdir(dir); }
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (e.code !== 'ENOENT') console.warn(`themes: cannot read ${dir}: ${e.message}`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
for (const f of entries.filter((n) => n.toLowerCase().endsWith('.json'))) {
|
|
63
|
+
try {
|
|
64
|
+
const raw = await fsp.readFile(path.join(dir, f), 'utf8');
|
|
65
|
+
const obj = JSON.parse(raw);
|
|
66
|
+
const id = f.replace(/\.json$/i, '');
|
|
67
|
+
themes.set(id, obj);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.warn(`themes: skip ${f}: ${e.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
app.get('/api/themes', (_req, res) => {
|
|
76
|
+
res.json({ themes: [...themes.values()] });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
app.post('/api/open', (req, res) => {
|
|
80
|
+
const target = (req.body && req.body.path) || '';
|
|
81
|
+
if (!target || typeof target !== 'string') return res.status(400).json({ ok: false, error: 'missing path' });
|
|
82
|
+
const normalized = path.normalize(target);
|
|
83
|
+
if (!fs.existsSync(normalized)) return res.status(404).json({ ok: false, error: `path not found: ${normalized}` });
|
|
84
|
+
const editor = process.env.EDITOR || process.env.VISUAL || (process.platform === 'win32' ? 'code' : 'vi');
|
|
85
|
+
try {
|
|
86
|
+
const { spawn } = require('node:child_process');
|
|
87
|
+
const isWin = process.platform === 'win32';
|
|
88
|
+
const parts = editor.split(/\s+/);
|
|
89
|
+
const cmd = parts.shift();
|
|
90
|
+
const args = [...parts, normalized];
|
|
91
|
+
const quotedArgs = isWin ? args.map((a) => `"${String(a).replace(/"/g, '\\"')}"`) : args;
|
|
92
|
+
const child = spawn(cmd, quotedArgs, {
|
|
93
|
+
detached: true,
|
|
94
|
+
stdio: 'ignore',
|
|
95
|
+
shell: isWin,
|
|
96
|
+
windowsVerbatimArguments: isWin,
|
|
97
|
+
});
|
|
98
|
+
child.on('error', (err) => console.warn(`open failed: ${err.message}`));
|
|
99
|
+
child.unref();
|
|
100
|
+
res.json({ ok: true, editor, path: normalized });
|
|
101
|
+
} catch (e) {
|
|
102
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
app.post('/api/focus', (req, res) => {
|
|
107
|
+
const sid = (req.body && req.body.session) || req.query.session || null;
|
|
108
|
+
const count = sseClients.size;
|
|
109
|
+
if (count > 0) broadcast('navigate', { session: sid });
|
|
110
|
+
res.json({ delivered: count });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// SSE
|
|
114
|
+
const sseClients = new Set();
|
|
115
|
+
app.get('/api/events', (req, res) => {
|
|
116
|
+
res.set({
|
|
117
|
+
'Content-Type': 'text/event-stream',
|
|
118
|
+
'Cache-Control': 'no-cache',
|
|
119
|
+
Connection: 'keep-alive',
|
|
120
|
+
});
|
|
121
|
+
res.flushHeaders();
|
|
122
|
+
res.write(': hello\n\n');
|
|
123
|
+
sseClients.add(res);
|
|
124
|
+
req.on('close', () => sseClients.delete(res));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
function broadcast(event, data) {
|
|
128
|
+
const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
129
|
+
for (const c of sseClients) {
|
|
130
|
+
try { c.write(payload); } catch {}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const SNAP_DIR = snapshots.snapshotDir();
|
|
135
|
+
fs.mkdirSync(SNAP_DIR, { recursive: true });
|
|
136
|
+
chokidar
|
|
137
|
+
.watch(SNAP_DIR, { ignoreInitial: true, depth: 1 })
|
|
138
|
+
.on('all', (kind, file) => {
|
|
139
|
+
if (!file.endsWith('.json')) return;
|
|
140
|
+
broadcast('snapshot', { kind, file: path.basename(file) });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
|
144
|
+
|
|
145
|
+
(async () => {
|
|
146
|
+
await loadThemes();
|
|
147
|
+
app.listen(PORT, () => {
|
|
148
|
+
const url = `http://localhost:${PORT}${openSession ? `/?session=${encodeURIComponent(openSession)}` : ''}`;
|
|
149
|
+
console.log(`pi-inspect listening on http://localhost:${PORT}`);
|
|
150
|
+
if (shouldOpen) open(url).catch(() => {});
|
|
151
|
+
});
|
|
152
|
+
})();
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
}
|