heyspex 0.2.3 → 0.2.5
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/.claude-plugin/marketplace.json +2 -1
- package/README.md +1 -0
- package/bin/hs.mjs +2 -2
- package/dist/cli-templates/harness-commands/claude/hs-log.md +1 -1
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +1 -1
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +18 -18
- package/docs/copilot-plugin-install.md +11 -0
- package/package.json +257 -255
- package/plugins/heyspex/.claude-plugin/plugin.json +13 -12
- package/plugins/heyspex/.cursor-plugin/plugin.json +16 -15
- package/plugins/heyspex/README.md +1 -1
- package/plugins/heyspex/assets/heyspex-glyph-128.png +0 -0
- package/plugins/heyspex/assets/heyspex-glyph-256.png +0 -0
- package/plugins/heyspex/assets/heyspex-glyph.svg +12 -0
- package/scripts/claude-statusline/statusline-command.mjs +24 -9
- package/scripts/copilot-statusline.mjs +130 -0
- package/scripts/install-copilot-hooks.mjs +89 -3
- package/vendor/heyspex-mcp/http-transport.mjs +6 -1
- package/vendor/heyspex-mcp/server-identity.mjs +13 -0
- package/vendor/heyspex-mcp/tool-dispatch.mjs +4 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512" role="img" aria-label="HeySpex">
|
|
2
|
+
<title>HeySpex</title>
|
|
3
|
+
<rect width="512" height="512" rx="112" fill="#1c1c1c"/>
|
|
4
|
+
<g fill="#00fa8e" transform="translate(97.5 78) scale(0.62)">
|
|
5
|
+
<g>
|
|
6
|
+
<path d="M492.09,442.88c-65.92,38.17-133.04,85.08-200.61,119.86-22.67,11.67-37.56,16.13-62.07,4.5-61.39-39.16-127.24-71.73-188.76-110.57-18.29-11.55-32.09-22.94-34.82-46.35-1.61-13.78-2.31-66.54.21-78.91,4.31-21.18,37.08-24.95,45.67-5.85,7.53,16.75.59,54.94,2.95,74.84.62,5.21,2.18,7.48,5.88,11.03l184.72,109.54c13.76,9.11,26.71-2.74,38.87-9.54,51.46-28.76,113.11-63.02,161.72-95.32,15-9.97,20.67-18.96,10.78-36.34-28.71-21.71-67.83-35.99-96.42-56.61-23.64-17.06-12.01-47.51,14.98-45.36,1.83.15,8.91,2.25,10.53,3.14,33.32,18.24,63.31,34.57,93.3,55.33,37.69,26.08,43.46,72.24,13.08,106.64Z"/>
|
|
7
|
+
<path d="M243.69.85c12.8-1.85,26.6-.86,38.25,4.84,62.2,41.11,135.65,72.85,197.08,114.09,17.62,11.83,30.42,26.6,32.33,48.84.94,10.91,1.45,52.76-1.36,61.33-5.34,16.31-28.33,21.56-40.21,8.99-14.65-15.5,2.93-59.4-11.75-74.5l-191.41-111.29c-13.78-6.07-14.69-3.43-27.04,2.78-61.47,30.91-118.4,69.68-179.46,102.42-2.08,1.12-6.61,6.79-7.92,8.75-4.63,6.91-5.05,15.42-1.49,23.04,1.11,2.38,6.69,10.57,16.31,18.03,9.62,7.47,79.88,43.77,79.88,43.77,0,0,8.04,5.6,11.22,9.33,32.8,38.48,78.67,72.23,109.62,110.4,9.39,11.58,16.25,24.24,3.26,36.99-4.39,4.31-19.18,8.74-26.16,3.21-52.15-41.35-104.86-107.39-115.17-115.21-34.22-25.96-113.6-51.84-126.21-93.64-8.65-28.69-1.2-62.67,24.3-79.87C92.85,88.7,155.73,43.65,220.97,10.27c7.03-3.6,14.88-8.28,22.72-9.42Z"/>
|
|
8
|
+
<path d="M288.09,263.38l63.24-63.56c17.83-21.25-3.94-49.23-28.94-35.71-17.77,9.61-44.6,47.97-63.02,62.12-.73.56-15.17,15.68-26.6,26.29-6.28,5.83-8.03,13.22-1.96,19.27,3.26,3.25,7.75,7.71,11.01,10.96,6.72,6.68,18.09,6.17,25.39.12,8.5-7.04,16.82-16.05,20.89-19.49Z"/>
|
|
9
|
+
<path d="M273.49,316.01c-37.73-38.63-74.58-78.18-113-116.16-14.21-20.09-.54-43.95,24.53-38.05,17.67,4.16,54.53,53.37,70.29,66.79,2.42,2.43,32.78,34.79,32.78,34.79l86.21,87.95c17.85,24.27-9.57,53.48-38.08,29.63-19.24-16.1-44.11-45.89-62.72-64.94Z"/>
|
|
10
|
+
</g>
|
|
11
|
+
</g>
|
|
12
|
+
</svg>
|
|
@@ -353,13 +353,23 @@ async function fetchPinnedSpecId(base) {
|
|
|
353
353
|
// running this returns null and the segment is simply dropped. A short-TTL file
|
|
354
354
|
// cache (see header) collapses the per-message / per-refresh burst of statusline
|
|
355
355
|
// invocations into at most one backend call per TTL window.
|
|
356
|
-
async function readPinnedSpecId(root) {
|
|
356
|
+
async function readPinnedSpecId(root, opts = {}) {
|
|
357
357
|
const base = consoleUrl(root);
|
|
358
358
|
if (!base) return null;
|
|
359
359
|
|
|
360
360
|
const cacheFile = focusCacheFile(root);
|
|
361
361
|
const cached = readFocusCache(cacheFile);
|
|
362
362
|
if (cached) return cached.specId;
|
|
363
|
+
// Non-Claude hosts (Copilot) never block on the network: serve the last pushed
|
|
364
|
+
// pin even when past its TTL, or drop the segment.
|
|
365
|
+
if (opts.cacheOnly) {
|
|
366
|
+
try {
|
|
367
|
+
const parsed = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
368
|
+
return typeof parsed?.specId === 'string' && parsed.specId ? parsed.specId : null;
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
363
373
|
|
|
364
374
|
const specId = await fetchPinnedSpecId(base);
|
|
365
375
|
writeFocusCache(cacheFile, specId);
|
|
@@ -492,10 +502,10 @@ function relevantSpecFromTranscript(session) {
|
|
|
492
502
|
// disables the transcript scan, restricting the segment to explicit claims
|
|
493
503
|
// only (name kept for backward compat with the original "suppress fallback"
|
|
494
504
|
// meaning — the fallback it originally guarded no longer exists).
|
|
495
|
-
function pickRelevantSpec(root, session) {
|
|
505
|
+
function pickRelevantSpec(root, session, opts = {}) {
|
|
496
506
|
const claim = readActiveClaim(root, session?.session_id);
|
|
497
507
|
const claimAt = claim ? Date.parse(claim.lastActivityAt ?? claim.claimedAt ?? '') : NaN;
|
|
498
|
-
const ref = process.env.HEYSPEX_STATUSLINE_NO_CLAIM === '1'
|
|
508
|
+
const ref = process.env.HEYSPEX_STATUSLINE_NO_CLAIM === '1' || opts.noTranscript
|
|
499
509
|
? null
|
|
500
510
|
: relevantSpecFromTranscript(session);
|
|
501
511
|
const refAt = ref?.at ? Date.parse(ref.at) : NaN;
|
|
@@ -983,7 +993,12 @@ function threadLabel(session) {
|
|
|
983
993
|
return typeof cache.label === 'string' && cache.label ? cache.label : null;
|
|
984
994
|
}
|
|
985
995
|
|
|
986
|
-
|
|
996
|
+
// `opts` (all optional; the Claude Code path passes none and is unchanged):
|
|
997
|
+
// host: 'copilot' - no Claude glyph, no Haiku thread label, no transcript scan
|
|
998
|
+
// (Copilot's transcript_path is a directory, and there is no `claude` CLI), and
|
|
999
|
+
// a cache-only focus-pin read so the line never waits on the console.
|
|
1000
|
+
export async function build(root, session, opts = {}) {
|
|
1001
|
+
const foreign = opts.host === 'copilot';
|
|
987
1002
|
const segments = [];
|
|
988
1003
|
|
|
989
1004
|
const mark = marker(root);
|
|
@@ -1005,16 +1020,16 @@ async function build(root, session) {
|
|
|
1005
1020
|
);
|
|
1006
1021
|
const modelSegment = [model, telemetrySegment].filter(Boolean).join(' · ');
|
|
1007
1022
|
if (modelSegment)
|
|
1008
|
-
segments.push(mark === GLYPH ? `${CLAUDE_GLYPH} ${modelSegment}` : modelSegment);
|
|
1023
|
+
segments.push(mark === GLYPH && !foreign ? `${CLAUDE_GLYPH} ${modelSegment}` : modelSegment);
|
|
1009
1024
|
|
|
1010
1025
|
// GitHub cluster: repo name + branch under one GitHub-mark prefix.
|
|
1011
1026
|
// Plain mode keeps the generic ⎇ ([HS] here would read as a claim segment).
|
|
1012
1027
|
const dir = basename(session?.workspace?.current_dir ?? session?.cwd) ?? basename(root);
|
|
1013
1028
|
const branch = readGitBranch(root);
|
|
1014
1029
|
const githubCluster = [dir, branch].filter(Boolean).join(' > ');
|
|
1015
|
-
if (githubCluster) segments.push(`${mark === GLYPH ? GITHUB_GLYPH : '⎇'} ${githubCluster}
|
|
1030
|
+
if (githubCluster) segments.push(`${mark === GLYPH ? GITHUB_GLYPH : foreign ? '' : '⎇'} ${githubCluster}`.trim());
|
|
1016
1031
|
|
|
1017
|
-
const pinned = await readPinnedSpecId(root);
|
|
1032
|
+
const pinned = await readPinnedSpecId(root, { cacheOnly: foreign });
|
|
1018
1033
|
if (pinned) segments.push(`${mark} ${pinned}`);
|
|
1019
1034
|
|
|
1020
1035
|
// HeySpex cluster, far right: "<glyph> <pillar> > <fileID> > <summary>"
|
|
@@ -1025,8 +1040,8 @@ async function build(root, session) {
|
|
|
1025
1040
|
// recent (pickRelevantSpec, above) — so it updates as the conversation's
|
|
1026
1041
|
// focus moves, and never shows another session's claim.
|
|
1027
1042
|
// Summary: what this session is about, in 2-6 words (sticky, pivot-only).
|
|
1028
|
-
const worked = workedDoc(root, pickRelevantSpec(root, session));
|
|
1029
|
-
const label = threadLabel(session);
|
|
1043
|
+
const worked = workedDoc(root, pickRelevantSpec(root, session, { noTranscript: foreign }));
|
|
1044
|
+
const label = foreign ? null : threadLabel(session);
|
|
1030
1045
|
const heyspexCluster = [worked?.pillar, worked?.display, label].filter(Boolean).join(' > ');
|
|
1031
1046
|
if (heyspexCluster) segments.push(`${mark} ${heyspexCluster}`);
|
|
1032
1047
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// HeySpex status line for GitHub Copilot CLI.
|
|
3
|
+
//
|
|
4
|
+
// Wired by `hs setup copilot --statusline` as `statusLine.command` in
|
|
5
|
+
// <COPILOT_HOME or ~/.copilot>/settings.json. Copilot pipes session status JSON on
|
|
6
|
+
// stdin and shows the first stdout line.
|
|
7
|
+
//
|
|
8
|
+
// This file is only a thin input adapter. All segment logic (model, repo > branch,
|
|
9
|
+
// pinned epic, claimed/relevant spec) is the Claude Code renderer's build() in
|
|
10
|
+
// scripts/claude-statusline/statusline-command.mjs, called with host:'copilot'
|
|
11
|
+
// (no Claude glyph, no Haiku label, no transcript scan, cache-only pin read).
|
|
12
|
+
//
|
|
13
|
+
// Contract: exit 0 always; never print an error or stack trace; on any failure or a
|
|
14
|
+
// hard 1.5 s budget overrun print the minimal "HeySpex" line. Plain ASCII-safe
|
|
15
|
+
// output (marker is "[HS]" unless HEYSPEX_STATUSLINE_GLYPH=1 and the font is installed).
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
import { build } from './claude-statusline/statusline-command.mjs';
|
|
21
|
+
import { isMainModule } from './lib/shared-path-helpers.mjs';
|
|
22
|
+
|
|
23
|
+
export const FALLBACK_LINE = 'HeySpex';
|
|
24
|
+
const STDIN_TIMEOUT_MS = 500;
|
|
25
|
+
const HARD_BUDGET_MS = 1500;
|
|
26
|
+
|
|
27
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
28
|
+
const str = (v) => (typeof v === 'string' && v.trim() ? v : null);
|
|
29
|
+
|
|
30
|
+
/** Copilot statusLine JSON -> the Claude-shaped session object build() reads. Never throws. */
|
|
31
|
+
export function adaptCopilotSession(input) {
|
|
32
|
+
const j = input && typeof input === 'object' ? input : {};
|
|
33
|
+
const cw = j.context_window && typeof j.context_window === 'object' ? j.context_window : {};
|
|
34
|
+
const dir = str(j.workspace?.current_dir) ?? str(j.cwd);
|
|
35
|
+
const used = num(cw.current_context_used_percentage) ?? num(cw.used_percentage);
|
|
36
|
+
const inTok = num(cw.total_input_tokens);
|
|
37
|
+
const outTok = num(cw.total_output_tokens);
|
|
38
|
+
const context_window = {};
|
|
39
|
+
// Zero tokens means "no API call yet" in Copilot; render nothing rather than "0 tok".
|
|
40
|
+
if (inTok !== null && inTok + (outTok ?? 0) > 0) {
|
|
41
|
+
context_window.total_input_tokens = inTok;
|
|
42
|
+
context_window.total_output_tokens = outTok ?? 0;
|
|
43
|
+
}
|
|
44
|
+
if (used !== null) context_window.remaining_percentage = 100 - used;
|
|
45
|
+
return {
|
|
46
|
+
session_id: str(j.session_id),
|
|
47
|
+
cwd: dir,
|
|
48
|
+
workspace: { current_dir: dir },
|
|
49
|
+
model: { display_name: str(j.model?.display_name) ?? str(j.model?.id) },
|
|
50
|
+
context_window,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Nearest dir at or above `start` holding .git or heyspex.yaml; else `start`. */
|
|
55
|
+
export function findRoot(start) {
|
|
56
|
+
let dir = path.resolve(start);
|
|
57
|
+
for (;;) {
|
|
58
|
+
if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'heyspex.yaml'))) return dir;
|
|
59
|
+
const up = path.dirname(dir);
|
|
60
|
+
if (up === dir) return path.resolve(start);
|
|
61
|
+
dir = up;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `hs mode off` honoured like the Claude line: env first, then <root>/<dot>heyspex/mode.json. */
|
|
66
|
+
function modeIsOff(root) {
|
|
67
|
+
try {
|
|
68
|
+
const raw = String(process.env.HEYSPEX_MODE ?? '').trim().toLowerCase();
|
|
69
|
+
if (raw) return ['off', '0', 'false', 'disabled', 'disable', 'no'].includes(raw);
|
|
70
|
+
return JSON.parse(fs.readFileSync(path.join(root, '.heyspex', 'mode.json'), 'utf8'))?.mode === 'off';
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Render one line for raw stdin text. Returns '' when HeySpex is switched off. Never throws. */
|
|
77
|
+
export async function renderLine(raw) {
|
|
78
|
+
try {
|
|
79
|
+
let parsed = null;
|
|
80
|
+
try { parsed = raw && raw.trim() ? JSON.parse(raw) : null; } catch { parsed = null; }
|
|
81
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return FALLBACK_LINE;
|
|
82
|
+
const session = adaptCopilotSession(parsed);
|
|
83
|
+
const root = findRoot(session.cwd ?? process.cwd());
|
|
84
|
+
if (modeIsOff(root)) return '';
|
|
85
|
+
const line = await build(root, session, { host: 'copilot' });
|
|
86
|
+
return (line ?? '').split(/\r?\n/)[0].trim() || FALLBACK_LINE;
|
|
87
|
+
} catch {
|
|
88
|
+
return FALLBACK_LINE;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readStdin() {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
if (process.stdin.isTTY) return resolve('');
|
|
95
|
+
const chunks = [];
|
|
96
|
+
let done = false;
|
|
97
|
+
const finish = () => {
|
|
98
|
+
if (done) return;
|
|
99
|
+
done = true;
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
102
|
+
};
|
|
103
|
+
const timer = setTimeout(finish, STDIN_TIMEOUT_MS);
|
|
104
|
+
try {
|
|
105
|
+
process.stdin.on('data', (c) => chunks.push(Buffer.from(c)));
|
|
106
|
+
process.stdin.on('end', finish);
|
|
107
|
+
process.stdin.on('error', finish);
|
|
108
|
+
} catch {
|
|
109
|
+
finish();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function main() {
|
|
115
|
+
const hard = setTimeout(() => {
|
|
116
|
+
try { process.stdout.write(`${FALLBACK_LINE}\n`); } catch { /* ignore */ }
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}, HARD_BUDGET_MS);
|
|
119
|
+
const line = await renderLine(await readStdin());
|
|
120
|
+
clearTimeout(hard);
|
|
121
|
+
if (line) process.stdout.write(`${line}\n`);
|
|
122
|
+
process.exit(0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isMainModule(import.meta.url)) {
|
|
126
|
+
main().catch(() => {
|
|
127
|
+
try { process.stdout.write(`${FALLBACK_LINE}\n`); } catch { /* ignore */ }
|
|
128
|
+
process.exit(0);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
// node scripts/install-copilot-hooks.mjs --print # agent-facing instructions to stdout
|
|
13
13
|
// --target <dir> repo to set up (default: cwd's repo root, walking up to .git / heyspex.yaml)
|
|
14
14
|
// --json (with --check) machine-readable report
|
|
15
|
+
// --statusline install ONLY the HeySpex status line: merges `statusLine` into <COPILOT_HOME or ~/.copilot>/settings.json
|
|
16
|
+
// (backup kept; a foreign statusLine is left alone unless --force). With --uninstall removes only ours.
|
|
17
|
+
// --force (with --statusline) replace a non-HeySpex statusLine
|
|
15
18
|
//
|
|
16
19
|
// Single-source rule: the plugin OR the repo hooks file, never both (every hook would fire twice).
|
|
17
20
|
//
|
|
@@ -33,6 +36,7 @@ import { COPILOT_PREREQUISITE, copilotHomeDir, parsePluginList, resolveCopilotCl
|
|
|
33
36
|
|
|
34
37
|
export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
35
38
|
export const HOOKS_REL = ['.github', 'hooks', 'heyspex.json'];
|
|
39
|
+
export const STATUSLINE_REL = ['scripts', 'copilot-statusline.mjs'];
|
|
36
40
|
const FALLBACK = { marketplace: 'heyspex-marketplace', plugin: 'heyspex' };
|
|
37
41
|
|
|
38
42
|
const fsDeps = {
|
|
@@ -73,10 +77,12 @@ export function findRepoRoot(start) {
|
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
function parseArgs(argv) {
|
|
76
|
-
const a = { hooks: false, check: false, uninstall: false, print: false, json: false, target: null, bad: null };
|
|
80
|
+
const a = { hooks: false, statusline: false, force: false, check: false, uninstall: false, print: false, json: false, target: null, bad: null };
|
|
77
81
|
for (let i = 0; i < argv.length; i++) {
|
|
78
82
|
const x = argv[i];
|
|
79
83
|
if (x === '--hooks') a.hooks = true;
|
|
84
|
+
else if (x === '--statusline') a.statusline = true;
|
|
85
|
+
else if (x === '--force') a.force = true;
|
|
80
86
|
else if (x === '--check') a.check = true;
|
|
81
87
|
else if (x === '--uninstall') a.uninstall = true;
|
|
82
88
|
else if (x === '--print') a.print = true;
|
|
@@ -151,6 +157,80 @@ function hooksFileState(target) {
|
|
|
151
157
|
return { file, exists: true, ours, current: raw.replace(/\r\n/g, '\n') === renderGithubHooksText() };
|
|
152
158
|
}
|
|
153
159
|
|
|
160
|
+
// ---- status line (opt-in) ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
export const STATUSLINE_REFRESH_SECS = 30;
|
|
163
|
+
const settingsPath = () => path.join(copilotHomeDir(), 'settings.json');
|
|
164
|
+
|
|
165
|
+
/** The statusLine command: node + the INSTALLED script, quoted so paths with spaces survive cmd.exe and sh. */
|
|
166
|
+
export function statuslineCommand(pkgRoot = PACKAGE_ROOT, platform = process.platform) {
|
|
167
|
+
const script = path.join(pkgRoot, ...STATUSLINE_REL);
|
|
168
|
+
return platform === 'win32' ? `node "${script}"` : `node '${script.replace(/'/g, "'\\''")}'`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const isOurStatusLine = (sl) => !!sl && typeof sl === 'object' && typeof sl.command === 'string' && /copilot-statusline\.mjs/.test(sl.command);
|
|
172
|
+
|
|
173
|
+
/** { file, exists, json, statusLine, error } for <COPILOT_HOME>/settings.json. */
|
|
174
|
+
export function readCopilotSettings(file = settingsPath()) {
|
|
175
|
+
const raw = fsDeps.readFile(file);
|
|
176
|
+
if (raw === null) return { file, exists: false, json: {}, statusLine: null, error: null };
|
|
177
|
+
try {
|
|
178
|
+
const json = raw.trim() ? JSON.parse(raw) : {};
|
|
179
|
+
if (!json || typeof json !== 'object' || Array.isArray(json)) throw new Error('not a JSON object');
|
|
180
|
+
return { file, exists: true, json, statusLine: json.statusLine ?? null, error: null };
|
|
181
|
+
} catch (e) {
|
|
182
|
+
return { file, exists: true, json: null, statusLine: null, error: e?.message ?? String(e) };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function backupSettings(file) {
|
|
187
|
+
const bak = `${file}.heyspex-bak`;
|
|
188
|
+
if (!fs.existsSync(bak)) fs.copyFileSync(file, bak);
|
|
189
|
+
return bak;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function writeSettings(file, json) {
|
|
193
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
194
|
+
fs.writeFileSync(file, `${JSON.stringify(json, null, 2)}
|
|
195
|
+
`, 'utf8');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function doStatusline(a, out, err) {
|
|
199
|
+
const script = path.join(PACKAGE_ROOT, ...STATUSLINE_REL);
|
|
200
|
+
const st = readCopilotSettings();
|
|
201
|
+
if (a.uninstall) {
|
|
202
|
+
if (st.error) { err(`hs setup copilot --statusline --uninstall: ${st.file} is not valid JSON (${st.error}); not touching it.`); return 1; }
|
|
203
|
+
if (!st.statusLine) { out(`no statusLine in ${st.file}; nothing to remove`); return 0; }
|
|
204
|
+
if (!isOurStatusLine(st.statusLine)) { out(`left the statusLine in ${st.file} alone: it is not the HeySpex one`); return 0; }
|
|
205
|
+
backupSettings(st.file);
|
|
206
|
+
const { statusLine: _drop, ...rest } = st.json;
|
|
207
|
+
writeSettings(st.file, rest);
|
|
208
|
+
out(`removed the HeySpex statusLine from ${st.file} (other keys kept; backup ${st.file}.heyspex-bak)`);
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
if (!fs.existsSync(script)) {
|
|
212
|
+
err(`hs setup copilot --statusline: ${script} not found. The heyspex package looks incomplete; run \`npm install heyspex@latest\` and retry.`);
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
if (st.error) { err(`hs setup copilot --statusline: ${st.file} is not valid JSON (${st.error}); fix or move it, then re-run. Nothing was changed.`); return 1; }
|
|
216
|
+
if (st.statusLine && !isOurStatusLine(st.statusLine) && !a.force) {
|
|
217
|
+
err(`hs setup copilot --statusline: ${st.file} already has a non-HeySpex statusLine (${JSON.stringify(st.statusLine.command ?? st.statusLine)}); not overwriting it.`);
|
|
218
|
+
err('Re-run with --force to replace it (a backup is kept).');
|
|
219
|
+
return 2;
|
|
220
|
+
}
|
|
221
|
+
const next = { type: 'command', command: statuslineCommand(), refreshInterval: STATUSLINE_REFRESH_SECS };
|
|
222
|
+
if (JSON.stringify(st.statusLine) === JSON.stringify(next)) {
|
|
223
|
+
out(`already current: statusLine in ${st.file}`);
|
|
224
|
+
} else {
|
|
225
|
+
if (st.exists) out(`backup: ${backupSettings(st.file)}`);
|
|
226
|
+
writeSettings(st.file, { ...st.json, statusLine: next });
|
|
227
|
+
out(`${st.statusLine ? 'updated' : 'wrote'} statusLine in ${st.file}`);
|
|
228
|
+
}
|
|
229
|
+
if (/[\\/]_npx[\\/]/.test(PACKAGE_ROOT)) out('WARNING: this ran from the npx cache, which can be pruned. Run it from a project that has heyspex installed (npm install heyspex) so the path stays valid.');
|
|
230
|
+
out('Next: restart Copilot (or /restart); the status line renders in interactive sessions only.');
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
154
234
|
function manualCommands(spec) {
|
|
155
235
|
return [
|
|
156
236
|
`copilot plugin marketplace add ${q(PACKAGE_ROOT)}`,
|
|
@@ -184,7 +264,11 @@ function doCheck(a, target, out) {
|
|
|
184
264
|
if (route === 'hooks' && hooks.ours && !hooks.current) problems.push('.github/hooks/heyspex.json is stale; re-run `npx heyspex setup copilot --hooks`');
|
|
185
265
|
if (route === 'none') problems.push(`no route active; run \`${nextCommand()}\` (plugin) or \`${nextCommand()} --hooks\``);
|
|
186
266
|
if (!cli && route === 'none') problems.push(`Copilot CLI not found (not on PATH, no Copilot app copy). ${COPILOT_PREREQUISITE}`);
|
|
187
|
-
const
|
|
267
|
+
const sl = readCopilotSettings();
|
|
268
|
+
const slState = sl.error ? 'unreadable' : !sl.statusLine ? 'absent' : isOurStatusLine(sl.statusLine) ? 'heyspex' : 'foreign';
|
|
269
|
+
const slScriptOk = fs.existsSync(path.join(PACKAGE_ROOT, ...STATUSLINE_REL));
|
|
270
|
+
if (slState === 'heyspex' && !slScriptOk) problems.push('statusLine points at a HeySpex script that is missing from this package; re-run `npx heyspex setup copilot --statusline`');
|
|
271
|
+
const report = { statusLine: slState, settingsPath: sl.file, route, plugin: ev.plugin, pluginEvidence: ev.evidence, hooksFile: hooks.exists, hooksFilePath: hooks.file, hooksCurrent: hooks.current, marketplaceManifest: spec.present, marketplacePath: spec.manifest, copilotCli: cli, copilotHome: copilotHomeDir(), problems };
|
|
188
272
|
if (a.json) out(JSON.stringify(report, null, 2));
|
|
189
273
|
else {
|
|
190
274
|
out(`hs setup copilot --check (target ${target})`);
|
|
@@ -193,6 +277,7 @@ function doCheck(a, target, out) {
|
|
|
193
277
|
out(` hooks file: ${hooks.exists ? `present${hooks.ours ? '' : ' (not HeySpex)'}${hooks.current ? ', current' : ', stale'}` : 'absent'}`);
|
|
194
278
|
out(` marketplace manifest in package: ${spec.present ? 'present' : 'MISSING'}`);
|
|
195
279
|
out(` copilot CLI: ${describeCli(cli)}`);
|
|
280
|
+
out(` statusLine: ${slState}${slState === 'heyspex' ? ` (${sl.statusLine.command})` : slState === 'foreign' ? ` (${JSON.stringify(sl.statusLine.command ?? sl.statusLine)}; not ours)` : slState === 'absent' ? ' (opt in: `npx heyspex setup copilot --statusline`)' : ` (${sl.error})`} in ${sl.file}`);
|
|
196
281
|
if (process.env.COPILOT_HOME) out(` COPILOT_HOME: ${copilotHomeDir()}`);
|
|
197
282
|
for (const p of problems) out(` PROBLEM: ${p}`);
|
|
198
283
|
if (!problems.length) out(' ok');
|
|
@@ -297,12 +382,13 @@ export function main(argv = process.argv.slice(2), io = {}) {
|
|
|
297
382
|
const a = parseArgs(argv);
|
|
298
383
|
if (a.bad) {
|
|
299
384
|
err(`hs setup copilot: ${a.bad}`);
|
|
300
|
-
err('usage: hs setup copilot [--hooks] [--check [--json]] [--uninstall] [--print] [--target <dir>]');
|
|
385
|
+
err('usage: hs setup copilot [--hooks] [--statusline [--force]] [--check [--json]] [--uninstall] [--print] [--target <dir>]');
|
|
301
386
|
return 2;
|
|
302
387
|
}
|
|
303
388
|
if (a.print) return doPrint(out);
|
|
304
389
|
const target = a.target ? path.resolve(a.target) : findRepoRoot(process.env.HS_SETUP_TARGET?.trim() || process.cwd());
|
|
305
390
|
if (a.check) return doCheck(a, target, out);
|
|
391
|
+
if (a.statusline) return doStatusline(a, out, err);
|
|
306
392
|
if (a.uninstall) return doUninstall(target, out, err);
|
|
307
393
|
if (a.hooks) return doHooks(target, out, err);
|
|
308
394
|
return doPlugin(target, out, err);
|
|
@@ -163,7 +163,12 @@ export function createRemoteMcpHandler(deps) {
|
|
|
163
163
|
// No listChanged: the remote surface is fixed (no progressive loader),
|
|
164
164
|
// and JSON-response mode has no channel to push a notification anyway.
|
|
165
165
|
capabilities: { tools: { listChanged: false } },
|
|
166
|
-
serverInfo: {
|
|
166
|
+
serverInfo: {
|
|
167
|
+
name: serverInfo?.name ?? 'heyspex',
|
|
168
|
+
...(serverInfo?.title ? { title: serverInfo.title } : {}),
|
|
169
|
+
version: serverInfo?.version ?? '0.0.0-unknown',
|
|
170
|
+
...(serverInfo?.icons ? { icons: serverInfo.icons } : {}),
|
|
171
|
+
},
|
|
167
172
|
...(instructions ? { instructions } : {}),
|
|
168
173
|
};
|
|
169
174
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server display identity (Implementation.title / Implementation.icons,
|
|
3
|
+
* MCP spec 2025-06-18 / 2025-11-25). `name` stays the stable identifier
|
|
4
|
+
* ("heyspex": tool prefixes mcp__heyspex__* depend on it); only `title` and
|
|
5
|
+
* `icons` are for display. The icon is the 128px PNG of
|
|
6
|
+
* plugins/heyspex/assets/heyspex-glyph-128.png inlined as a data: URI (PNG is
|
|
7
|
+
* the format the spec requires clients to support); a test keeps the two in sync.
|
|
8
|
+
*/
|
|
9
|
+
export const SERVER_TITLE = "HeySpex";
|
|
10
|
+
export const SERVER_ICON_DATA_URI = "data:image/png;base64,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";
|
|
11
|
+
export const SERVER_ICONS = Object.freeze([
|
|
12
|
+
Object.freeze({ src: SERVER_ICON_DATA_URI, mimeType: "image/png", sizes: Object.freeze(["128x128"]) }),
|
|
13
|
+
]);
|
|
@@ -39,6 +39,7 @@ import { applyToolBudget } from './response-budget.mjs';
|
|
|
39
39
|
import { resolveDevMode } from '../../scripts/lib/dev-mode.mjs';
|
|
40
40
|
import { dirname, join } from 'node:path';
|
|
41
41
|
import { MODULE_DIR } from './package-root.mjs';
|
|
42
|
+
import { SERVER_TITLE, SERVER_ICONS } from './server-identity.mjs';
|
|
42
43
|
|
|
43
44
|
export const SERVER_NAME = 'heyspex';
|
|
44
45
|
|
|
@@ -144,7 +145,10 @@ export async function handleInitialize(params) {
|
|
|
144
145
|
instructions,
|
|
145
146
|
serverInfo: {
|
|
146
147
|
name: SERVER_NAME,
|
|
148
|
+
// Display identity (MCP Implementation.title / .icons); `name` is the stable id.
|
|
149
|
+
title: SERVER_TITLE,
|
|
147
150
|
version: SERVER_VERSION,
|
|
151
|
+
icons: SERVER_ICONS,
|
|
148
152
|
},
|
|
149
153
|
};
|
|
150
154
|
}
|