@pharaoh-so/mcp 0.3.53 → 0.3.56
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/.mcp.json +1 -1
- package/CHANGELOG.md +41 -0
- package/LICENSE +1 -1
- package/NOTICE +28 -0
- package/README.md +5 -5
- package/commands/plan.md +11 -11
- package/commands/review.md +2 -2
- package/dist/auth.d.ts +5 -0
- package/dist/credentials.d.ts +8 -0
- package/dist/gateway.js +1 -1
- package/dist/helpers.d.ts +15 -5
- package/dist/helpers.js +71 -2
- package/dist/index.js +51 -26
- package/dist/install-skills.d.ts +30 -5
- package/dist/install-skills.js +210 -11
- package/dist/refresh.d.ts +8 -2
- package/dist/refresh.js +33 -6
- package/dist/setup.d.ts +5 -0
- package/dist/setup.js +66 -0
- package/inspect-tools.json +89 -36
- package/{skills → npx-skills}/brainstorm/SKILL.md +2 -1
- package/{skills → npx-skills}/debug/SKILL.md +2 -1
- package/{skills → npx-skills}/pharaoh/SKILL.md +13 -6
- package/{skills → npx-skills}/plan/SKILL.md +13 -14
- package/npx-skills/refactor/SKILL.md +38 -0
- package/{skills → npx-skills}/review/SKILL.md +4 -5
- package/{skills → npx-skills}/tdd/SKILL.md +2 -1
- package/{skills → npx-skills}/verify/SKILL.md +2 -1
- package/package.json +6 -4
- package/project-memory/bundle.mjs +137 -0
- package/project-memory/execute-turn.mjs +240 -0
- package/project-memory/hook-config.mjs +91 -0
- package/project-memory/hook-events.mjs +141 -0
- package/project-memory/local.mjs +463 -0
- package/project-memory/memory-api.mjs +182 -0
- package/project-memory/pharaoh-memory-hook.mjs +33 -0
- package/project-memory/pharaoh-memory.mjs +55 -0
- package/project-memory/setup.d.mts +14 -0
- package/project-memory/setup.mjs +151 -0
- package/project-memory/user-target.mjs +221 -0
- package/trace-app.html +222 -101
- package/skills/.gitkeep +0 -0
- package/skills/refactor/SKILL.md +0 -34
package/trace-app.html
CHANGED
|
@@ -3,94 +3,122 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light dark">
|
|
6
7
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'none'; img-src 'none'; font-src 'none'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'">
|
|
7
8
|
<title>Pharaoh Trace</title>
|
|
8
9
|
<style>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
--
|
|
10
|
+
/* Host tokens first: styles.variables lands on :root. Each fallback is Claude's default, so a host
|
|
11
|
+
that sends none still gets a themed card. Gold and the slice colours are the card's own; the rest is the host's. */
|
|
12
|
+
:root { color-scheme:light dark;
|
|
13
|
+
--ink:var(--color-text-primary,light-dark(#141413,#faf9f5)); --text:var(--color-text-secondary,light-dark(#3d3d3a,#c2c0b6)); --muted:var(--color-text-tertiary,light-dark(#73726c,#9c9a92));
|
|
14
|
+
--rule:var(--color-border-tertiary,light-dark(rgba(31,30,29,.15),rgba(222,220,209,.15))); --track:var(--color-background-secondary,light-dark(#f5f4ed,#262624)); --focus:var(--color-ring-primary,light-dark(rgba(20,20,19,.7),rgba(250,249,245,.7)));
|
|
15
|
+
--red:var(--color-text-danger,light-dark(#7f2c28,#ee8884)); --amber:var(--color-text-warning,light-dark(#5a4815,#d1a041)); --green:var(--color-text-success,light-dark(#265b19,#7ab948));
|
|
16
|
+
--gold:light-dark(#86610c,#d4a843);
|
|
17
|
+
/* Slices are separate things, so they share one lightness and differ by hue. The order alternates warm and cool, which keeps neighbours apart under colour blindness; it clears red, amber and green, which mean error, warning and ok here. */
|
|
18
|
+
--s0:light-dark(#977100,#cca13e); --s1:light-dark(#7d66b9,#ad97ee); --s2:light-dark(#008d65,#4ac094); --s3:light-dark(#5374c1,#81a5f6); --s4:light-dark(#aa5688,#df86ba);
|
|
19
|
+
--s-muted:var(--color-border-primary,light-dark(rgba(31,30,29,.4),rgba(222,220,209,.4))); --s-warning:var(--color-border-warning,light-dark(#805c1f,#a87829)); --s-error:var(--color-border-danger,light-dark(#a73d39,#cd5c58));
|
|
20
|
+
--sans:var(--font-sans,system-ui,-apple-system,"Segoe UI",sans-serif); --mono:var(--font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);
|
|
21
|
+
--type-heading:var(--font-heading-md-size,16px); --type-body:var(--font-text-sm-size,14px); --type-caption:var(--font-text-xs-size,12px); --leading:var(--font-text-sm-line-height,1.4);
|
|
22
|
+
--regular:var(--font-weight-normal,400); --strong:var(--font-weight-semibold,600);
|
|
23
|
+
--line:var(--border-width-regular,.5px) solid var(--rule); --radius:var(--border-radius-md,8px); --radius-small:var(--border-radius-xs,4px); }
|
|
24
|
+
:root[data-theme="light"] { color-scheme:light; } :root[data-theme="dark"] { color-scheme:dark; }
|
|
14
25
|
* { box-sizing:border-box; }
|
|
15
26
|
html,body { margin:0; background:transparent; min-width:0; }
|
|
27
|
+
body { padding:var(--safe-top,0) var(--safe-right,0) var(--safe-bottom,0) var(--safe-left,0); }
|
|
16
28
|
.label,.detail,.badge { overflow-wrap:anywhere; min-width:0; unicode-bidi:plaintext; }
|
|
17
|
-
.mark-s { font:
|
|
18
|
-
#trace {
|
|
29
|
+
.mark-s { font:var(--strong) var(--type-caption) var(--mono); width:1ch; }
|
|
30
|
+
#trace { color:var(--text); border:var(--line); border-radius:var(--radius); padding:10px 12px; font:var(--regular) var(--type-body)/var(--leading) var(--sans); min-width:0; }
|
|
19
31
|
.head { display:flex; flex-wrap:wrap; align-items:baseline; gap:2px 10px; }
|
|
20
|
-
.mark { color:var(--gold); font-size:
|
|
32
|
+
.mark { color:var(--gold); font-size:var(--type-heading); line-height:1; }
|
|
21
33
|
.headline { flex:1 1 16em; margin:0; min-width:0; overflow-wrap:anywhere; }
|
|
22
34
|
#kind { color:var(--muted); }
|
|
23
|
-
#target { color:var(--
|
|
35
|
+
#target { color:var(--ink); font:var(--strong) var(--type-heading)/var(--leading) var(--mono); }
|
|
24
36
|
#target,.legend > span,.rows .label { unicode-bidi:isolate; }
|
|
25
|
-
#title { color:var(--
|
|
37
|
+
#title { color:var(--ink); font-size:var(--type-heading); font-weight:var(--strong); }
|
|
26
38
|
#summary { margin-left:8px; }
|
|
27
|
-
.num { color:var(--gold); font:
|
|
28
|
-
.meta { margin-left:auto; display:flex; gap:10px; white-space:nowrap; font-size:
|
|
39
|
+
.num { color:var(--gold); font-family:var(--mono); font-weight:var(--strong); }
|
|
40
|
+
.meta { margin-left:auto; display:flex; gap:10px; white-space:nowrap; font-size:var(--type-caption); }
|
|
29
41
|
#verdict[data-state="error"] { color:var(--red); } #verdict[data-state="warning"] { color:var(--amber); } #verdict[data-state="ok"] { color:var(--green); }
|
|
30
|
-
#sha { color:var(--muted); font:
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
.bar
|
|
34
|
-
.
|
|
35
|
-
.
|
|
36
|
-
.
|
|
37
|
-
.
|
|
42
|
+
#sha { color:var(--muted); font-family:var(--mono); }
|
|
43
|
+
/* Bars are text. Each cell is a tick-sized window onto a caption-size glyph, set apart from the next, so █ reads as a tick, not a rule.
|
|
44
|
+
Whole pixels: a window's edges round the same way, so every tick lands the same width on a 1x screen. */
|
|
45
|
+
.bar,.sw { --tick-width:5px; --tick-height:8px; }
|
|
46
|
+
.mini,.meter { --tick-width:4px; --tick-height:6px; }
|
|
47
|
+
.bar,.mini,.meter,.sw { font:var(--regular) var(--type-caption)/var(--tick-height) var(--mono); letter-spacing:0; font-variant-ligatures:none; -webkit-user-select:none; user-select:none; }
|
|
48
|
+
.bar,.mini,.meter { display:grid; grid-template-columns:repeat(var(--cols),var(--tick-width)); justify-content:space-between; min-width:0; }
|
|
49
|
+
.bar span,.mini span,.meter span,.sw { position:relative; width:var(--tick-width); height:var(--tick-height); overflow:hidden; }
|
|
50
|
+
.bar span,.mini span,.meter span { color:var(--s-muted); }
|
|
51
|
+
.bar span::after,.mini span::after,.meter span::after { content:attr(data-g); position:absolute; inset:0; color:var(--tone,var(--gold)); }
|
|
52
|
+
.bar { margin:8px 0 0; }
|
|
53
|
+
.s0 { --tone:var(--s0); } .s1 { --tone:var(--s1); } .s2 { --tone:var(--s2); } .s3 { --tone:var(--s3); } .s4 { --tone:var(--s4); }
|
|
54
|
+
.muted { --tone:var(--muted); } .warning { --tone:var(--s-warning); } .error { --tone:var(--s-error); }
|
|
55
|
+
.legend { display:flex; flex-wrap:wrap; gap:2px 14px; margin:6px 0 0; color:var(--muted); font-size:var(--type-caption); min-width:0; }
|
|
56
|
+
.legend:empty { display:none; }
|
|
38
57
|
#trace[data-reveal] .legend { opacity:0; }
|
|
39
|
-
.legend b { color:var(--text); font:
|
|
40
|
-
.sw { display:inline-block;
|
|
41
|
-
.
|
|
42
|
-
.
|
|
43
|
-
.
|
|
44
|
-
.
|
|
45
|
-
.
|
|
46
|
-
.
|
|
58
|
+
.legend b { color:var(--text); font:var(--strong) var(--type-caption) var(--mono); margin-left:4px; }
|
|
59
|
+
.sw { display:inline-block; color:var(--tone); margin-right:5px; }
|
|
60
|
+
.foot { display:flex; justify-content:flex-end; gap:14px; margin-top:6px; }
|
|
61
|
+
.action { display:flex; align-items:center; gap:4px; border:0; background:none; padding:2px 0; color:var(--gold); font:var(--regular) var(--type-caption) var(--sans); cursor:pointer; white-space:nowrap; }
|
|
62
|
+
.action:hover { color:var(--ink); }
|
|
63
|
+
.action:focus-visible,summary:focus-visible { outline:2px solid var(--focus); outline-offset:2px; border-radius:var(--radius-small); }
|
|
64
|
+
.chev { display:inline-block; }
|
|
65
|
+
.action[aria-expanded="true"] .chev { transform:rotate(90deg); }
|
|
66
|
+
.caveat { margin:6px 0 0; color:var(--amber); font-size:var(--type-caption); overflow-wrap:anywhere; }
|
|
47
67
|
.caveat span + span::before { content:" · "; color:var(--muted); }
|
|
48
|
-
.rows { list-style:none; margin:4px 0 0; padding:0; }
|
|
49
|
-
.rows li { display:flex; flex-wrap:wrap; align-items:baseline; gap:0 8px; padding:4px 0 5px; border-top:
|
|
68
|
+
.rows { list-style:none; margin:4px 0 0; padding:0; font-size:var(--type-caption); }
|
|
69
|
+
.rows li { display:flex; flex-wrap:wrap; align-items:baseline; gap:0 8px; padding:4px 0 5px; border-top:var(--line); min-width:0; }
|
|
50
70
|
.rows li:first-child { border-top:0; }
|
|
51
|
-
.rows .kind { color:var(--muted);
|
|
52
|
-
.rows .label { color:var(--
|
|
53
|
-
.rows .prose { color:var(--text); font:
|
|
54
|
-
.rows .detail { color:var(--text);
|
|
55
|
-
.rows .end { margin-left:auto;
|
|
56
|
-
.rows .more { color:var(--muted);
|
|
57
|
-
.rows .mini {
|
|
71
|
+
.rows .kind { color:var(--muted); }
|
|
72
|
+
.rows .label { color:var(--ink); font-family:var(--mono); }
|
|
73
|
+
.rows .prose { color:var(--text); font-family:var(--sans); }
|
|
74
|
+
.rows .detail { color:var(--text); }
|
|
75
|
+
.rows .end { margin-left:auto; white-space:nowrap; }
|
|
76
|
+
.rows .more { color:var(--muted); }
|
|
77
|
+
.rows .mini { flex:1 0 100%; margin-top:4px; }
|
|
58
78
|
[data-state="ok"] > .mark-s { color:var(--green); } [data-state="warning"] > .mark-s, [data-state="pending"] > .mark-s { color:var(--amber); } [data-state="error"] > .mark-s, .end[data-state="error"] { color:var(--red); } .end[data-state="warning"] { color:var(--amber); }
|
|
59
79
|
.drawer { display:grid; grid-template-rows:0fr; }
|
|
60
80
|
.drawer[data-open] { grid-template-rows:1fr; }
|
|
61
81
|
.drawer-inner { overflow:hidden; min-height:0; }
|
|
62
|
-
.drawer[data-open] .drawer-inner { border-top:
|
|
82
|
+
.drawer[data-open] .drawer-inner { border-top:var(--line); margin-top:8px; padding-top:4px; }
|
|
63
83
|
ul.tree,.tree ul { list-style:none; margin:0; padding:0; }
|
|
64
|
-
.tree ul { margin:0 0 4px 6px; padding-left:10px; border-left:
|
|
65
|
-
.tree summary,.leaf { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:10px; align-items:baseline; padding:3px 0; font-size:
|
|
84
|
+
.tree ul { margin:0 0 4px 6px; padding-left:10px; border-left:var(--line); }
|
|
85
|
+
.tree summary,.leaf { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:10px; align-items:baseline; padding:3px 0; font-size:var(--type-caption); min-width:0; }
|
|
66
86
|
.tree summary { list-style:none; cursor:pointer; color:var(--text); }
|
|
67
87
|
.tree summary::-webkit-details-marker { display:none; }
|
|
68
|
-
.tree summary:hover { background:var(--
|
|
69
|
-
.tree summary .label::before { content:"▸"; display:inline-block; color:var(--gold);
|
|
88
|
+
.tree summary:hover { background:var(--track); }
|
|
89
|
+
.tree summary .label::before { content:"▸"; display:inline-block; color:var(--gold); width:1em; }
|
|
70
90
|
.tree details[open] > summary .label::before { transform:rotate(90deg); }
|
|
71
91
|
.tree details[open] > summary .label { color:var(--gold); }
|
|
72
|
-
.leaf .label { color:var(--
|
|
73
|
-
.tree > li > .leaf { padding-left:
|
|
74
|
-
.tree > li > .leaf .label { color:var(--text); font:
|
|
75
|
-
.tree .label .detail { color:var(--muted); font:
|
|
76
|
-
.tree .label .caveat { color:var(--amber); font:
|
|
77
|
-
.badge { color:var(--muted); font:
|
|
92
|
+
.leaf .label { color:var(--ink); font-family:var(--mono); }
|
|
93
|
+
.tree > li > .leaf { padding-left:1em; }
|
|
94
|
+
.tree > li > .leaf .label { color:var(--text); font-family:var(--sans); }
|
|
95
|
+
.tree .label .detail { color:var(--muted); font-family:var(--mono); margin-left:8px; }
|
|
96
|
+
.tree .label .caveat { color:var(--amber); font-family:var(--sans); margin:0 0 0 8px; }
|
|
97
|
+
.badge { color:var(--muted); font-family:var(--mono); text-align:right; white-space:nowrap; }
|
|
78
98
|
.badge[data-state="ok"] { color:var(--green); } .badge[data-state="warning"],.badge[data-state="pending"] { color:var(--amber); } .badge[data-state="error"] { color:var(--red); }
|
|
79
|
-
.meter {
|
|
80
|
-
.
|
|
81
|
-
.notes,.provenance { color:var(--muted); font-size:11.5px; margin:6px 0 2px; }
|
|
99
|
+
.meter { margin-top:4px; max-width:190px; }
|
|
100
|
+
.notes,.provenance { color:var(--muted); font-size:var(--type-caption); margin:6px 0 2px; }
|
|
82
101
|
.notes p { margin:2px 0; overflow-wrap:anywhere; white-space:pre-wrap; }
|
|
83
|
-
#fallback { font:
|
|
102
|
+
#fallback { font:var(--regular) var(--type-caption)/1.5 var(--mono); color:var(--text); margin:6px 0 0; white-space:pre-wrap; overflow-wrap:anywhere; unicode-bidi:plaintext; }
|
|
84
103
|
@media(max-width:400px) { #trace { padding:8px 10px; } #summary { margin-left:0; } #summary:not([hidden]) { display:block; } .tree summary,.leaf { grid-template-columns:minmax(0,1fr); gap:0; } .badge { text-align:left; } }
|
|
104
|
+
/* A finger needs 44pt: the MCP Apps minimum for anything tappable. */
|
|
105
|
+
@media(pointer:coarse) { .action { min-height:44px; min-width:44px; justify-content:center; } .tree summary { min-height:44px; align-items:center; } .foot { gap:8px; } }
|
|
106
|
+
/* Inline cards can't scroll, so full screen is where long Details go: the page scrolls and they stay open. */
|
|
107
|
+
:root[data-display="fullscreen"],:root[data-display="fullscreen"] body { height:100%; }
|
|
108
|
+
:root[data-display="fullscreen"] body { overflow-y:auto; }
|
|
109
|
+
:root[data-display="fullscreen"] #trace { border:0; border-radius:0; }
|
|
110
|
+
/* The frame is short for a moment before a taller result is sized: a scrollbar then would shrink the card and redraw its bars mid-arrival. */
|
|
111
|
+
:root:not([data-display="fullscreen"]) { scrollbar-width:none; }
|
|
85
112
|
/* Motion only for people who have not asked to reduce it. */
|
|
86
113
|
@media(prefers-reduced-motion:no-preference) {
|
|
87
|
-
.bar
|
|
114
|
+
#trace:not([data-settled]) .bar span::after,#trace:not([data-settled]) .mini span::after { animation:tick 1ms calc(var(--i) * .7s / var(--cols)) backwards; }
|
|
115
|
+
@keyframes tick { from { opacity:0; } }
|
|
88
116
|
.legend { transition:opacity .4s ease .2s; }
|
|
89
117
|
.chev { transition:transform .3s ease; }
|
|
90
118
|
.drawer { transition:grid-template-rows .3s ease; }
|
|
91
119
|
.tree summary .label::before { transition:transform .2s ease; }
|
|
92
|
-
#trace[data-state="pending"] .bar::after,#trace[data-state="waiting"] .bar::after { content:"";
|
|
93
|
-
@keyframes
|
|
120
|
+
#trace[data-state="pending"] .bar span::after,#trace[data-state="waiting"] .bar span::after { content:"█"; color:var(--gold); opacity:0; animation:walk 1.5s linear calc(var(--i) * 1.5s / var(--cols)) infinite; }
|
|
121
|
+
@keyframes walk { from { opacity:1; } 16%,to { opacity:0; } }
|
|
94
122
|
}
|
|
95
123
|
[hidden] { display:none; }
|
|
96
124
|
</style>
|
|
@@ -100,15 +128,16 @@
|
|
|
100
128
|
<div class="head">
|
|
101
129
|
<span class="mark" aria-hidden="true">☥</span>
|
|
102
130
|
<p class="headline"><span id="kind"></span><span id="target"></span><span id="title">Pharaoh</span><span id="summary" role="status" aria-live="polite">Waiting for the tool</span></p>
|
|
103
|
-
<span
|
|
131
|
+
<span class="meta"><span id="verdict" hidden></span><span id="sha" hidden></span></span>
|
|
104
132
|
</div>
|
|
105
133
|
<div id="bar" class="bar" aria-hidden="true"></div>
|
|
106
|
-
<
|
|
107
|
-
<p id="legend" class="legend"></p>
|
|
108
|
-
<button id="toggle" class="toggle" type="button" aria-expanded="false" aria-controls="drawer" hidden><span id="toggle-label">Details</span><span class="chev" aria-hidden="true">▸</span></button>
|
|
109
|
-
</div>
|
|
134
|
+
<p id="legend" class="legend"></p>
|
|
110
135
|
<p id="caveat" class="caveat" hidden></p>
|
|
111
136
|
<ul id="rows" class="rows" hidden></ul>
|
|
137
|
+
<div id="foot" class="foot" hidden>
|
|
138
|
+
<button id="toggle" class="action" type="button" aria-expanded="false" aria-controls="drawer" hidden><span id="toggle-label">Details</span><span class="chev" aria-hidden="true">▸</span></button>
|
|
139
|
+
<button id="expand" class="action" type="button" hidden></button>
|
|
140
|
+
</div>
|
|
112
141
|
<div id="drawer" class="drawer" inert>
|
|
113
142
|
<div class="drawer-inner">
|
|
114
143
|
<ul id="tree" class="tree" aria-label="Result evidence"></ul>
|
|
@@ -127,14 +156,28 @@
|
|
|
127
156
|
const LIMITS = { bytes:65536, nodes:120, depth:6, label:512, detail:2048, badge:128, summary:1024, notes:16, note:1024, repo:256, segments:8 };
|
|
128
157
|
const MAX_TEXT = 32768;
|
|
129
158
|
const MAX_ROWS = 5;
|
|
159
|
+
// Px from one tick to the next, its width plus the gap: a wider card gets more ticks, not fatter ones.
|
|
160
|
+
const TICK_PITCH = {bar:7,mini:6};
|
|
161
|
+
const TICK_GAP = 2;
|
|
162
|
+
// Lays bars out while the frame has no width yet; the first resize refits them.
|
|
163
|
+
const FALLBACK_WIDTH = 340;
|
|
164
|
+
const METER_CELLS = 32;
|
|
165
|
+
const BLOCKS = ["","▏","▎","▍","▌","▋","▊","▉","█"];
|
|
130
166
|
const TONES = ["warning","error","muted"];
|
|
131
167
|
const STATES = ["ok","pending","warning","error"];
|
|
168
|
+
const DISPLAY_MODES = ["inline","fullscreen"];
|
|
169
|
+
// The tokens hosts send, and only those: a host never overwrites the card's own names (--gold, --rule).
|
|
170
|
+
const HOST_VARIABLE = /^--(color|font|border|shadow)-[a-z0-9-]+$/;
|
|
171
|
+
const MAX_HOST_VALUE = 512;
|
|
132
172
|
// What each tool is doing while it runs, shown with the argument it targets.
|
|
133
|
-
const VERBS = { get_blast_radius:"Tracing callers of", get_unused_code:"Checking reachability in", search_functions:"Searching for", get_codebase_map:"Mapping", get_module_context:"Reading module", pharaoh_recon:"Running recon on", pharaoh_account:"Checking account" };
|
|
173
|
+
const VERBS = { get_blast_radius:"Tracing callers of", get_unused_code:"Checking reachability in", search_functions:"Searching for", get_codebase_map:"Mapping", get_module_context:"Reading module", pharaoh_recon:"Running recon on", pharaoh_account:"Checking account", pharaoh_manage_repo:"Updating" };
|
|
134
174
|
const root = document.getElementById("trace");
|
|
135
|
-
const
|
|
175
|
+
const head = root.querySelector(".head");
|
|
176
|
+
const page = document.documentElement;
|
|
177
|
+
const fields = Object.fromEntries(["kind","target","title","summary","verdict","sha","bar","caveat","rows","foot","legend","toggle","toggle-label","expand","drawer","tree","fallback","notes","provenance"].map(id => [id,document.getElementById(id)]));
|
|
136
178
|
const compact = new Intl.NumberFormat("en",{notation:"compact",maximumFractionDigits:1});
|
|
137
179
|
const initId = "pharaoh-trace-initialize";
|
|
180
|
+
const displayId = "pharaoh-trace-display-";
|
|
138
181
|
let tool = "";
|
|
139
182
|
let ready = false;
|
|
140
183
|
let closed = false;
|
|
@@ -142,6 +185,11 @@
|
|
|
142
185
|
let resizeFrame = 0;
|
|
143
186
|
let lastHeight = 0;
|
|
144
187
|
let lastWidth = 0;
|
|
188
|
+
let bars = [];
|
|
189
|
+
let hasDetails = false;
|
|
190
|
+
let displayMode = "inline";
|
|
191
|
+
let hostModes = [];
|
|
192
|
+
let displayRequests = 0;
|
|
145
193
|
const record = value => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
146
194
|
const string = (value,max) => typeof value === "string" && value.length <= max;
|
|
147
195
|
const send = message => { if (!closed) window.parent.postMessage({jsonrpc:"2.0",...message},"*"); };
|
|
@@ -172,10 +220,13 @@
|
|
|
172
220
|
rich(fields.summary,summary);
|
|
173
221
|
fields.summary.hidden = !summary;
|
|
174
222
|
}
|
|
175
|
-
//
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
fields.
|
|
223
|
+
// Details and full screen sit at the foot of the card; full screen only where the host offers it.
|
|
224
|
+
function showActions() {
|
|
225
|
+
const full = displayMode === "fullscreen";
|
|
226
|
+
fields.toggle.hidden = !hasDetails || full;
|
|
227
|
+
fields.expand.hidden = !(hasDetails || full) || !hostModes.includes(full ? "inline" : "fullscreen");
|
|
228
|
+
fields.expand.textContent = full ? "Exit full screen" : "Full screen";
|
|
229
|
+
fields.foot.hidden = fields.toggle.hidden && fields.expand.hidden;
|
|
179
230
|
}
|
|
180
231
|
function setOpen(open) {
|
|
181
232
|
fields.drawer.toggleAttribute("data-open",open);
|
|
@@ -183,16 +234,26 @@
|
|
|
183
234
|
fields.toggle.setAttribute("aria-expanded",String(open));
|
|
184
235
|
fields["toggle-label"].textContent = open ? "Hide" : "Details";
|
|
185
236
|
}
|
|
237
|
+
function setDisplay(mode) {
|
|
238
|
+
// Hosts repeat the mode in context updates, and Details stay as the user left them.
|
|
239
|
+
if (mode !== displayMode) setOpen(mode === "fullscreen");
|
|
240
|
+
displayMode = mode;
|
|
241
|
+
page.dataset.display = mode;
|
|
242
|
+
showActions();
|
|
243
|
+
}
|
|
186
244
|
function clearResult() {
|
|
187
|
-
setOpen(
|
|
245
|
+
setOpen(displayMode === "fullscreen");
|
|
188
246
|
for (const field of ["bar","rows","legend","tree","notes"]) fields[field].replaceChildren();
|
|
189
247
|
for (const field of ["verdict","sha","caveat","provenance","fallback"]) display(field,"");
|
|
190
248
|
delete fields.verdict.dataset.state;
|
|
191
249
|
fields.sha.title = "";
|
|
192
250
|
fields.bar.hidden = true;
|
|
251
|
+
bars = [];
|
|
252
|
+
delete root.dataset.settled;
|
|
193
253
|
fields.rows.hidden = true;
|
|
194
254
|
fields.notes.hidden = true;
|
|
195
|
-
|
|
255
|
+
hasDetails = false;
|
|
256
|
+
showActions();
|
|
196
257
|
}
|
|
197
258
|
function notes(items) {
|
|
198
259
|
for (const text of items) fields.notes.append(element("p","",text));
|
|
@@ -213,8 +274,8 @@
|
|
|
213
274
|
display("verdict",state === "error" ? "Failed" : "Cancelled");
|
|
214
275
|
}
|
|
215
276
|
if (notice) notes([notice]);
|
|
216
|
-
|
|
217
|
-
|
|
277
|
+
hasDetails = Boolean(text);
|
|
278
|
+
showActions();
|
|
218
279
|
}
|
|
219
280
|
function readableText(result) {
|
|
220
281
|
if (!record(result) || !Array.isArray(result.content)) return {text:"No readable text was returned. Read the tool response in chat.",omitted:false};
|
|
@@ -264,30 +325,70 @@
|
|
|
264
325
|
return view.nodes.every(node => visit(node,1));
|
|
265
326
|
} catch { return false; }
|
|
266
327
|
}
|
|
267
|
-
//
|
|
268
|
-
function
|
|
269
|
-
const total =
|
|
270
|
-
|
|
328
|
+
// Largest remainder: cells sum to exactly cols, and every slice keeps at least one.
|
|
329
|
+
function allocate(values,cols) {
|
|
330
|
+
const total = values.reduce((sum,value) => sum + value,0);
|
|
331
|
+
const exact = values.map(value => value / total * cols);
|
|
332
|
+
const cells = exact.map(share => Math.max(1,Math.floor(share)));
|
|
333
|
+
const spare = cols - cells.reduce((sum,count) => sum + count,0);
|
|
334
|
+
const shortest = exact.map((share,index) => [share - cells[index],index]).sort((a,b) => b[0] - a[0]);
|
|
335
|
+
for (const [,index] of shortest.slice(0,Math.max(0,spare))) cells[index]++;
|
|
336
|
+
for (let over = -spare; over > 0; over--) cells[cells.indexOf(Math.max(...cells))]--;
|
|
337
|
+
return cells;
|
|
338
|
+
}
|
|
339
|
+
// Every cell is a ░ with its real glyph laid over it, so a bar fills in over its own track.
|
|
340
|
+
function cell(target,tone,glyph) {
|
|
341
|
+
const node = element("span",tone,"░");
|
|
342
|
+
if (glyph) node.dataset.g = glyph;
|
|
343
|
+
node.style.setProperty("--i",target.childElementCount);
|
|
344
|
+
target.append(node);
|
|
345
|
+
}
|
|
346
|
+
const blockFor = item => item.tone === "muted" ? "░" : "█";
|
|
347
|
+
const colsFor = ({target,shown},width) => Math.max(shown.length * 2 - 1,Math.floor(((width || FALLBACK_WIDTH) + TICK_GAP) / TICK_PITCH[target.className]));
|
|
348
|
+
// One empty cell separates slices: ticks have no edge of their own to tell two golds apart.
|
|
349
|
+
function drawBar(bar,width) {
|
|
350
|
+
const {target,shown} = bar;
|
|
351
|
+
bar.cols = colsFor(bar,width);
|
|
352
|
+
const counts = allocate(shown.map(item => item.value),bar.cols - shown.length + 1);
|
|
353
|
+
target.style.setProperty("--cols",bar.cols);
|
|
354
|
+
target.replaceChildren();
|
|
271
355
|
let shade = 0;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
target.append(slice);
|
|
277
|
-
return slice;
|
|
356
|
+
shown.forEach((item,position) => {
|
|
357
|
+
if (position) target.append(element("span"));
|
|
358
|
+
const tone = item.tone || `s${Math.min(shade++,4)}`;
|
|
359
|
+
for (let count = counts[position]; count > 0; count--) cell(target,tone,blockFor(item));
|
|
278
360
|
});
|
|
361
|
+
while (target.childElementCount < bar.cols) cell(target);
|
|
362
|
+
}
|
|
363
|
+
function addBar(target,items) {
|
|
364
|
+
const bar = {target,shown:items.filter(item => item.value > 0)};
|
|
365
|
+
bars.push(bar);
|
|
366
|
+
drawBar(bar,head.clientWidth);
|
|
279
367
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
368
|
+
// A new column count redraws a bar, without the arrival motion. Smaller changes are the cell spacing's to absorb, so running motion is left alone.
|
|
369
|
+
function refit() {
|
|
370
|
+
const width = head.clientWidth;
|
|
371
|
+
const stale = bars.filter(bar => colsFor(bar,width) !== bar.cols);
|
|
372
|
+
if (!stale.length) return;
|
|
373
|
+
root.dataset.settled = "";
|
|
374
|
+
for (const bar of stale) drawBar(bar,width);
|
|
375
|
+
}
|
|
376
|
+
// The empty bar shown until a result arrives.
|
|
377
|
+
function showTrack() {
|
|
378
|
+
addBar(fields.bar,[]);
|
|
379
|
+
fields.bar.hidden = false;
|
|
380
|
+
}
|
|
381
|
+
// The bars fill by themselves in CSS; the legend fades in once they have painted.
|
|
382
|
+
function reveal() {
|
|
383
|
+
requestAnimationFrame(() => requestAnimationFrame(() => { delete root.dataset.reveal; }));
|
|
285
384
|
}
|
|
286
385
|
function legend(items) {
|
|
287
386
|
let shade = 0;
|
|
288
387
|
for (const item of items.filter(item => item.value > 0)) {
|
|
289
388
|
const entry = element("span");
|
|
290
|
-
|
|
389
|
+
const swatch = element("span",`sw ${item.tone || `s${Math.min(shade++,4)}`}`,blockFor(item));
|
|
390
|
+
swatch.setAttribute("aria-hidden","true");
|
|
391
|
+
entry.append(swatch,item.label,element("b","",compact.format(item.value)));
|
|
291
392
|
fields.legend.append(entry);
|
|
292
393
|
}
|
|
293
394
|
}
|
|
@@ -298,7 +399,7 @@
|
|
|
298
399
|
target.hidden = false;
|
|
299
400
|
}
|
|
300
401
|
// Sections (recon) and status lines (states, named checks) stay visible; groups open on demand.
|
|
301
|
-
function rowFor(node
|
|
402
|
+
function rowFor(node) {
|
|
302
403
|
const item = element("li");
|
|
303
404
|
if (node.status) {
|
|
304
405
|
item.dataset.state = node.status;
|
|
@@ -317,7 +418,7 @@
|
|
|
317
418
|
if (node.glance?.segments?.length) {
|
|
318
419
|
const mini = element("span","mini");
|
|
319
420
|
mini.setAttribute("aria-hidden","true");
|
|
320
|
-
|
|
421
|
+
addBar(mini,node.glance.segments);
|
|
321
422
|
item.append(mini);
|
|
322
423
|
}
|
|
323
424
|
return item;
|
|
@@ -331,9 +432,11 @@
|
|
|
331
432
|
if (node.meter) {
|
|
332
433
|
const meter = element("span","meter");
|
|
333
434
|
meter.title = `${node.meter.value} / ${node.meter.total}`;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
435
|
+
meter.setAttribute("aria-hidden","true");
|
|
436
|
+
meter.style.setProperty("--cols",METER_CELLS);
|
|
437
|
+
// Eighths of a cell, and never none: a tiny value still shows. The window clips the top three to look full.
|
|
438
|
+
const eighths = Math.max(1,Math.round(node.meter.value / node.meter.total * METER_CELLS * 8));
|
|
439
|
+
for (let index = 0; index < METER_CELLS; index++) cell(meter,"",BLOCKS[Math.min(8,Math.max(0,eighths - index * 8))]);
|
|
337
440
|
label.append(meter);
|
|
338
441
|
}
|
|
339
442
|
if (node.glance?.caveat) label.append(element("span","caveat",node.glance.caveat));
|
|
@@ -365,24 +468,25 @@
|
|
|
365
468
|
}
|
|
366
469
|
if (freshness) display("provenance",[freshness.mappedCommit ? `Mapped ${freshness.mappedCommit}` : "Mapped commit unknown",freshness.lastAnalyzed ? `as of ${freshness.lastAnalyzed}` : "map time unknown",`status: ${freshness.status}`].join(" · "));
|
|
367
470
|
caveats([glance.caveat,freshness?.status === "stale" ? "Map is behind the latest push" : ""].filter(Boolean));
|
|
368
|
-
const
|
|
369
|
-
fields.bar.
|
|
471
|
+
const shown = glance.segments?.some(item => item.value > 0);
|
|
472
|
+
if (shown) addBar(fields.bar,glance.segments);
|
|
473
|
+
fields.bar.hidden = !shown;
|
|
370
474
|
legend(glance.segments || []);
|
|
371
475
|
// Up to MAX_ROWS rows, warnings and verdicts first, shown in their original order.
|
|
372
476
|
const candidates = view.nodes.filter(node => node.glance || node.status);
|
|
373
477
|
const urgent = node => node.status === "error" || node.status === "warning" || node.glance?.verdict || node.glance?.caveat;
|
|
374
478
|
const chosen = new Set([...new Set([...candidates.filter(urgent),...candidates])].slice(0,MAX_ROWS));
|
|
375
479
|
const visible = candidates.filter(node => chosen.has(node));
|
|
376
|
-
for (const node of visible) fields.rows.append(rowFor(node
|
|
480
|
+
for (const node of visible) fields.rows.append(rowFor(node));
|
|
377
481
|
if (candidates.length > visible.length) fields.rows.append(element("li","more",`+${candidates.length - visible.length} more in Details`));
|
|
378
482
|
fields.rows.hidden = visible.length === 0;
|
|
379
483
|
for (const node of view.nodes) if (!visible.includes(node) || node.children?.length) fields.tree.append(renderNode(node));
|
|
380
484
|
// One click on Details shows evidence: the first group is already open inside the drawer.
|
|
381
485
|
fields.tree.querySelector("details")?.setAttribute("open","");
|
|
382
486
|
notes(view.notes || []);
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
487
|
+
hasDetails = fields.tree.childElementCount > 0 || !fields.notes.hidden || !fields.provenance.hidden;
|
|
488
|
+
showActions();
|
|
489
|
+
reveal();
|
|
386
490
|
}
|
|
387
491
|
function input(params) {
|
|
388
492
|
cancelled = false;
|
|
@@ -394,7 +498,7 @@
|
|
|
394
498
|
headline("","");
|
|
395
499
|
display("kind",target ? `${verb} ` : `${verb}…`);
|
|
396
500
|
display("target",target ? `${target}…` : "");
|
|
397
|
-
|
|
501
|
+
showTrack();
|
|
398
502
|
}
|
|
399
503
|
function result(params) {
|
|
400
504
|
if (cancelled) return;
|
|
@@ -409,15 +513,24 @@
|
|
|
409
513
|
else fallback("Tool response",text.text,[view === undefined ? "" : "Trace metadata could not be displayed; showing the tool's text response.",omission].filter(Boolean).join(" "));
|
|
410
514
|
}
|
|
411
515
|
function context(params) {
|
|
412
|
-
|
|
516
|
+
// Updates carry only the fields that changed.
|
|
517
|
+
if (params.theme === "dark" || params.theme === "light") page.dataset.theme = params.theme;
|
|
518
|
+
const variables = params.styles?.variables;
|
|
519
|
+
if (record(variables)) for (const [name,value] of Object.entries(variables)) if (HOST_VARIABLE.test(name) && string(value,MAX_HOST_VALUE)) page.style.setProperty(name,value);
|
|
520
|
+
const insets = params.safeAreaInsets;
|
|
521
|
+
if (record(insets)) for (const side of ["top","right","bottom","left"]) if (Number.isFinite(insets[side]) && insets[side] >= 0) page.style.setProperty(`--safe-${side}`,`${insets[side]}px`);
|
|
522
|
+
if (Array.isArray(params.availableDisplayModes)) hostModes = params.availableDisplayModes;
|
|
523
|
+
if (DISPLAY_MODES.includes(params.displayMode)) setDisplay(params.displayMode);
|
|
413
524
|
const name = params.toolInfo?.tool?.name;
|
|
414
525
|
if (string(name,128)) tool = name;
|
|
526
|
+
showActions();
|
|
415
527
|
}
|
|
416
528
|
function size() {
|
|
417
529
|
if (!ready || closed || resizeFrame) return;
|
|
418
530
|
resizeFrame = requestAnimationFrame(() => {
|
|
419
531
|
resizeFrame = 0;
|
|
420
|
-
|
|
532
|
+
refit();
|
|
533
|
+
const height = Math.ceil(document.body.getBoundingClientRect().height);
|
|
421
534
|
const width = Math.ceil(document.documentElement.clientWidth);
|
|
422
535
|
if (height !== lastHeight || width !== lastWidth) {
|
|
423
536
|
lastHeight = height;
|
|
@@ -432,6 +545,7 @@
|
|
|
432
545
|
root.addEventListener("transitionend",size);
|
|
433
546
|
window.addEventListener("resize",size);
|
|
434
547
|
fields.toggle.addEventListener("click",() => { setOpen(fields.toggle.getAttribute("aria-expanded") !== "true"); size(); });
|
|
548
|
+
fields.expand.addEventListener("click",() => send({id:`${displayId}${++displayRequests}`,method:"ui/request-display-mode",params:{mode:displayMode === "fullscreen" ? "inline" : "fullscreen"}}));
|
|
435
549
|
function receive(event) {
|
|
436
550
|
if (closed || event.source !== window.parent || !record(event.data) || event.data.jsonrpc !== "2.0") return;
|
|
437
551
|
const message = event.data;
|
|
@@ -449,6 +563,12 @@
|
|
|
449
563
|
size();
|
|
450
564
|
return;
|
|
451
565
|
}
|
|
566
|
+
// The host answers with the mode it actually set, which may not be the one requested.
|
|
567
|
+
if (typeof message.id === "string" && message.id.startsWith(displayId) && message.method === undefined) {
|
|
568
|
+
if (DISPLAY_MODES.includes(message.result?.mode)) setDisplay(message.result.mode);
|
|
569
|
+
size();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
452
572
|
const request = typeof message.id === "string" || typeof message.id === "number";
|
|
453
573
|
if (request) {
|
|
454
574
|
if (message.method === "ping") send({id:message.id,result:{}});
|
|
@@ -473,10 +593,11 @@
|
|
|
473
593
|
size();
|
|
474
594
|
}
|
|
475
595
|
window.addEventListener("message",receive);
|
|
596
|
+
showTrack();
|
|
476
597
|
const initTimeout = setTimeout(() => {
|
|
477
598
|
if (!ready && !closed) fallback("Text response available","Read the tool response in chat.","Trace has not received a host connection.");
|
|
478
599
|
},10000);
|
|
479
|
-
send({id:initId,method:"ui/initialize",params:{appInfo:{name:"Pharaoh Trace",version:"1.
|
|
600
|
+
send({id:initId,method:"ui/initialize",params:{appInfo:{name:"Pharaoh Trace",version:"1.4.0"},appCapabilities:{availableDisplayModes:DISPLAY_MODES},protocolVersion:PROTOCOL_VERSION}});
|
|
480
601
|
})();
|
|
481
602
|
</script>
|
|
482
603
|
</body>
|
package/skills/.gitkeep
DELETED
|
File without changes
|
package/skills/refactor/SKILL.md
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: refactor
|
|
3
|
-
prompt-name: safe-refactor
|
|
4
|
-
description: "Safe refactoring workflow using Pharaoh codebase knowledge graph. Six-step process: module context, blast radius of downstream callers, reachability verification, dependency mapping, naming conflict detection, and test coverage assessment. Produces a refactoring plan with every caller listed, test files identified, unreachable code flagged, and high-risk paths warned."
|
|
5
|
-
version: 0.2.0
|
|
6
|
-
homepage: https://pharaoh.so
|
|
7
|
-
user-invocable: true
|
|
8
|
-
metadata: {"emoji": "☥", "tags": ["refactoring", "blast-radius", "architecture", "pharaoh", "safe-refactor", "test-coverage"]}
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Safe Refactor
|
|
12
|
-
|
|
13
|
-
Step-by-step workflow to safely refactor a function or module with full blast radius awareness. Uses `safe-refactor` — a 6-step process that maps every caller, identifies affected tests, flags unreachable code, and warns about high-risk downstream paths before a single line changes.
|
|
14
|
-
|
|
15
|
-
## When to Use
|
|
16
|
-
|
|
17
|
-
Invoke before refactoring any function, module, or file — especially shared utilities, exports used across multiple modules, or anything with an unclear caller graph. Use it whenever the blast radius of a change is unknown.
|
|
18
|
-
|
|
19
|
-
## Workflow
|
|
20
|
-
|
|
21
|
-
1. Call `get_module_context` for the module containing the target to understand its current structure.
|
|
22
|
-
2. Call `get_blast_radius` for the target to identify all downstream callers and affected modules.
|
|
23
|
-
3. Call `search_functions` to check if the refactored version's name already exists elsewhere.
|
|
24
|
-
|
|
25
|
-
Do not propose a refactoring plan until all 3 steps are complete.
|
|
26
|
-
|
|
27
|
-
## Output
|
|
28
|
-
|
|
29
|
-
A refactoring plan containing:
|
|
30
|
-
- **Callers to update:** every function and file that calls the target, with update requirements
|
|
31
|
-
- **Tests to change:** test files covering the refactored code, with required modifications
|
|
32
|
-
- **Dead code:** unreachable code paths that can be deleted during the refactor
|
|
33
|
-
- **High-risk paths:** downstream modules with wide blast radius or high complexity scores
|
|
34
|
-
- **Naming conflicts:** existing functions whose names would conflict with the refactored version
|