@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 +29 -15
- package/package.json +1 -1
- package/src/api-client.ts +5 -0
- package/src/skills.ts +75 -26
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
|
-
|
|
4867
|
-
|
|
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
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
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
|
|
4876
|
-
|
|
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
|
-
|
|
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(
|
|
4960
|
+
console.error("Harmony: Refreshed skills from server");
|
|
4947
4961
|
}
|
|
4948
4962
|
} catch {}
|
|
4949
4963
|
}
|
package/package.json
CHANGED
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;
|
|
95
|
-
|
|
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
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
115
|
-
*
|
|
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
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|