heyspex 0.2.3 → 0.2.4

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.
@@ -8,7 +8,8 @@
8
8
  {
9
9
  "name": "heyspex",
10
10
  "source": "./plugins/heyspex",
11
- "description": "SpexCode plugin (for Claude Code and GitHub Copilot CLI): HeySpex vault integration, MCP server (spec vault tools + hs-* prompts) and lifecycle hooks. Prototype — see plugins/heyspex/README.md before enabling."
11
+ "displayName": "HeySpex",
12
+ "description": "HeySpex plugin for Claude Code and GitHub Copilot CLI: vault integration, MCP server (spec vault tools + hs-* prompts) and lifecycle hooks. Prototype — see plugins/heyspex/README.md before enabling."
12
13
  }
13
14
  ]
14
15
  }
package/README.md CHANGED
@@ -10,6 +10,7 @@ HeySpex sits between "I have an idea" and "an AI agent writes code" - it distill
10
10
  npm install heyspex
11
11
  npx heyspex setup copilot # registers the plugin with the Copilot CLI
12
12
  npx heyspex setup copilot --check # confirm; exit 0 = installed
13
+ npx heyspex setup copilot --statusline # optional: HeySpex status line in Copilot's settings.json (--uninstall to remove)
13
14
  ```
14
15
 
15
16
  Prerequisite: the GitHub Copilot CLI. Either install the GitHub Copilot app (its bundled CLI at `%LOCALAPPDATA%\github-copilot-sdk\cli\<version>\copilot.exe` is auto-detected, newest version wins), or `npm install -g @github/copilot`, or `winget install GitHub.Copilot`. If none is found, `setup copilot` installs nothing and exits non-zero. `COPILOT_HOME` is honoured. Full guide: [docs/copilot-plugin-install.md](docs/copilot-plugin-install.md).
package/bin/hs.mjs CHANGED
@@ -1077,7 +1077,7 @@ function main(argv) {
1077
1077
  `hs setup: expected \`claude\`, \`cursor\` or \`copilot\` (got \`${command ?? ""}\`)\n` +
1078
1078
  " hs setup claude [--check|--uninstall] Claude Code lifecycle hooks\n" +
1079
1079
  " hs setup cursor [--check|--uninstall] Cursor CLI lifecycle hooks\n" +
1080
- " hs setup copilot [--check|--uninstall|--print|--hooks] GitHub Copilot (plugin; --hooks for the cloud agent)\n",
1080
+ " hs setup copilot [--check|--uninstall|--print|--hooks|--statusline [--force]] GitHub Copilot (plugin; --hooks for the cloud agent; --statusline opt-in status line)\n",
1081
1081
  );
1082
1082
  process.exit(2);
1083
1083
  }
@@ -1089,7 +1089,7 @@ function main(argv) {
1089
1089
  setupFlags.length = 0;
1090
1090
  for (let i = 0; i < rest.length; i++) {
1091
1091
  const a = rest[i];
1092
- if (["--check", "--uninstall", "--print", "--hooks", "--json"].includes(a)) setupFlags.push(a);
1092
+ if (["--check", "--uninstall", "--print", "--hooks", "--json", "--statusline", "--force"].includes(a)) setupFlags.push(a);
1093
1093
  else if (a === "--target" && rest[i + 1]) setupFlags.push(a, rest[++i]);
1094
1094
  else if (a.startsWith("--target=")) setupFlags.push(a);
1095
1095
  }
@@ -187,3 +187,14 @@ The install commands come from GitHub's plugin reference, and the hook behaviour
187
187
  replaying Copilot-shaped payloads through the registered commands, not by a live Copilot session.
188
188
  The first agent that completes section 4 is the real test. Fix history: diagnose HS-20SW and
189
189
  capture HS-HNO1 (2026-10-16).
190
+
191
+ ## Optional: HeySpex status line
192
+
193
+ Opt in with `npx heyspex setup copilot --statusline`. It merges a `statusLine` block into `<COPILOT_HOME or ~/.copilot>/settings.json`
194
+ (other keys are kept; the first change leaves `settings.json.heyspex-bak`). The command is `node "<installed heyspex>/scripts/copilot-statusline.mjs"`,
195
+ so run it from a project that has `heyspex` installed, not from the npx cache. A non-HeySpex `statusLine` is never overwritten unless you pass `--force`.
196
+ `npx heyspex setup copilot --statusline --uninstall` removes only the HeySpex one; `--check` reports its state.
197
+
198
+ The line shows `Auto · 12% used | repo > branch | [HS] <pinned epic> | [HS] <pillar> > <claimed spec>`, reusing the Claude Code renderer.
199
+ It is plain text (no font needed), refreshes every 30 s (`refreshInterval`) plus on Copilot events, never waits on the network, and prints a bare `HeySpex`
200
+ if anything fails. `hs mode off` / `HEYSPEX_MODE=off` blanks it. Copilot renders it in interactive sessions only.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heyspex",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "HeySpex CLI + MCP server — connect Claude Code, Cursor, and any MCP client to your HeySpex spec vault",
5
5
  "keywords": [
6
6
  "heyspex",
@@ -76,6 +76,7 @@
76
76
  "scripts/install-claude-code-hooks.mjs",
77
77
  "scripts/install-cursor-hooks.mjs",
78
78
  "scripts/install-copilot-hooks.mjs",
79
+ "scripts/copilot-statusline.mjs",
79
80
  "scripts/lib/customer-kit.mjs",
80
81
  "scripts/lib/copilot-cli.mjs",
81
82
  "scripts/lib/hook-dispositions.mjs",
@@ -110,6 +111,7 @@
110
111
  "plugins/heyspex/.mcp.json",
111
112
  "plugins/heyspex/.cursor-plugin/plugin.json",
112
113
  "plugins/heyspex/mcp.json",
114
+ "plugins/heyspex/assets/*",
113
115
  "plugins/heyspex/hooks/*.json",
114
116
  "scripts/claude-hooks/*.mjs",
115
117
  "!scripts/claude-hooks/*.test.mjs",
@@ -1,12 +1,13 @@
1
- {
2
- "name": "heyspex",
3
- "displayName": "SpexCode plugin (for Claude Code and GitHub Copilot CLI)",
4
- "description": "HeySpex vault integration for Claude Code and GitHub Copilot CLI (Claude-format plugin, installed from the same marketplace): MCP server (spec vault tools + hs-* prompts) and lifecycle hooks. Prototype packaging per HS-QTBK — additive; the project-level .mcp.json/settings.json wiring remains the live source of truth until cutover.",
5
- "author": {
6
- "name": "pwher",
7
- "email": "paul@monstergrowth.com"
8
- },
9
- "homepage": "https://www.npmjs.com/package/heyspex",
10
- "license": "UNLICENSED",
11
- "keywords": ["heyspex", "spec-vault", "mcp", "hooks"]
12
- }
1
+ {
2
+ "name": "heyspex",
3
+ "displayName": "HeySpex",
4
+ "icon": "./assets/heyspex-glyph-256.png",
5
+ "description": "HeySpex vault integration for Claude Code and GitHub Copilot CLI (Claude-format plugin, installed from the same marketplace): MCP server (spec vault tools + hs-* prompts) and lifecycle hooks. Prototype packaging per HS-QTBK — additive; the project-level .mcp.json/settings.json wiring remains the live source of truth until cutover.",
6
+ "author": {
7
+ "name": "pwher",
8
+ "email": "paul@monstergrowth.com"
9
+ },
10
+ "homepage": "https://www.npmjs.com/package/heyspex",
11
+ "license": "UNLICENSED",
12
+ "keywords": ["heyspex", "spec-vault", "mcp", "hooks"]
13
+ }
@@ -1,15 +1,16 @@
1
- {
2
- "name": "heyspex",
3
- "displayName": "SpexCode plugin (for Claude Code)",
4
- "version": "0.1.0",
5
- "description": "HeySpex vault integration for Cursor: MCP server (spec vault tools + hs-* workflows) and the full lifecycle hook set. Prototype packaging — additive; the project-level .cursor/mcp.json and .cursor/hooks.json wiring remains the live source of truth until cutover.",
6
- "author": {
7
- "name": "pwher",
8
- "email": "paul@monstergrowth.com"
9
- },
10
- "homepage": "https://www.npmjs.com/package/heyspex",
11
- "license": "UNLICENSED",
12
- "keywords": ["heyspex", "spec-vault", "mcp", "hooks"],
13
- "mcp": "mcp.json",
14
- "hooks": "hooks/cursor-hooks.json"
15
- }
1
+ {
2
+ "name": "heyspex",
3
+ "displayName": "HeySpex",
4
+ "version": "0.1.0",
5
+ "logo": "assets/heyspex-glyph.svg",
6
+ "description": "HeySpex vault integration for Cursor: MCP server (spec vault tools + hs-* workflows) and the full lifecycle hook set. Prototype packaging — additive; the project-level .cursor/mcp.json and .cursor/hooks.json wiring remains the live source of truth until cutover.",
7
+ "author": {
8
+ "name": "pwher",
9
+ "email": "paul@monstergrowth.com"
10
+ },
11
+ "homepage": "https://www.npmjs.com/package/heyspex",
12
+ "license": "UNLICENSED",
13
+ "keywords": ["heyspex", "spec-vault", "mcp", "hooks"],
14
+ "mcp": "mcp.json",
15
+ "hooks": "hooks/cursor-hooks.json"
16
+ }
@@ -1,4 +1,4 @@
1
- # SpexCode plugin (for Claude Code and GitHub Copilot CLI) — Claude Code + Copilot CLI + Cursor (prototype)
1
+ # HeySpex plugin — Claude Code + Copilot CLI + Cursor (prototype)
2
2
 
3
3
  Prototype packaging of HeySpex's agent integration as a self-hosted plugin for **both** hosts,
4
4
  implementing migration-path steps 1–2 of [[HS-QTBK-5EK5-RES-claude-code-plugin-packaging-for]].
@@ -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
- async function build(root, session) {
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 report = { 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 };
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: { name: serverInfo?.name ?? 'heyspex', version: serverInfo?.version ?? '0.0.0-unknown' },
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
  }