@gethmy/mcp 2.5.2 → 2.5.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.
package/dist/cli.js CHANGED
@@ -4786,7 +4786,6 @@ class HarmonyMCPServer {
4786
4786
  // src/skills.ts
4787
4787
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
4788
4788
  import { dirname } from "node:path";
4789
- var VERSION_MARKER_PREFIX = "<!-- skills-version:";
4790
4789
  var HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
4791
4790
 
4792
4791
  Start work on a Harmony card. Card reference: $ARGUMENTS
@@ -4863,17 +4862,34 @@ If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
4863
4862
  **AI:** \`harmony_generate_prompt\`, \`harmony_process_command\`
4864
4863
  `;
4865
4864
  function buildSkillFile(skill) {
4866
- const major = Number.parseInt(skill.version.split(".")[0] ?? "", 10);
4867
- if (!Number.isFinite(major) || major < 1) {
4868
- throw new Error(`Invalid skill version: ${skill.version}`);
4865
+ if (skill.skillVersion !== undefined && !hasMetadataVersion(skill.content)) {
4866
+ return injectMetadataVersion(skill.content, skill.skillVersion);
4869
4867
  }
4870
- const trimmed = skill.content.replace(/\n+$/, "");
4871
- return `${trimmed}
4872
- ${VERSION_MARKER_PREFIX}${major} -->`;
4868
+ return skill.content;
4869
+ }
4870
+ function hasMetadataVersion(content) {
4871
+ return parseSkillVersion(content) !== null;
4872
+ }
4873
+ function injectMetadataVersion(content, version) {
4874
+ const fmMatch = content.match(/^(---\n[\s\S]*?\n)(---\n)([\s\S]*)$/);
4875
+ if (!fmMatch)
4876
+ return content;
4877
+ const [, head, close, body] = fmMatch;
4878
+ const block = `metadata:
4879
+ version: "${version}"
4880
+ `;
4881
+ return `${head}${block}${close}${body}`;
4873
4882
  }
4874
4883
  function parseSkillVersion(content) {
4875
- const match = content.match(/<!-- skills-version:(\d+) -->/);
4876
- return match ? match[1] : null;
4884
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
4885
+ if (fmMatch) {
4886
+ const fm = fmMatch[1];
4887
+ const verMatch = fm.match(/^metadata:[\s\S]*?\n[ \t]+version:[ \t]*["']?(\d+)["']?\s*$/m);
4888
+ if (verMatch)
4889
+ return verMatch[1];
4890
+ }
4891
+ const legacy = content.match(/<!-- skills-version:(\d+) -->/);
4892
+ return legacy ? legacy[1] : null;
4877
4893
  }
4878
4894
  function findSkillFiles(paths, knownNames) {
4879
4895
  const results = [];
@@ -4919,18 +4935,16 @@ async function refreshSkills() {
4919
4935
  }
4920
4936
  if (skillFiles.length === 0)
4921
4937
  return;
4922
- const remoteMajor = Number.parseInt(versionInfo.version.split(".")[0] ?? "", 10);
4923
- if (!Number.isFinite(remoteMajor))
4924
- return;
4925
4938
  let updated = false;
4926
4939
  for (const { name, filePath } of skillFiles) {
4927
4940
  try {
4928
4941
  const currentContent = readFileSync3(filePath, "utf-8");
4929
4942
  const localVersion = parseSkillVersion(currentContent);
4930
- if (localVersion !== null && Number(localVersion) >= remoteMajor) {
4943
+ const fetched = await client3.fetchSkill(name);
4944
+ const remoteVersion = fetched.skillVersion;
4945
+ if (remoteVersion !== undefined && localVersion !== null && Number(localVersion) >= remoteVersion) {
4931
4946
  continue;
4932
4947
  }
4933
- const fetched = await client3.fetchSkill(name);
4934
4948
  const newContent = buildSkillFile(fetched);
4935
4949
  const dir = dirname(filePath);
4936
4950
  if (!existsSync3(dir))
@@ -4943,7 +4957,7 @@ async function refreshSkills() {
4943
4957
  }
4944
4958
  }
4945
4959
  if (updated) {
4946
- console.error(`Harmony: Updated skills to v${versionInfo.version}`);
4960
+ console.error("Harmony: Refreshed skills from server");
4947
4961
  }
4948
4962
  } catch {}
4949
4963
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.5.2",
3
+ "version": "2.5.4",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/api-client.ts CHANGED
@@ -342,10 +342,15 @@ export class HarmonyApiClient {
342
342
  /**
343
343
  * GET /v1/skills/{name} — authenticated content fetch.
344
344
  * Returns rendered SKILL.md (frontmatter + body) ready to install.
345
+ *
346
+ * Phase 3b: `skillVersion` (per-skill integer) is returned alongside the
347
+ * legacy aggregate `version` string so refreshSkills can compare per-skill
348
+ * instead of treating any global bump as "everything stale."
345
349
  */
346
350
  async fetchSkill(name: string): Promise<{
347
351
  name: string;
348
352
  version: string;
353
+ skillVersion?: number;
349
354
  content: string;
350
355
  sha256: string;
351
356
  signature: string;
package/src/skills.ts CHANGED
@@ -3,8 +3,6 @@ import { dirname } from "node:path";
3
3
  import { HarmonyApiClient } from "./api-client.js";
4
4
  import { areSkillsInstalled, isConfigured } from "./config.js";
5
5
 
6
- const VERSION_MARKER_PREFIX = "<!-- skills-version:";
7
-
8
6
  /**
9
7
  * Legacy workflow prompt used by Codex / Cursor agents. Kept inline because
10
8
  * those agents install via AGENTS.md and not via /v1/skills. Claude Code
@@ -88,35 +86,76 @@ If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
88
86
 
89
87
  /**
90
88
  * Shape of a /v1/skills/{name} response. Used by buildSkillFile + tests.
89
+ *
90
+ * Phase 3b: `skillVersion` is the per-skill integer from `skill_resource.version`.
91
+ * `version` remains the aggregate "<major>.0.0" string used by the bash bootstrap
92
+ * for cache freshness — kept for compatibility, not load-bearing for refresh.
91
93
  */
92
94
  export interface FetchedSkill {
93
95
  name: string;
94
- version: string; // "X.Y.Z" aggregate from /v1/skills/{name}
95
- content: string; // rendered SKILL.md (frontmatter + body), already complete
96
+ version: string;
97
+ /** Per-skill row.version (Phase 3b). Optional during rollout older harmony-api
98
+ * deployments may omit it; treat absence as "trust the rendered frontmatter". */
99
+ skillVersion?: number;
100
+ content: string;
96
101
  }
97
102
 
98
103
  /**
99
- * Append the version marker so refreshSkills() can detect stale files later.
100
- * The harmony-api distribution shape omits the marker; mcp-server adds it on
101
- * install so parseSkillVersion() can read it back. The marker integer is the
102
- * major component of the aggregate version (e.g., "5.0.0" → 5).
104
+ * Pass the API-rendered SKILL.md through to disk. As of Phase 3b the
105
+ * frontmatter already carries `metadata.version: "<N>"` (rendered server-side
106
+ * by harmony-api), which is the source of truth `parseSkillVersion` reads on
107
+ * the next refresh. No EOF marker is appended.
108
+ *
109
+ * If `skillVersion` is provided AND the rendered content lacks the metadata.version
110
+ * block (older harmony-api deployment), we splice it in so install state always
111
+ * carries a parseable version. This preserves Phase-3b semantics across rolling
112
+ * deploys where the API hasn't been upgraded yet.
103
113
  */
104
114
  export function buildSkillFile(skill: FetchedSkill): string {
105
- const major = Number.parseInt(skill.version.split(".")[0] ?? "", 10);
106
- if (!Number.isFinite(major) || major < 1) {
107
- throw new Error(`Invalid skill version: ${skill.version}`);
115
+ if (skill.skillVersion !== undefined && !hasMetadataVersion(skill.content)) {
116
+ return injectMetadataVersion(skill.content, skill.skillVersion);
108
117
  }
109
- const trimmed = skill.content.replace(/\n+$/, "");
110
- return `${trimmed}\n${VERSION_MARKER_PREFIX}${major} -->`;
118
+ return skill.content;
119
+ }
120
+
121
+ function hasMetadataVersion(content: string): boolean {
122
+ return parseSkillVersion(content) !== null;
123
+ }
124
+
125
+ /**
126
+ * Splice `metadata.version: "<N>"` into the frontmatter of a rendered
127
+ * SKILL.md. Only used when harmony-api hasn't been upgraded to Phase-3b
128
+ * yet but mcp-server has — the fallback keeps install state consistent.
129
+ */
130
+ function injectMetadataVersion(content: string, version: number): string {
131
+ const fmMatch = content.match(/^(---\n[\s\S]*?\n)(---\n)([\s\S]*)$/);
132
+ if (!fmMatch) return content;
133
+ const [, head, close, body] = fmMatch;
134
+ const block = `metadata:\n version: "${version}"\n`;
135
+ return `${head}${block}${close}${body}`;
111
136
  }
112
137
 
113
138
  /**
114
- * Parse the embedded version marker out of an installed skill file.
115
- * Returns null if no marker is found caller treats that as "stale".
139
+ * Parse `metadata.version` out of an installed skill's frontmatter.
140
+ * Falls back to the legacy `<!-- skills-version:N -->` EOF marker so files
141
+ * installed by pre-Phase-3b mcp-server builds keep refreshing correctly
142
+ * until they're rewritten. Returns null when neither is present — the
143
+ * caller treats null as "stale, rewrite from API".
116
144
  */
117
145
  function parseSkillVersion(content: string): string | null {
118
- const match = content.match(/<!-- skills-version:(\d+) -->/);
119
- return match ? match[1] : null;
146
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
147
+ if (fmMatch) {
148
+ const fm = fmMatch[1];
149
+ // Match a `version:` line under a `metadata:` block. Tolerates either
150
+ // 2-space or 4-space child indentation and "..." or '...' quoting.
151
+ const verMatch = fm.match(
152
+ /^metadata:[\s\S]*?\n[ \t]+version:[ \t]*["']?(\d+)["']?\s*$/m,
153
+ );
154
+ if (verMatch) return verMatch[1];
155
+ }
156
+ // Legacy fallback — installed by pre-Phase-3b mcp-server builds.
157
+ const legacy = content.match(/<!-- skills-version:(\d+) -->/);
158
+ return legacy ? legacy[1] : null;
120
159
  }
121
160
 
122
161
  /**
@@ -146,6 +185,12 @@ function findSkillFiles(
146
185
  * Silently refresh installed skill files if remote versions are newer.
147
186
  * Called at MCP server startup. Non-blocking — any error (offline, 401,
148
187
  * partial install) is caught and the existing files are left in place.
188
+ *
189
+ * Phase 3b: per-skill compare. Each skill_resource row carries its own
190
+ * monotonic `version`. The mcp-server fetches each skill's `/v1/skills/<name>`
191
+ * once, reads the per-skill `skillVersion` from the response, and rewrites
192
+ * only the local files that are behind. An admin bumping `hmy` no longer
193
+ * triggers a re-fetch of `hmy-plan`, `hmy-cleanup`, etc.
149
194
  */
150
195
  export async function refreshSkills(): Promise<void> {
151
196
  try {
@@ -179,21 +224,25 @@ export async function refreshSkills(): Promise<void> {
179
224
  }
180
225
  if (skillFiles.length === 0) return;
181
226
 
182
- const remoteMajor = Number.parseInt(
183
- versionInfo.version.split(".")[0] ?? "",
184
- 10,
185
- );
186
- if (!Number.isFinite(remoteMajor)) return;
187
-
188
227
  let updated = false;
189
228
  for (const { name, filePath } of skillFiles) {
190
229
  try {
191
230
  const currentContent = readFileSync(filePath, "utf-8");
192
231
  const localVersion = parseSkillVersion(currentContent);
193
- if (localVersion !== null && Number(localVersion) >= remoteMajor) {
232
+
233
+ // Fetch first so we can read the authoritative per-skill version
234
+ // from the response. Cheaper than a separate version probe per skill.
235
+ const fetched = await client.fetchSkill(name);
236
+ const remoteVersion = fetched.skillVersion;
237
+
238
+ if (
239
+ remoteVersion !== undefined &&
240
+ localVersion !== null &&
241
+ Number(localVersion) >= remoteVersion
242
+ ) {
194
243
  continue;
195
244
  }
196
- const fetched = await client.fetchSkill(name);
245
+
197
246
  const newContent = buildSkillFile(fetched);
198
247
  const dir = dirname(filePath);
199
248
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
@@ -205,7 +254,7 @@ export async function refreshSkills(): Promise<void> {
205
254
  }
206
255
  }
207
256
  if (updated) {
208
- console.error(`Harmony: Updated skills to v${versionInfo.version}`);
257
+ console.error("Harmony: Refreshed skills from server");
209
258
  }
210
259
  } catch {
211
260
  // Non-blocking — offline / 401 / partial install all fall through here.