@gethmy/mcp 2.5.1 → 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/src/skills.ts CHANGED
@@ -1,14 +1,12 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { areSkillsInstalled } from "./config.js";
4
-
5
- export const SKILLS_VERSION = "4";
6
-
7
- const VERSION_MARKER_PREFIX = "<!-- skills-version:";
3
+ import { HarmonyApiClient } from "./api-client.js";
4
+ import { areSkillsInstalled, isConfigured } from "./config.js";
8
5
 
9
6
  /**
10
- * Legacy workflow prompt used by Codex, Cursor agents.
11
- * Claude Code skills use the newer SKILL_DEFINITIONS content instead.
7
+ * Legacy workflow prompt used by Codex / Cursor agents. Kept inline because
8
+ * those agents install via AGENTS.md and not via /v1/skills. Claude Code
9
+ * agents use the DB-backed skill_resource registry (see Phase 0 of card #162).
12
10
  */
13
11
  export const HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
14
12
 
@@ -87,545 +85,178 @@ If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
87
85
  `;
88
86
 
89
87
  /**
90
- * New Claude Code skill content with intent detection.
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
- const HMY_SKILL_CONTENT = `# Harmony Card Workflow
93
-
94
- User input: $ARGUMENTS
95
-
96
- ## 0. Detect Intent
97
-
98
- Parse \`$ARGUMENTS\` to determine what the user wants:
99
-
100
- | Pattern | Intent | Go to |
101
- |---|---|---|
102
- | \`create ...\` or \`new ...\` | **Create** a new card | Step A |
103
- | \`#42\`, \`42\`, UUID, or card name (no action verb) | **Work on** an existing card | Step B |
104
- | \`move #42 to Done\`, \`update #42 ...\`, \`assign #42 ...\` | **Quick action** on a card | Step C |
105
- | \`show #42\`, \`view #42\`, \`status #42\` | **View** card details | Step D |
106
-
107
- If ambiguous, ask the user what they'd like to do.
108
-
109
- ---
110
-
111
- ## Step A: Create Card
112
-
113
- Create a card without starting work on it.
114
-
115
- 1. Parse the title and any details from the arguments (e.g., \`create Add dark mode toggle\` → title: "Add dark mode toggle")
116
- 2. Call \`harmony_create_card\` with:
117
- - \`title\`: extracted title
118
- - \`description\`: if the user provided details beyond the title
119
- - \`priority\`, \`columnId\`, \`assigneeId\`: only if explicitly specified
120
- 3. Show the created card: title, short ID, column, and a link if available.
121
- 4. **Stop here.** Do not start an agent session or begin implementation.
122
-
123
- ---
124
-
125
- ## Step B: Work on Existing Card
126
-
127
- Start work on a Harmony card.
128
-
129
- ### B1. Find & Fetch Card
130
-
131
- Parse the reference and fetch the card:
132
- - \`#42\` or \`42\` → \`harmony_get_card_by_short_id\` with \`shortId: 42\`
133
- - UUID → \`harmony_get_card\` with \`cardId\`
134
- - Name/text → \`harmony_search_cards\` with \`query\`
135
-
136
- ### B2. Start Agent Session
137
-
138
- Call \`harmony_start_agent_session\` with:
139
- - \`cardId\`: Card UUID
140
- - \`agentIdentifier\`: Your agent identifier
141
- - \`agentName\`: Your agent name
142
- - \`currentTask\`: A specific description of the first thing you'll do (e.g., "Exploring codebase to understand auth flow"), NOT a generic phrase like "Analyzing card requirements"
143
- - \`moveToColumn\`: "In Progress"
144
- - \`addLabels\`: ["agent"]
145
-
146
- This single call moves the card, adds the label, auto-assigns, and starts the session.
147
-
148
- ### B3. Generate Work Prompt
149
-
150
- Call \`harmony_generate_prompt\` with:
151
- - \`cardId\` or \`shortId\` (+ \`projectId\` if using shortId)
152
- - \`variant\`: Select based on task:
153
- - \`"execute"\` (default) → Clear tasks, bug fixes, well-defined work
154
- - \`"analysis"\` → Complex features, unclear requirements
155
- - \`"draft"\` → Medium complexity, want feedback first
156
-
157
- The generated prompt provides role framing, focus areas, subtasks, linked cards, and suggested outputs.
158
-
159
- ### B4. Display Card Summary
160
-
161
- Show the user: Card title, short ID, role, priority, labels, due date, description, and subtasks.
162
-
163
- ### B5. Implement Solution
164
-
165
- Work on the card following the generated prompt's guidance.
166
-
167
- **REQUIRED: Update progress at each milestone** by calling \`harmony_update_agent_progress\`. This is not optional — the card's live status badge depends on these updates.
168
-
169
- | Milestone | \`progressPercent\` | Example \`currentTask\` |
170
- |---|---|---|
171
- | After exploring codebase & understanding requirements | 20 | "Reading auth middleware and identifying affected routes" |
172
- | When starting implementation | 50 | "Refactoring token validation in auth.ts" |
173
- | When moving to testing/verification | 80 | "Running build and verifying changes compile" |
174
- | When done, before ending session | 100 | "All changes complete, ready for review" |
175
-
176
- Always set \`currentTask\` to a specific description of what you're actually doing — never leave it as "Analyzing card requirements" or other generic text.
177
-
178
- ### B6. Complete Work
179
-
180
- When finished:
181
- 1. \`harmony_end_agent_session\` with \`status: "completed"\`, \`progressPercent: 100\`, \`moveToColumn: "Review"\`
182
- 2. Summarize accomplishments
183
-
184
- If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
185
-
186
- ---
187
-
188
- ## Step C: Quick Action
189
-
190
- Fetch the card first (same as B1), then perform the requested action:
191
- - **Move:** \`harmony_move_card\` with target column
192
- - **Update:** \`harmony_update_card\` with changed fields
193
- - **Assign:** \`harmony_assign_card\`
194
- - **Label:** \`harmony_add_label_to_card\` / \`harmony_remove_label_from_card\`
195
- - **Archive:** \`harmony_archive_card\`
196
-
197
- Show confirmation and stop. Do not start an agent session.
198
-
199
- ---
200
-
201
- ## Step D: View Card
202
-
203
- Fetch the card (same as B1) and display: title, short ID, column, priority, labels, assignee, due date, description, subtasks, and links.
204
-
205
- Do not start an agent session.
206
-
207
- ---
208
-
209
- ## Step E: Auto-Detect Card for Implementation Tasks
210
-
211
- **IMPORTANT:** This step applies when you are about to implement a plan, feature, or fix
212
- that was NOT started via /hmy. Before writing any code, check if the work maps to an
213
- existing Harmony card.
214
-
215
- ### When to run this check
216
- - User says "implement this plan", "build this feature", "fix this bug" (without /hmy)
217
- - You are about to execute a plan file
218
- - Skip if: the user already started with /hmy, or no Harmony MCP tools are available
219
-
220
- ### Detection steps
221
- 1. Call \`harmony_search_cards\` with keywords from the plan title or task description
222
- 2. If a card matches (same feature/fix described), tell the user:
223
- "This maps to card #N — starting a session to track progress."
224
- 3. Call \`harmony_start_agent_session\` with \`moveToColumn: "In Progress"\`, \`addLabels: ["agent"]\`
225
- 4. Track progress with \`harmony_update_agent_progress\` at milestones
226
- 5. When done, call \`harmony_end_agent_session\` with \`status: "completed"\`, \`moveToColumn: "Review"\`
227
-
228
- ### If no match
229
- Proceed normally without a session. No action needed.
230
-
231
- ---
232
-
233
- ## Key Tools Reference
234
-
235
- **Cards:** \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\`, \`harmony_create_card\`, \`harmony_update_card\`, \`harmony_move_card\`, \`harmony_delete_card\`, \`harmony_assign_card\`
236
-
237
- **Subtasks:** \`harmony_create_subtask\`, \`harmony_toggle_subtask\`, \`harmony_delete_subtask\`
238
-
239
- **Labels:** \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\`, \`harmony_create_label\`
240
-
241
- **Links:** \`harmony_add_link_to_card\`, \`harmony_remove_link_from_card\`, \`harmony_get_card_links\`
242
-
243
- **Board:** \`harmony_get_board\`, \`harmony_list_projects\`, \`harmony_get_context\`, \`harmony_set_project_context\`
244
-
245
- **Sessions:** \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\`, \`harmony_get_agent_session\`
246
-
247
- **AI:** \`harmony_generate_prompt\`, \`harmony_process_command\`
248
- `;
249
-
250
- const HMY_PLAN_CONTENT = `# Harmony Plan Workflow
251
-
252
- Create a new plan or work on an existing one. Argument: $ARGUMENTS
253
-
254
- ## Step 1 — Detect Intent
255
-
256
- Parse \`$ARGUMENTS\` to determine the workflow:
257
-
258
- - **UUID** (contains dashes, 36 chars) → call \`harmony_get_plan\` with \`planId\` directly → **Step 2A**
259
- - **\`#N\`** (short ID) → call \`harmony_get_card_by_short_id\` to get the card, then \`harmony_get_plan\` with \`cardId\` → **Step 2A**
260
- - **Text** (anything else) → call \`harmony_list_plans\` with \`search\` set to the text
261
- - If **one match** → use it → **Step 2A**
262
- - If **multiple matches** → list them with title, status, phase, and updated date. Ask the user to pick one using \`AskUserQuestion\` → **Step 2A**
263
- - If **no matches** → ask user: "No existing plans found for '$ARGUMENTS'. Would you like to create a new plan on this topic?" → **Step 2B**
264
- - **Empty / vague topic** → **Step 2B** (create new plan)
265
-
266
- ---
267
-
268
- ## Step 2A — Work on Existing Plan
269
-
270
- ### 2A.1 — Analyze & Display
271
-
272
- Once you have the plan ID, call \`harmony_get_plan\` to fetch the full plan with tasks. Show a structured summary:
273
-
274
- \\\`\\\`\\\`
275
- ## [Plan Title]
276
- **Status:** draft/active/archived | **Phase:** plan/execute/verify/done
277
- **Tasks:** N total (X pending, Y in_progress, Z completed)
278
- **URL:** https://app.gethmy.com/plans/{id}
279
- \\\`\\\`\\\`
280
-
281
- If plan is in execute phase and tasks already have linked cards, note which tasks have cards and which don't.
282
-
283
- ### 2A.2 — Recall Context
284
-
285
- Call \`harmony_memory_search\` with the plan title to find related knowledge, patterns, or decisions that may inform execution.
286
-
287
- ### 2A.3 — Present Options
288
-
289
- Use \`AskUserQuestion\` to offer execution choices. Adapt options based on plan state:
290
-
291
- #### Default options (plan in \`plan\` phase):
292
-
293
- **(A) Single card** — Create one card with plan summary + link to plan. Best for simple plans.
294
- **(B) Multiple cards** — Create one card per task via \`harmony_advance_plan\`. Best for complex plans with independent tasks.
295
- **(C) Analyze only** — Review the plan and design an implementation approach. Create cards later.
296
- **(D) Skip** — Do nothing.
297
-
298
- #### If plan has no tasks:
299
- Only offer **(A) Single card**, **(C) Analyze only**, or **(D) Skip**.
300
-
301
- #### If plan is already in \`execute\` phase with existing cards:
302
- **(A) Work on existing cards** — List cards with short IDs, suggest \`/hmy #N\` to start
303
- **(B) Create cards for remaining tasks** — Only create cards for tasks without linked cards
304
- **(C) Analyze progress** — Review what's done vs remaining
305
- **(D) Skip**
306
-
307
- ### 2A.4 — Execute Choice
308
-
309
- #### Option A — Single card
310
- 1. Call \`harmony_create_card\` with:
311
- - \`title\`: Plan title
312
- - \`description\`: Brief 2-3 sentence summary of the plan + \`\\n\\n[View plan](https://app.gethmy.com/plans/{planId})\`
313
- - \`priority\`: based on plan task priorities (use highest)
314
- 2. Call \`harmony_update_plan\` to set \`status: "active"\`, \`workflowPhase: "execute"\`
315
-
316
- #### Option B — Multiple cards (advance plan)
317
- 1. Call \`harmony_advance_plan\` with:
318
- - \`planId\`: the plan ID
319
- - \`phase\`: \`"execute"\`
320
- - \`summary\`: Brief summary of the plan scope
321
- 2. Display the created cards with their short IDs
322
-
323
- #### Option C — Analyze only
324
- 1. Present a structured implementation analysis:
325
- - Suggested order of tasks
326
- - Dependencies between tasks
327
- - Key technical considerations from memory search
328
- - Estimated complexity
329
- 2. Suggest creating cards when ready
330
-
331
- #### Option D — Skip
332
- Acknowledge and stop.
333
-
334
- ### 2A.5 — Summary
335
-
336
- After execution, show:
337
- - Created card(s) with short IDs
338
- - Current plan phase
339
- - Suggest next step: \`/hmy #N\` to start working on a card
340
-
341
- ---
342
-
343
- ## Step 2B — Create New Plan
344
-
345
- ### 2B.1 — Context Gathering
346
-
347
- Before interviewing, gather existing context:
348
-
349
- 1. Call \`harmony_get_board\` for board state (columns, cards, labels)
350
- 2. Call \`harmony_recall\` with relevant tags to find existing knowledge on the topic
351
- 3. Call \`harmony_memory_search\` with the topic to find related patterns/decisions/lessons
352
-
353
- Note what you learn — reference it during the interview to ask smarter questions.
354
-
355
- ### 2B.2 — Structured Interview (3-5 questions)
356
-
357
- Interview the user with **3-5 focused questions** using AskUserQuestion. Adapt based on complexity.
358
-
359
- #### Core Questions
360
- 1. **Problem & Audience**: What problem are we solving? Who benefits?
361
- 2. **Scope**: What's in v1, what's deferred?
362
- 3. **Technical Constraints**: Any technical preferences, constraints, or existing patterns to follow?
363
-
364
- #### Adaptive Follow-ups (if needed)
365
- - Key user flows or interactions?
366
- - Integration points with existing systems?
367
- - Performance, security, or accessibility requirements?
368
-
369
- #### Interview Rules
370
- - Reference what you found in Step 2B.1 (board state, memories) to ask informed questions
371
- - Skip questions that aren't relevant — simple features need fewer questions
372
- - Tell the user when you have enough information to draft
373
-
374
- ### 2B.3 — Plan Document
375
-
376
- Create a structured markdown document:
377
-
378
- \\\`\\\`\\\`markdown
379
- # [Title]
380
-
381
- ## Problem
382
- [What problem we're solving and why]
383
-
384
- ## Scope
385
-
386
- ### In Scope
387
- - [Feature/capability 1]
388
- - [Feature/capability 2]
389
-
390
- ### Out of Scope
391
- - [Deferred items]
392
-
393
- ## Approach
394
- [Technical design, architecture decisions, key patterns]
395
-
396
- ### Key Decisions
397
- | Decision | Choice | Rationale |
398
- |----------|--------|-----------|
399
- | ... | ... | ... |
400
-
401
- ## Tasks
402
- 1. **[Task title]** — priority: high
403
- [Brief description]
404
-
405
- 2. **[Task title]** — priority: medium
406
- [Brief description]
407
-
408
- [Continue for all tasks...]
409
-
410
- ## Risks & Mitigations
411
- | Risk | Mitigation |
412
- |------|------------|
413
- | ... | ... |
414
-
415
- ## Success Criteria
416
- - [ ] [Measurable criterion]
417
- \\\`\\\`\\\`
418
-
419
- ### 2B.4 — Create Plan
420
-
421
- Call \`harmony_create_plan\` with:
422
- - \`title\`: The plan title
423
- - \`content\`: Full markdown document from Step 2B.3
424
- - \`source\`: \`"agent"\`
425
- - \`workflowPhase\`: \`"plan"\`
426
- - \`tasks\`: Array of tasks from the Tasks section, each with:
427
- - \`content\`: Task title + brief description
428
- - \`priority\`: \`"high"\`, \`"medium"\`, or \`"low"\`
429
- - \`status\`: \`"pending"\`
430
-
431
- ### 2B.5 — User Approval
432
-
433
- Show the user:
434
- - Plan URL: \`https://app.gethmy.com/plans/{id}\`
435
- - Number of tasks created
436
- - Brief summary
437
-
438
- Then ask: **"Ready to execute? I'll create board cards from these tasks and start the execution phase."**
439
-
440
- Options:
441
- 1. **Yes, advance to Execute** — proceed to Step 2B.6
442
- 2. **Let me review first** — end here, they can advance later from the UI
443
- 3. **Make changes** — iterate on the plan
444
-
445
- ### 2B.6 — Advance to Execute
446
-
447
- Call \`harmony_advance_plan\` with:
448
- - \`planId\`: The plan ID from Step 2B.4
449
- - \`phase\`: \`"execute"\`
450
- - \`summary\`: A 2-3 sentence summary of the key decisions made during planning
451
-
452
- Report the result:
453
- - "Created N cards in 'To Do'. Use \`/hmy #<id>\` to start working on any card."
454
- - List the created cards with their short IDs
455
-
456
- ## Key Tools Reference
457
-
458
- **Discovery:** \`harmony_list_plans\`, \`harmony_get_plan\`, \`harmony_get_card_by_short_id\`
459
- **Context:** \`harmony_get_board\`, \`harmony_recall\`, \`harmony_memory_search\`
460
- **Planning:** \`harmony_create_plan\`, \`harmony_advance_plan\`, \`harmony_update_plan\`
461
- **Execution:** \`harmony_create_card\`, \`harmony_update_card\`
462
- `;
463
-
464
- export interface SkillDefinition {
94
+ export interface FetchedSkill {
465
95
  name: string;
466
- description: string;
467
- argumentHint: string;
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;
468
100
  content: string;
469
101
  }
470
102
 
471
- export const SKILL_DEFINITIONS: Record<string, SkillDefinition> = {
472
- hmy: {
473
- name: "hmy",
474
- description:
475
- "Work with Harmony cards — create, view, update, or start working on them. Use when given a card reference like #42, or commands like create, move, update.",
476
- argumentHint: "<command or card-reference>",
477
- content: HMY_SKILL_CONTENT,
478
- },
479
- "hmy-plan": {
480
- name: "hmy-plan",
481
- description:
482
- "Create a new plan or work on an existing one. Use when asked to plan a feature, execute a plan, review a plan, or given a plan reference.",
483
- argumentHint: "[plan name, ID, or topic to plan]",
484
- content: HMY_PLAN_CONTENT,
485
- },
486
- };
487
-
488
103
  /**
489
- * Build a complete skill file with frontmatter and version marker.
490
- * Optionally substitutes agent identifier/name for agent-specific builds.
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.
491
113
  */
492
- export function buildSkillFile(skillId: string, agentId?: string): string {
493
- const skill = SKILL_DEFINITIONS[skillId];
494
- if (!skill) {
495
- throw new Error(`Unknown skill: ${skillId}`);
496
- }
497
-
498
- let content = skill.content;
499
-
500
- // Apply agent-specific substitutions
501
- if (agentId === "claude") {
502
- content = content
503
- .replace("Your agent identifier", "claude-code")
504
- .replace("Your agent name", "Claude Code");
114
+ export function buildSkillFile(skill: FetchedSkill): string {
115
+ if (skill.skillVersion !== undefined && !hasMetadataVersion(skill.content)) {
116
+ return injectMetadataVersion(skill.content, skill.skillVersion);
505
117
  }
118
+ return skill.content;
119
+ }
506
120
 
507
- const frontmatter = `---
508
- name: ${skill.name}
509
- description: ${skill.description}
510
- argument-hint: ${skill.argumentHint}
511
- ---`;
121
+ function hasMetadataVersion(content: string): boolean {
122
+ return parseSkillVersion(content) !== null;
123
+ }
512
124
 
513
- return `${frontmatter}\n\n${content}\n${VERSION_MARKER_PREFIX}${SKILLS_VERSION} -->`;
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}`;
514
136
  }
515
137
 
516
138
  /**
517
- * Parse the skills version from a skill file's content.
518
- * Returns null if no version marker is found.
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".
519
144
  */
520
145
  function parseSkillVersion(content: string): string | null {
521
- const match = content.match(/<!-- skills-version:(\d+) -->/);
522
- 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;
523
159
  }
524
160
 
525
161
  /**
526
- * Find all skill files in a given directory (global or local).
527
- * Returns an array of { skillId, filePath } for each found skill.
162
+ * Locate already-installed skill files under one of the standard paths.
163
+ * Matches the install layout produced by tui/setup.ts:
164
+ * ~/.agents/skills/<name>/SKILL.md (global)
165
+ * <cwd>/.claude/skills/<name>/SKILL.md (local)
528
166
  */
529
167
  function findSkillFiles(
530
168
  paths: string[],
531
- ): { skillId: string; filePath: string }[] {
532
- const results: { skillId: string; filePath: string }[] = [];
533
-
169
+ knownNames: string[],
170
+ ): { name: string; filePath: string }[] {
171
+ const results: { name: string; filePath: string }[] = [];
534
172
  for (const filePath of paths) {
535
173
  if (!existsSync(filePath)) continue;
536
-
537
- // Determine skill ID from path
538
- for (const skillId of Object.keys(SKILL_DEFINITIONS)) {
539
- if (
540
- filePath.includes(`/${skillId}/`) ||
541
- filePath.includes(`/${skillId}.md`)
542
- ) {
543
- results.push({ skillId, filePath });
174
+ for (const name of knownNames) {
175
+ if (filePath.includes(`/${name}/`) || filePath.includes(`/${name}.md`)) {
176
+ results.push({ name, filePath });
544
177
  break;
545
178
  }
546
179
  }
547
180
  }
548
-
549
181
  return results;
550
182
  }
551
183
 
552
184
  /**
553
- * Silently refresh installed skill files if they are outdated.
554
- * Called at MCP server startup. Non-blocking — errors are caught and logged.
185
+ * Silently refresh installed skill files if remote versions are newer.
186
+ * Called at MCP server startup. Non-blocking — any error (offline, 401,
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.
555
194
  */
556
195
  export async function refreshSkills(): Promise<void> {
557
196
  try {
197
+ if (!isConfigured()) return;
558
198
  const status = areSkillsInstalled();
199
+ if (!status.installed) return;
559
200
 
560
- if (!status.installed) {
561
- // User hasn't run setup yet — don't install skills
562
- return;
563
- }
201
+ const client = new HarmonyApiClient();
202
+ const versionInfo = await client.fetchSkillsVersion();
564
203
 
565
- // Find skill files from the detected paths
566
- const skillFiles = findSkillFiles(status.paths);
204
+ const skillFiles = findSkillFiles(status.paths, versionInfo.skills);
567
205
 
568
- // Also check for sibling skills in the same directory
569
- // e.g., if hmy is installed at ~/.agents/skills/hmy/SKILL.md,
570
- // check for hmy-plan at ~/.agents/skills/hmy-plan/SKILL.md
206
+ // Also pick up sibling skills next to the ones we found (e.g., if `hmy`
207
+ // is installed but `hmy-cleanup` isn't yet, install it during refresh).
571
208
  if (skillFiles.length > 0) {
572
209
  const samplePath = skillFiles[0].filePath;
573
-
574
- for (const skillId of Object.keys(SKILL_DEFINITIONS)) {
575
- const alreadyFound = skillFiles.some((sf) => sf.skillId === skillId);
576
- if (alreadyFound) continue;
577
-
578
- // Try to find sibling skill file
210
+ for (const name of versionInfo.skills) {
211
+ if (skillFiles.some((sf) => sf.name === name)) continue;
579
212
  let siblingPath: string;
580
213
  if (samplePath.endsWith("SKILL.md")) {
581
- // ~/.agents/skills/hmy/SKILL.md -> ~/.agents/skills/hmy-plan/SKILL.md
582
214
  const parentDir = dirname(dirname(samplePath));
583
- siblingPath = `${parentDir}/${skillId}/SKILL.md`;
215
+ siblingPath = `${parentDir}/${name}/SKILL.md`;
584
216
  } else {
585
- // ~/.claude/skills/hmy.md -> ~/.claude/skills/hmy-plan.md
586
217
  const parentDir = dirname(samplePath);
587
- siblingPath = `${parentDir}/${skillId}.md`;
218
+ siblingPath = `${parentDir}/${name}.md`;
588
219
  }
589
-
590
220
  if (existsSync(siblingPath)) {
591
- skillFiles.push({ skillId, filePath: siblingPath });
221
+ skillFiles.push({ name, filePath: siblingPath });
592
222
  }
593
223
  }
594
224
  }
595
-
596
- if (skillFiles.length === 0) {
597
- return;
598
- }
225
+ if (skillFiles.length === 0) return;
599
226
 
600
227
  let updated = false;
601
-
602
- for (const { skillId, filePath } of skillFiles) {
228
+ for (const { name, filePath } of skillFiles) {
603
229
  try {
604
230
  const currentContent = readFileSync(filePath, "utf-8");
605
- const currentVersion = parseSkillVersion(currentContent);
231
+ const localVersion = parseSkillVersion(currentContent);
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;
606
237
 
607
- // Update if no version marker or version is older
608
238
  if (
609
- currentVersion === null ||
610
- Number(currentVersion) < Number(SKILLS_VERSION)
239
+ remoteVersion !== undefined &&
240
+ localVersion !== null &&
241
+ Number(localVersion) >= remoteVersion
611
242
  ) {
612
- const newContent = buildSkillFile(skillId, "claude");
613
- const dir = dirname(filePath);
614
- if (!existsSync(dir)) {
615
- mkdirSync(dir, { recursive: true });
616
- }
617
- writeFileSync(filePath, newContent);
618
- updated = true;
243
+ continue;
619
244
  }
620
- } catch {
621
- // Silently skip files we can't read/write
245
+
246
+ const newContent = buildSkillFile(fetched);
247
+ const dir = dirname(filePath);
248
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
249
+ writeFileSync(filePath, newContent);
250
+ updated = true;
251
+ } catch (err) {
252
+ const msg = err instanceof Error ? err.message : String(err);
253
+ console.error(`Harmony: skill "${name}" refresh failed: ${msg}`);
622
254
  }
623
255
  }
624
-
625
256
  if (updated) {
626
- console.error(`Harmony: Updated skills to v${SKILLS_VERSION}`);
257
+ console.error("Harmony: Refreshed skills from server");
627
258
  }
628
259
  } catch {
629
- // Non-blocking — if anything fails, continue silently
260
+ // Non-blocking — offline / 401 / partial install all fall through here.
630
261
  }
631
262
  }