@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 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
- - **Knowledge Graph Memory** - persistent memory with entity types, tiers, scopes, and typed relations
10
- - **Active Learning** - auto-extracts lessons, solutions, and error patterns from completed work sessions
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
- 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}`);
4859
+ if (skill.skillVersion !== undefined && !hasMetadataVersion(skill.content)) {
4860
+ return injectMetadataVersion(skill.content, skill.skillVersion);
4869
4861
  }
4870
- const trimmed = skill.content.replace(/\n+$/, "");
4871
- return `${trimmed}
4872
- ${VERSION_MARKER_PREFIX}${major} -->`;
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 match = content.match(/<!-- skills-version:(\d+) -->/);
4876
- return match ? match[1] : null;
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
- if (localVersion !== null && Number(localVersion) >= remoteMajor) {
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(`Harmony: Updated skills to v${versionInfo.version}`);
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: [
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.5.2",
3
+ "version": "2.5.5",
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;
@@ -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. Phase 1 of card #162 — the
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> — Phase 1 of card #162. This read IS the
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; // "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.