@gethmy/mcp 2.5.2 → 2.5.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/README.md +19 -4
- package/dist/cli.js +29 -21
- package/dist/index.js +0 -6
- package/dist/lib/api-client.js +0 -5
- package/package.json +1 -1
- package/src/api-client.ts +5 -21
- package/src/server.ts +2 -12
- package/src/skills.ts +75 -26
package/README.md
CHANGED
|
@@ -6,9 +6,8 @@ Enables AI coding agents (Claude Code, OpenAI Codex, Cursor) to interact with yo
|
|
|
6
6
|
## Features
|
|
7
7
|
|
|
8
8
|
- **56 MCP Tools** for full board control, knowledge graph, and workflow plans
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **Context Assembly** - token-budget-aware memory injection into AI prompts
|
|
9
|
+
- **4 Global Skills** — `/hmy`, `/hmy-plan`, `/hmy-cleanup`, `/hmy-standup`, served from the DB-backed [skill hub](../../docs/skills.md) with auto-update and admin-managed versioning
|
|
10
|
+
- **Knowledge Graph Memory** — Phase 1 surface: hybrid retrieval (vector + lexical + RRF), session-scoped working memory, activity feed. See [docs/memory.md](../../docs/memory.md)
|
|
12
11
|
- **GSD Workflow Plans** - plan/execute/verify/done lifecycle with auto card creation
|
|
13
12
|
- **Card Linking** - create relationships between cards (blocks, relates_to, duplicates, is_part_of)
|
|
14
13
|
- **Prompt Builder** - generate AI-ready prompts from cards with context
|
|
@@ -16,7 +15,7 @@ Enables AI coding agents (Claude Code, OpenAI Codex, Cursor) to interact with yo
|
|
|
16
15
|
- **Auto-Session Detection** - automatically starts/ends sessions when agents interact with cards
|
|
17
16
|
- **Auto-Assignment** - automatically assign cards to you when starting agent sessions
|
|
18
17
|
- **Memory Sync** - bidirectional sync between local markdown files and remote database
|
|
19
|
-
- **Multi-Agent Support** - works with Claude Code, Codex, Cursor
|
|
18
|
+
- **Multi-Agent Support** - works with Claude Code, Codex, Cursor, Claude.ai
|
|
20
19
|
- **Smart Setup** - one command configures everything
|
|
21
20
|
- **API Key Authentication** - no database credentials required
|
|
22
21
|
|
|
@@ -155,6 +154,10 @@ npx @gethmy/mcp serve # Start MCP server
|
|
|
155
154
|
|
|
156
155
|
## Skills
|
|
157
156
|
|
|
157
|
+
Four global skills ship with the MCP server and are installed automatically by `npx @gethmy/mcp setup`. They live in the `skill_resource` Postgres table, are fetched via `GET /v1/skills/<name>`, and render-time composed with a shared auto-update preamble.
|
|
158
|
+
|
|
159
|
+
For the full skill hub architecture (storage, versioning, auto-update, admin management), see [docs/skills.md](../../docs/skills.md).
|
|
160
|
+
|
|
158
161
|
### `/hmy` — Card Workflow
|
|
159
162
|
|
|
160
163
|
When you start working on a card (e.g., `/hmy #42`):
|
|
@@ -191,6 +194,18 @@ A single command for both creating and executing plans. It auto-detects intent b
|
|
|
191
194
|
3. Offers options: create single card, create multiple cards, analyze only, or skip
|
|
192
195
|
4. Creates cards and advances the plan phase
|
|
193
196
|
|
|
197
|
+
### `/hmy-cleanup` — Board Audit
|
|
198
|
+
|
|
199
|
+
Scans the board for stale cards — long-idle, missing owners, or stuck in review — and proposes cleanup actions. Read-only by default; opt-in to apply suggestions.
|
|
200
|
+
|
|
201
|
+
### `/hmy-standup` — Daily Summary
|
|
202
|
+
|
|
203
|
+
Generates a structured standup summary: what shipped, what's in progress, what's blocked, and where agents need input. Pulls from card activity, plan progress, and the memory activity feed.
|
|
204
|
+
|
|
205
|
+
### Auto-Update
|
|
206
|
+
|
|
207
|
+
Every skill ships with an `hmy-update-check` preamble that fires on invocation. It compares local versions against `/v1/skills/version` and prompts (or silently auto-installs, if `~/.hmy/config.yaml` has `auto_upgrade: true`). Snooze levels: 24h / 48h / 7d.
|
|
208
|
+
|
|
194
209
|
## Available Tools
|
|
195
210
|
|
|
196
211
|
### Card Operations
|
package/dist/cli.js
CHANGED
|
@@ -1321,11 +1321,6 @@ class HarmonyApiClient {
|
|
|
1321
1321
|
async fetchSkill(name) {
|
|
1322
1322
|
return this.request("GET", `/skills/${encodeURIComponent(name)}`);
|
|
1323
1323
|
}
|
|
1324
|
-
async recordSkillInvocation(name) {
|
|
1325
|
-
try {
|
|
1326
|
-
await this.request("POST", "/skills/telemetry", { name });
|
|
1327
|
-
} catch {}
|
|
1328
|
-
}
|
|
1329
1324
|
async listWorkspaces() {
|
|
1330
1325
|
return this.request("GET", "/workspaces");
|
|
1331
1326
|
}
|
|
@@ -3667,7 +3662,6 @@ function registerHandlers(server, deps) {
|
|
|
3667
3662
|
throw new Error(`Cannot read skill "${name}": Harmony MCP server is not configured. Run \`hmy-mcp setup\`.`);
|
|
3668
3663
|
}
|
|
3669
3664
|
const client3 = deps.getClient();
|
|
3670
|
-
client3.recordSkillInvocation(name);
|
|
3671
3665
|
const fetched = await client3.fetchSkill(name);
|
|
3672
3666
|
return {
|
|
3673
3667
|
contents: [
|
|
@@ -4786,7 +4780,6 @@ class HarmonyMCPServer {
|
|
|
4786
4780
|
// src/skills.ts
|
|
4787
4781
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4788
4782
|
import { dirname } from "node:path";
|
|
4789
|
-
var VERSION_MARKER_PREFIX = "<!-- skills-version:";
|
|
4790
4783
|
var HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
|
|
4791
4784
|
|
|
4792
4785
|
Start work on a Harmony card. Card reference: $ARGUMENTS
|
|
@@ -4863,17 +4856,34 @@ If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
|
|
|
4863
4856
|
**AI:** \`harmony_generate_prompt\`, \`harmony_process_command\`
|
|
4864
4857
|
`;
|
|
4865
4858
|
function buildSkillFile(skill) {
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
throw new Error(`Invalid skill version: ${skill.version}`);
|
|
4859
|
+
if (skill.skillVersion !== undefined && !hasMetadataVersion(skill.content)) {
|
|
4860
|
+
return injectMetadataVersion(skill.content, skill.skillVersion);
|
|
4869
4861
|
}
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4862
|
+
return skill.content;
|
|
4863
|
+
}
|
|
4864
|
+
function hasMetadataVersion(content) {
|
|
4865
|
+
return parseSkillVersion(content) !== null;
|
|
4866
|
+
}
|
|
4867
|
+
function injectMetadataVersion(content, version) {
|
|
4868
|
+
const fmMatch = content.match(/^(---\n[\s\S]*?\n)(---\n)([\s\S]*)$/);
|
|
4869
|
+
if (!fmMatch)
|
|
4870
|
+
return content;
|
|
4871
|
+
const [, head, close, body] = fmMatch;
|
|
4872
|
+
const block = `metadata:
|
|
4873
|
+
version: "${version}"
|
|
4874
|
+
`;
|
|
4875
|
+
return `${head}${block}${close}${body}`;
|
|
4873
4876
|
}
|
|
4874
4877
|
function parseSkillVersion(content) {
|
|
4875
|
-
const
|
|
4876
|
-
|
|
4878
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
4879
|
+
if (fmMatch) {
|
|
4880
|
+
const fm = fmMatch[1];
|
|
4881
|
+
const verMatch = fm.match(/^metadata:[\s\S]*?\n[ \t]+version:[ \t]*["']?(\d+)["']?\s*$/m);
|
|
4882
|
+
if (verMatch)
|
|
4883
|
+
return verMatch[1];
|
|
4884
|
+
}
|
|
4885
|
+
const legacy = content.match(/<!-- skills-version:(\d+) -->/);
|
|
4886
|
+
return legacy ? legacy[1] : null;
|
|
4877
4887
|
}
|
|
4878
4888
|
function findSkillFiles(paths, knownNames) {
|
|
4879
4889
|
const results = [];
|
|
@@ -4919,18 +4929,16 @@ async function refreshSkills() {
|
|
|
4919
4929
|
}
|
|
4920
4930
|
if (skillFiles.length === 0)
|
|
4921
4931
|
return;
|
|
4922
|
-
const remoteMajor = Number.parseInt(versionInfo.version.split(".")[0] ?? "", 10);
|
|
4923
|
-
if (!Number.isFinite(remoteMajor))
|
|
4924
|
-
return;
|
|
4925
4932
|
let updated = false;
|
|
4926
4933
|
for (const { name, filePath } of skillFiles) {
|
|
4927
4934
|
try {
|
|
4928
4935
|
const currentContent = readFileSync3(filePath, "utf-8");
|
|
4929
4936
|
const localVersion = parseSkillVersion(currentContent);
|
|
4930
|
-
|
|
4937
|
+
const fetched = await client3.fetchSkill(name);
|
|
4938
|
+
const remoteVersion = fetched.skillVersion;
|
|
4939
|
+
if (remoteVersion !== undefined && localVersion !== null && Number(localVersion) >= remoteVersion) {
|
|
4931
4940
|
continue;
|
|
4932
4941
|
}
|
|
4933
|
-
const fetched = await client3.fetchSkill(name);
|
|
4934
4942
|
const newContent = buildSkillFile(fetched);
|
|
4935
4943
|
const dir = dirname(filePath);
|
|
4936
4944
|
if (!existsSync3(dir))
|
|
@@ -4943,7 +4951,7 @@ async function refreshSkills() {
|
|
|
4943
4951
|
}
|
|
4944
4952
|
}
|
|
4945
4953
|
if (updated) {
|
|
4946
|
-
console.error(
|
|
4954
|
+
console.error("Harmony: Refreshed skills from server");
|
|
4947
4955
|
}
|
|
4948
4956
|
} catch {}
|
|
4949
4957
|
}
|
package/dist/index.js
CHANGED
|
@@ -1317,11 +1317,6 @@ class HarmonyApiClient {
|
|
|
1317
1317
|
async fetchSkill(name) {
|
|
1318
1318
|
return this.request("GET", `/skills/${encodeURIComponent(name)}`);
|
|
1319
1319
|
}
|
|
1320
|
-
async recordSkillInvocation(name) {
|
|
1321
|
-
try {
|
|
1322
|
-
await this.request("POST", "/skills/telemetry", { name });
|
|
1323
|
-
} catch {}
|
|
1324
|
-
}
|
|
1325
1320
|
async listWorkspaces() {
|
|
1326
1321
|
return this.request("GET", "/workspaces");
|
|
1327
1322
|
}
|
|
@@ -3663,7 +3658,6 @@ function registerHandlers(server, deps) {
|
|
|
3663
3658
|
throw new Error(`Cannot read skill "${name}": Harmony MCP server is not configured. Run \`hmy-mcp setup\`.`);
|
|
3664
3659
|
}
|
|
3665
3660
|
const client3 = deps.getClient();
|
|
3666
|
-
client3.recordSkillInvocation(name);
|
|
3667
3661
|
const fetched = await client3.fetchSkill(name);
|
|
3668
3662
|
return {
|
|
3669
3663
|
contents: [
|
package/dist/lib/api-client.js
CHANGED
|
@@ -924,11 +924,6 @@ class HarmonyApiClient {
|
|
|
924
924
|
async fetchSkill(name) {
|
|
925
925
|
return this.request("GET", `/skills/${encodeURIComponent(name)}`);
|
|
926
926
|
}
|
|
927
|
-
async recordSkillInvocation(name) {
|
|
928
|
-
try {
|
|
929
|
-
await this.request("POST", "/skills/telemetry", { name });
|
|
930
|
-
} catch {}
|
|
931
|
-
}
|
|
932
927
|
async listWorkspaces() {
|
|
933
928
|
return this.request("GET", "/workspaces");
|
|
934
929
|
}
|
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;
|
|
@@ -353,27 +358,6 @@ export class HarmonyApiClient {
|
|
|
353
358
|
return this.request("GET", `/skills/${encodeURIComponent(name)}`);
|
|
354
359
|
}
|
|
355
360
|
|
|
356
|
-
/**
|
|
357
|
-
* POST /v1/skills/telemetry — fire-and-forget activation signal.
|
|
358
|
-
* Called from the ReadResourceRequest handler when an agent reads
|
|
359
|
-
* a harmony://skills/<name> resource. Increments invocation_count +
|
|
360
|
-
* sets last_used = now() on the matching skill_resource row.
|
|
361
|
-
*
|
|
362
|
-
* Best-effort: returns void; never throws. If the endpoint doesn't
|
|
363
|
-
* exist yet (pre-Phase-2 backend), or the network is flaky, the
|
|
364
|
-
* activation is dropped silently. The skill content read itself
|
|
365
|
-
* is NOT gated on this call succeeding.
|
|
366
|
-
*/
|
|
367
|
-
async recordSkillInvocation(name: string): Promise<void> {
|
|
368
|
-
try {
|
|
369
|
-
await this.request("POST", "/skills/telemetry", { name });
|
|
370
|
-
} catch {
|
|
371
|
-
// Fire-and-forget: any failure (404 if endpoint not deployed yet,
|
|
372
|
-
// 401 if API key rotated, network timeout) is swallowed so the
|
|
373
|
-
// caller can proceed with serving content.
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
361
|
// ============ WORKSPACE OPERATIONS ============
|
|
378
362
|
|
|
379
363
|
async listWorkspaces(): Promise<{ workspaces: unknown[] }> {
|
package/src/server.ts
CHANGED
|
@@ -1533,9 +1533,7 @@ export const RESOURCES = [
|
|
|
1533
1533
|
/**
|
|
1534
1534
|
* Build the dynamic resource list. Skill resources (harmony://skills/<name>)
|
|
1535
1535
|
* are derived at request time from /v1/skills/version so the list always
|
|
1536
|
-
* matches what the agent will get on read.
|
|
1537
|
-
* ReadResourceRequest on harmony://skills/<name> is the activation signal
|
|
1538
|
-
* for skill telemetry.
|
|
1536
|
+
* matches what the agent will get on read.
|
|
1539
1537
|
*
|
|
1540
1538
|
* Offline / unauthenticated → returns just the static RESOURCES. Agents
|
|
1541
1539
|
* that can't reach the API still see the context resource and any error
|
|
@@ -1716,11 +1714,7 @@ export function registerHandlers(server: Server, deps: ToolDeps): void {
|
|
|
1716
1714
|
};
|
|
1717
1715
|
}
|
|
1718
1716
|
|
|
1719
|
-
// harmony://skills/<name> —
|
|
1720
|
-
// activation event: agent matched the skill's description, harness
|
|
1721
|
-
// calls ReadResource, we fire fire-and-forget telemetry before
|
|
1722
|
-
// returning the body. recordSkillInvocation() swallows all errors
|
|
1723
|
-
// so a telemetry outage never blocks skill delivery.
|
|
1717
|
+
// harmony://skills/<name> — serve the rendered SKILL.md.
|
|
1724
1718
|
const SKILL_URI_RE = /^harmony:\/\/skills\/([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
1725
1719
|
const skillMatch = uri.match(SKILL_URI_RE);
|
|
1726
1720
|
if (skillMatch) {
|
|
@@ -1731,10 +1725,6 @@ export function registerHandlers(server: Server, deps: ToolDeps): void {
|
|
|
1731
1725
|
);
|
|
1732
1726
|
}
|
|
1733
1727
|
const client = deps.getClient();
|
|
1734
|
-
// Fire-and-forget BEFORE awaiting the content fetch so a slow
|
|
1735
|
-
// telemetry endpoint never delays skill delivery. The promise
|
|
1736
|
-
// is intentionally dropped (caught inside recordSkillInvocation).
|
|
1737
|
-
void client.recordSkillInvocation(name);
|
|
1738
1728
|
const fetched = await client.fetchSkill(name);
|
|
1739
1729
|
return {
|
|
1740
1730
|
contents: [
|
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.
|