pi-inspect 0.4.0 → 0.5.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 +8 -0
- package/package.json +1 -1
- package/public/app.js +68 -7
- package/public/index.html +6 -1
- package/public/share.js +119 -0
- package/public/sw.js +4 -2
package/README.md
CHANGED
|
@@ -31,6 +31,14 @@ Then use `/inspect start | stop | restart | status | open | list | snapshot` fro
|
|
|
31
31
|
|
|
32
32
|
State is driven entirely through the `?session=` URL param — share or refresh URLs to pin views. The in-page picker also writes to the URL.
|
|
33
33
|
|
|
34
|
+
## Sharing a snapshot
|
|
35
|
+
|
|
36
|
+
Click **Share** in the topbar to copy a self-contained link of the current snapshot. The snapshot is `deflate-raw` compressed and base64url-encoded into the URL hash (`#s=…`) — no server, no upload, no account.
|
|
37
|
+
|
|
38
|
+
Recipients open the link on the hosted static dashboard at **https://nikiforovall.blog/pi-inspect/** and see the exact same tools / commands / skills / system prompt. The page makes no network requests; everything is in the URL.
|
|
39
|
+
|
|
40
|
+
Heads up: the link includes the system prompt and `cwd`. Don't share secrets you wouldn't paste in chat.
|
|
41
|
+
|
|
34
42
|
## What it captures
|
|
35
43
|
|
|
36
44
|
- **Tools** — name, description, parameter schema, source
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -10,6 +10,7 @@ const state = {
|
|
|
10
10
|
expandAll: true,
|
|
11
11
|
highlight: -1,
|
|
12
12
|
visibleRows: [],
|
|
13
|
+
staticMode: false,
|
|
13
14
|
};
|
|
14
15
|
const els = {};
|
|
15
16
|
|
|
@@ -358,7 +359,9 @@ function renderTree() {
|
|
|
358
359
|
const root = $('treeContainer');
|
|
359
360
|
const items = filterItems(buildItems());
|
|
360
361
|
if (!state.snapshot) {
|
|
361
|
-
root.innerHTML =
|
|
362
|
+
root.innerHTML = state.staticMode
|
|
363
|
+
? `<div class="loading">No snapshot in this URL. Open a shared link, or run pi /inspect locally.</div>`
|
|
364
|
+
: `<div class="loading">No snapshot for this session. Run <code>/inspect snapshot</code> in a pi session.</div>`;
|
|
362
365
|
return;
|
|
363
366
|
}
|
|
364
367
|
if (!items.length) {
|
|
@@ -542,12 +545,13 @@ function renderDetail() {
|
|
|
542
545
|
|
|
543
546
|
const ghUrl = githubUrlFor(it);
|
|
544
547
|
const toggleInfo = toggleInfoFor(it);
|
|
548
|
+
const allowEditor = !state.staticMode && it.path;
|
|
545
549
|
panel.innerHTML = `
|
|
546
550
|
<div class="detail-header">
|
|
547
551
|
<h3>${iconFor(it.kind)} ${esc(it.name)} <span class="version">${esc(it.kind)}</span></h3>
|
|
548
552
|
<div class="detail-header-actions">
|
|
549
553
|
${toggleInfo ? `<button class="detail-action" id="toggleBtn" title="${toggleInfo.enable ? 'Enable' : 'Disable'} (${toggleInfo.scope})">${toggleInfo.enable ? 'Enable' : 'Disable'}</button>` : ''}
|
|
550
|
-
${
|
|
554
|
+
${allowEditor ? `<button class="detail-action" id="openEditorBtn" title="Open in $EDITOR"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></button>` : ''}
|
|
551
555
|
${it.path ? `<button class="detail-action" id="copyPathBtn" title="Copy path"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>` : ''}
|
|
552
556
|
${ghUrl ? `<button class="detail-action" id="openGithubBtn" title="Open on GitHub"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.56v-2c-3.2.7-3.88-1.37-3.88-1.37-.52-1.33-1.27-1.68-1.27-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.03 1.76 2.7 1.25 3.36.96.1-.75.4-1.25.73-1.54-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.18-3.1-.12-.29-.51-1.46.11-3.05 0 0 .96-.31 3.15 1.18a10.96 10.96 0 015.74 0c2.18-1.49 3.14-1.18 3.14-1.18.63 1.59.23 2.76.11 3.05.74.81 1.18 1.84 1.18 3.1 0 4.42-2.69 5.39-5.25 5.68.41.36.78 1.06.78 2.14v3.17c0 .31.21.68.8.56C20.21 21.38 23.5 17.07 23.5 12 23.5 5.65 18.35.5 12 .5z"/></svg></button>` : ''}
|
|
553
557
|
<button class="detail-close" id="detailCloseBtn" title="Close">✕</button>
|
|
@@ -588,6 +592,7 @@ function renderDetail() {
|
|
|
588
592
|
});
|
|
589
593
|
const toggleBtn = $('toggleBtn');
|
|
590
594
|
if (toggleBtn && toggleInfo) toggleBtn.addEventListener('click', async () => {
|
|
595
|
+
if (state.staticMode) { toast('shared view is read-only — run pi-inspect locally to toggle'); return; }
|
|
591
596
|
toggleBtn.disabled = true;
|
|
592
597
|
try {
|
|
593
598
|
const r = await fetch('/api/toggle', {
|
|
@@ -677,6 +682,24 @@ function bindEvents() {
|
|
|
677
682
|
renderTree();
|
|
678
683
|
});
|
|
679
684
|
|
|
685
|
+
$('shareBtn').addEventListener('click', async () => {
|
|
686
|
+
if (!state.snapshot) { toast('no snapshot to share'); return; }
|
|
687
|
+
if (!window.piShare) { toast('share module not loaded'); return; }
|
|
688
|
+
const btn = $('shareBtn');
|
|
689
|
+
btn.classList.add('loading');
|
|
690
|
+
try {
|
|
691
|
+
const encoded = await window.piShare.encodeSnapshot(state.snapshot);
|
|
692
|
+
const url = window.piShare.buildShareUrl(encoded);
|
|
693
|
+
await navigator.clipboard.writeText(url);
|
|
694
|
+
const kb = Math.max(1, Math.round(url.length / 1024));
|
|
695
|
+
toast(`Share link copied — ${kb} KB. Paths redacted to <home>.`);
|
|
696
|
+
} catch (e) {
|
|
697
|
+
toast(`Share failed: ${e.message}`);
|
|
698
|
+
} finally {
|
|
699
|
+
btn.classList.remove('loading');
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
680
703
|
$('refreshBtn').addEventListener('click', async () => {
|
|
681
704
|
const btn = $('refreshBtn');
|
|
682
705
|
btn.classList.add('loading');
|
|
@@ -887,6 +910,34 @@ function bindSse() {
|
|
|
887
910
|
//#endregion
|
|
888
911
|
|
|
889
912
|
//#region INIT
|
|
913
|
+
async function loadSharedSnapshot() {
|
|
914
|
+
const param = window.piShare?.getSharedSnapshotParam();
|
|
915
|
+
if (!param) return false;
|
|
916
|
+
try {
|
|
917
|
+
state.snapshot = await window.piShare.decodeSnapshot(param);
|
|
918
|
+
state.currentSessionId = state.snapshot?.sessionId ?? null;
|
|
919
|
+
state.sessions = state.currentSessionId
|
|
920
|
+
? [{ id: state.currentSessionId, name: state.snapshot.sessionName, cwd: state.snapshot.cwd }]
|
|
921
|
+
: [];
|
|
922
|
+
state.staticMode = true;
|
|
923
|
+
return true;
|
|
924
|
+
} catch (e) {
|
|
925
|
+
console.warn('Failed to decode shared snapshot:', e);
|
|
926
|
+
toast(`shared link decode failed: ${e.message}`, 'error');
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function applyStaticModeUi() {
|
|
932
|
+
document.body.classList.add('static-mode');
|
|
933
|
+
for (const id of ['sessionSelect', 'cleanupSessionsBtn', 'refreshBtn']) {
|
|
934
|
+
const el = $(id);
|
|
935
|
+
if (el) el.style.display = 'none';
|
|
936
|
+
}
|
|
937
|
+
const share = $('shareBtn');
|
|
938
|
+
if (share) share.title = 'Re-copy this shared snapshot link';
|
|
939
|
+
}
|
|
940
|
+
|
|
890
941
|
(async function init() {
|
|
891
942
|
try {
|
|
892
943
|
if (localStorage.getItem('inspect.theme') === 'light') document.body.classList.add('light');
|
|
@@ -895,10 +946,20 @@ function bindSse() {
|
|
|
895
946
|
bindResize();
|
|
896
947
|
bindEvents();
|
|
897
948
|
|
|
898
|
-
await
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
949
|
+
const sharedLoaded = await loadSharedSnapshot();
|
|
950
|
+
if (!sharedLoaded) {
|
|
951
|
+
try {
|
|
952
|
+
await loadSessions();
|
|
953
|
+
const requested = getUrlSession();
|
|
954
|
+
await loadSnapshot(requested);
|
|
955
|
+
if (state.currentSessionId && !requested) setUrlSession(state.currentSessionId, true);
|
|
956
|
+
} catch {
|
|
957
|
+
state.staticMode = true;
|
|
958
|
+
state.snapshot = null;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (state.staticMode) applyStaticModeUi();
|
|
902
963
|
|
|
903
964
|
if (state.snapshot?.systemPrompt) {
|
|
904
965
|
const firstCtx = buildItems().find((x) => x.kind === 'context');
|
|
@@ -908,6 +969,6 @@ function bindSse() {
|
|
|
908
969
|
renderTopbar();
|
|
909
970
|
renderTree();
|
|
910
971
|
renderDetail();
|
|
911
|
-
bindSse();
|
|
972
|
+
if (!state.staticMode) bindSse();
|
|
912
973
|
})();
|
|
913
974
|
//#endregion
|
package/public/index.html
CHANGED
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
|
|
43
43
|
<span id="projectPath">—</span>
|
|
44
44
|
</div>
|
|
45
|
+
<button class="topbar-btn" id="shareBtn" title="Copy a shareable link for the current snapshot">
|
|
46
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
|
|
47
|
+
Share
|
|
48
|
+
</button>
|
|
45
49
|
<button class="topbar-btn" id="refreshBtn" title="Refresh data">
|
|
46
50
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
|
|
47
51
|
Refresh
|
|
@@ -111,11 +115,12 @@
|
|
|
111
115
|
</div>
|
|
112
116
|
</div>
|
|
113
117
|
|
|
118
|
+
<script src="share.js"></script>
|
|
114
119
|
<script src="app.js"></script>
|
|
115
120
|
<script>
|
|
116
121
|
if ('serviceWorker' in navigator) {
|
|
117
122
|
window.addEventListener('load', () => {
|
|
118
|
-
navigator.serviceWorker.register('
|
|
123
|
+
navigator.serviceWorker.register('./sw.js').catch((e) => console.warn('sw register failed:', e));
|
|
119
124
|
});
|
|
120
125
|
}
|
|
121
126
|
</script>
|
package/public/share.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Shareable snapshot encoding: JSON → deflate-raw → base64url.
|
|
2
|
+
// URL shape mirrors plannotator.ai: `#s=<base64url>`.
|
|
3
|
+
|
|
4
|
+
const HASH_PREFIX = '#s=';
|
|
5
|
+
|
|
6
|
+
function bytesToBase64Url(bytes) {
|
|
7
|
+
let bin = '';
|
|
8
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
9
|
+
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function base64UrlToBytes(str) {
|
|
13
|
+
const pad = str.length % 4 === 0 ? '' : '='.repeat(4 - (str.length % 4));
|
|
14
|
+
const b64 = str.replace(/-/g, '+').replace(/_/g, '/') + pad;
|
|
15
|
+
const bin = atob(b64);
|
|
16
|
+
const out = new Uint8Array(bin.length);
|
|
17
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function streamThrough(transformer, bytes) {
|
|
22
|
+
const stream = new Blob([bytes]).stream().pipeThrough(transformer);
|
|
23
|
+
const buf = await new Response(stream).arrayBuffer();
|
|
24
|
+
return new Uint8Array(buf);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Path-redaction: strip the sender's HOME prefix from every string and key,
|
|
28
|
+
// in both slash flavors. Preserves project basename + relative tail so the
|
|
29
|
+
// shared view still has useful structure ("<home>/dev/pi-inspect").
|
|
30
|
+
function escapeRegex(s) {
|
|
31
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function deriveHome(cwd) {
|
|
35
|
+
if (!cwd || typeof cwd !== 'string') return null;
|
|
36
|
+
const patterns = [
|
|
37
|
+
/^([a-zA-Z]:[\\/]Users[\\/][^\\/]+)/, // Windows: C:\Users\<user>
|
|
38
|
+
/^(\/Users\/[^/]+)/, // macOS: /Users/<user>
|
|
39
|
+
/^(\/home\/[^/]+)/, // Linux: /home/<user>
|
|
40
|
+
/^(\/[a-zA-Z]\/Users\/[^/]+)/, // Git Bash: /c/Users/<user>
|
|
41
|
+
];
|
|
42
|
+
for (const re of patterns) {
|
|
43
|
+
const m = cwd.match(re);
|
|
44
|
+
if (m) return m[1];
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildHomeReplacers(cwd) {
|
|
50
|
+
const home = deriveHome(cwd);
|
|
51
|
+
if (!home) return [];
|
|
52
|
+
const alt = home.includes('\\') ? home.replace(/\\/g, '/') : home.replace(/\//g, '\\');
|
|
53
|
+
const variants = new Set([home, alt]);
|
|
54
|
+
return [...variants].map((v) => [new RegExp(escapeRegex(v), 'g'), '<home>']);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function redactString(s, reps) {
|
|
58
|
+
if (typeof s !== 'string') return s;
|
|
59
|
+
let out = s;
|
|
60
|
+
for (const [re, rep] of reps) out = out.replace(re, rep);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Keys whose values are home-path-keyed maps — redact keys here too.
|
|
65
|
+
const PATH_KEYED_MAPS = new Set(['githubSources']);
|
|
66
|
+
|
|
67
|
+
function redactDeep(value, reps, redactKeys) {
|
|
68
|
+
if (value == null) return value;
|
|
69
|
+
if (typeof value === 'string') return redactString(value, reps);
|
|
70
|
+
if (Array.isArray(value)) return value.map((v) => redactDeep(v, reps, false));
|
|
71
|
+
if (typeof value === 'object') {
|
|
72
|
+
const out = {};
|
|
73
|
+
for (const [k, v] of Object.entries(value)) {
|
|
74
|
+
const newKey = redactKeys ? redactString(k, reps) : k;
|
|
75
|
+
out[newKey] = redactDeep(v, reps, PATH_KEYED_MAPS.has(k));
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function redactSnapshot(snapshot) {
|
|
83
|
+
if (!snapshot) return snapshot;
|
|
84
|
+
const reps = buildHomeReplacers(snapshot.cwd || '');
|
|
85
|
+
if (!reps.length) return snapshot;
|
|
86
|
+
return redactDeep(snapshot, reps, false);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function encodeSnapshot(snapshot) {
|
|
90
|
+
const json = JSON.stringify(redactSnapshot(snapshot));
|
|
91
|
+
const raw = new TextEncoder().encode(json);
|
|
92
|
+
const compressed = await streamThrough(new CompressionStream('deflate-raw'), raw);
|
|
93
|
+
return bytesToBase64Url(compressed);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function decodeSnapshot(encoded) {
|
|
97
|
+
const compressed = base64UrlToBytes(encoded);
|
|
98
|
+
const raw = await streamThrough(new DecompressionStream('deflate-raw'), compressed);
|
|
99
|
+
return JSON.parse(new TextDecoder().decode(raw));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getSharedSnapshotParam() {
|
|
103
|
+
const h = location.hash || '';
|
|
104
|
+
return h.startsWith(HASH_PREFIX) ? h.slice(HASH_PREFIX.length) : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const PUBLIC_BASE_URL = 'https://nikiforovall.blog/pi-inspect/';
|
|
108
|
+
|
|
109
|
+
function buildShareUrl(encoded) {
|
|
110
|
+
// Local pi-inspect runs on localhost — recipients can't open that. Always
|
|
111
|
+
// anchor share links on the hosted static dashboard. When the current page
|
|
112
|
+
// is already a non-localhost origin (e.g. the hosted site itself), reuse it.
|
|
113
|
+
const host = location.hostname;
|
|
114
|
+
const isLocal = !host || host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
115
|
+
const base = isLocal ? PUBLIC_BASE_URL : `${location.origin}${location.pathname}`;
|
|
116
|
+
return `${base}${HASH_PREFIX}${encoded}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
window.piShare = { encodeSnapshot, decodeSnapshot, getSharedSnapshotParam, buildShareUrl, redactSnapshot };
|
package/public/sw.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// pi-inspect service worker — network-first for dynamic data, cache-first for static shell.
|
|
2
|
-
const VERSION = 'pi-inspect-
|
|
3
|
-
|
|
2
|
+
const VERSION = 'pi-inspect-v6';
|
|
3
|
+
// Resolve relative to the SW scope so this works under any subpath (e.g. GitHub Pages).
|
|
4
|
+
const BASE = new URL('./', self.registration?.scope || self.location.href).pathname;
|
|
5
|
+
const SHELL = ['', 'index.html', 'style.css', 'app.js', 'share.js', 'manifest.webmanifest', 'icon.svg'].map((p) => BASE + p);
|
|
4
6
|
|
|
5
7
|
self.addEventListener('install', (event) => {
|
|
6
8
|
event.waitUntil(caches.open(VERSION).then((c) => c.addAll(SHELL)).catch(() => {}));
|