harmony-mcp 1.3.3 → 1.5.1

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.
Files changed (5) hide show
  1. package/README.md +115 -140
  2. package/dist/cli.js +21398 -17412
  3. package/dist/index.js +19903 -15767
  4. package/dist/init.js +183 -84
  5. package/package.json +10 -10
package/dist/init.js CHANGED
@@ -28,7 +28,7 @@ var __export = (target, all) => {
28
28
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
29
29
 
30
30
  // src/init.ts
31
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
31
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, symlinkSync, unlinkSync, lstatSync } from "node:fs";
32
32
  import { join, dirname } from "node:path";
33
33
  import { homedir } from "node:os";
34
34
  var HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
@@ -207,6 +207,28 @@ function writeFileIfNotExists(filePath, content, force) {
207
207
  writeFileSync(filePath, content, "utf-8");
208
208
  return { created: true, skipped: false };
209
209
  }
210
+ var GLOBAL_SKILLS_DIR = join(homedir(), ".agents", "skills");
211
+ function pathExists(filePath) {
212
+ try {
213
+ lstatSync(filePath);
214
+ return true;
215
+ } catch {
216
+ return false;
217
+ }
218
+ }
219
+ function createSymlink(target, linkPath, force) {
220
+ if (pathExists(linkPath)) {
221
+ if (!force) {
222
+ return { created: false, skipped: true };
223
+ }
224
+ try {
225
+ unlinkSync(linkPath);
226
+ } catch {}
227
+ }
228
+ ensureDir(dirname(linkPath));
229
+ symlinkSync(target, linkPath);
230
+ return { created: true, skipped: false };
231
+ }
210
232
  function mergeJsonFile(filePath, updates, force) {
211
233
  if (!existsSync(filePath)) {
212
234
  ensureDir(dirname(filePath));
@@ -233,49 +255,76 @@ function mergeJsonFile(filePath, updates, force) {
233
255
  return { created: false, skipped: true, merged: false };
234
256
  }
235
257
  }
236
- function initClaude(cwd, force) {
258
+ function initClaude(cwd, force, installMode) {
237
259
  const result = {
238
260
  agent: "claude",
239
261
  filesCreated: [],
240
- filesSkipped: []
262
+ filesSkipped: [],
263
+ symlinksCreated: []
241
264
  };
242
- const commandContent = `---
243
- description: Start working on a Harmony card (moves to In Progress, adds agent label)
265
+ const skillContent = `---
266
+ name: hmy
267
+ description: Start working on a Harmony card. Use when given a card reference like #42, UUID, or card name to implement.
244
268
  argument-hint: <card-reference>
245
269
  ---
246
270
 
247
271
  ${HARMONY_WORKFLOW_PROMPT.replace("Your agent identifier", "claude-code").replace("Your agent name", "Claude Code")}
248
272
  `;
249
- const commandPath = join(cwd, ".claude", "commands", "hmy.md");
250
- const { created, skipped } = writeFileIfNotExists(commandPath, commandContent, force);
251
- if (created)
252
- result.filesCreated.push(commandPath);
253
- if (skipped)
254
- result.filesSkipped.push(commandPath);
255
273
  const standupContent = `---
256
- description: Generate a daily standup summary for the current Harmony project
274
+ name: hmy-standup
275
+ description: Generate a daily standup summary. Use when asked for project status, daily update, or standup report.
257
276
  ---
258
277
 
259
278
  ${HARMONY_STANDUP_PROMPT}
260
279
  `;
261
- const standupPath = join(cwd, ".claude", "commands", "hmy-standup.md");
262
- const standupResult = writeFileIfNotExists(standupPath, standupContent, force);
263
- if (standupResult.created)
264
- result.filesCreated.push(standupPath);
265
- if (standupResult.skipped)
266
- result.filesSkipped.push(standupPath);
267
280
  const cleanupContent = `---
268
- description: Analyze the Harmony board and suggest cleanup actions for stale cards
281
+ name: hmy-cleanup
282
+ description: Analyze board for stale cards and suggest cleanup. Use when asked to clean up, audit, or organize the board.
269
283
  ---
270
284
 
271
285
  ${HARMONY_CLEANUP_PROMPT}
272
286
  `;
273
- const cleanupPath = join(cwd, ".claude", "commands", "hmy-cleanup.md");
274
- const cleanupResult = writeFileIfNotExists(cleanupPath, cleanupContent, force);
275
- if (cleanupResult.created)
276
- result.filesCreated.push(cleanupPath);
277
- if (cleanupResult.skipped)
278
- result.filesSkipped.push(cleanupPath);
287
+ if (installMode === "global") {
288
+ const skills = [
289
+ { name: "hmy", content: skillContent },
290
+ { name: "hmy-standup", content: standupContent },
291
+ { name: "hmy-cleanup", content: cleanupContent }
292
+ ];
293
+ for (const skill of skills) {
294
+ const centralPath = join(GLOBAL_SKILLS_DIR, skill.name, "SKILL.md");
295
+ const { created, skipped } = writeFileIfNotExists(centralPath, skill.content, force);
296
+ if (created)
297
+ result.filesCreated.push(centralPath);
298
+ if (skipped)
299
+ result.filesSkipped.push(centralPath);
300
+ const symlinkPath = join(homedir(), ".claude", "skills", skill.name);
301
+ const symlinkTarget = join(GLOBAL_SKILLS_DIR, skill.name);
302
+ const symlinkResult = createSymlink(symlinkTarget, symlinkPath, force);
303
+ if (symlinkResult.created)
304
+ result.symlinksCreated.push(`${symlinkPath} → ${symlinkTarget}`);
305
+ if (symlinkResult.skipped)
306
+ result.filesSkipped.push(symlinkPath);
307
+ }
308
+ } else {
309
+ const skillPath = join(cwd, ".claude", "skills", "hmy", "SKILL.md");
310
+ const { created, skipped } = writeFileIfNotExists(skillPath, skillContent, force);
311
+ if (created)
312
+ result.filesCreated.push(skillPath);
313
+ if (skipped)
314
+ result.filesSkipped.push(skillPath);
315
+ const standupPath = join(cwd, ".claude", "skills", "hmy-standup", "SKILL.md");
316
+ const standupResult = writeFileIfNotExists(standupPath, standupContent, force);
317
+ if (standupResult.created)
318
+ result.filesCreated.push(standupPath);
319
+ if (standupResult.skipped)
320
+ result.filesSkipped.push(standupPath);
321
+ const cleanupPath = join(cwd, ".claude", "skills", "hmy-cleanup", "SKILL.md");
322
+ const cleanupResult = writeFileIfNotExists(cleanupPath, cleanupContent, force);
323
+ if (cleanupResult.created)
324
+ result.filesCreated.push(cleanupPath);
325
+ if (cleanupResult.skipped)
326
+ result.filesSkipped.push(cleanupPath);
327
+ }
279
328
  const globalConfigPath = join(homedir(), ".claude", "settings.json");
280
329
  const mcpConfig = {
281
330
  mcpServers: {
@@ -293,11 +342,12 @@ ${HARMONY_CLEANUP_PROMPT}
293
342
  }
294
343
  return result;
295
344
  }
296
- function initCodex(cwd, force) {
345
+ function initCodex(cwd, force, installMode) {
297
346
  const result = {
298
347
  agent: "codex",
299
348
  filesCreated: [],
300
- filesSkipped: []
349
+ filesSkipped: [],
350
+ symlinksCreated: []
301
351
  };
302
352
  const agentsContent = `# Harmony Integration
303
353
 
@@ -325,12 +375,6 @@ When given a card reference (e.g., #42 or a card name), follow this workflow:
325
375
  - \`harmony_get_board\` - Get board state
326
376
  - \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
327
377
  `;
328
- const agentsPath = join(cwd, "AGENTS.md");
329
- const { created: agentsCreated, skipped: agentsSkipped } = writeFileIfNotExists(agentsPath, agentsContent, force);
330
- if (agentsCreated)
331
- result.filesCreated.push(agentsPath);
332
- if (agentsSkipped)
333
- result.filesSkipped.push(agentsPath);
334
378
  const promptContent = `---
335
379
  name: hmy
336
380
  description: Start working on a Harmony card
@@ -342,13 +386,33 @@ arguments:
342
386
 
343
387
  ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "{{card}}").replace("Your agent identifier", "codex").replace("Your agent name", "OpenAI Codex")}
344
388
  `;
345
- const promptsDir = join(homedir(), ".codex", "prompts");
346
- const promptPath = join(promptsDir, "hmy.md");
347
- const { created: promptCreated, skipped: promptSkipped } = writeFileIfNotExists(promptPath, promptContent, force);
348
- if (promptCreated)
349
- result.filesCreated.push(promptPath);
350
- if (promptSkipped)
351
- result.filesSkipped.push(promptPath);
389
+ if (installMode === "global") {
390
+ const centralPromptPath = join(GLOBAL_SKILLS_DIR, "codex", "hmy.md");
391
+ const { created, skipped } = writeFileIfNotExists(centralPromptPath, promptContent, force);
392
+ if (created)
393
+ result.filesCreated.push(centralPromptPath);
394
+ if (skipped)
395
+ result.filesSkipped.push(centralPromptPath);
396
+ const symlinkPath = join(homedir(), ".codex", "prompts", "hmy.md");
397
+ const symlinkResult = createSymlink(centralPromptPath, symlinkPath, force);
398
+ if (symlinkResult.created)
399
+ result.symlinksCreated.push(`${symlinkPath} → ${centralPromptPath}`);
400
+ if (symlinkResult.skipped)
401
+ result.filesSkipped.push(symlinkPath);
402
+ } else {
403
+ const promptPath = join(homedir(), ".codex", "prompts", "hmy.md");
404
+ const { created, skipped } = writeFileIfNotExists(promptPath, promptContent, force);
405
+ if (created)
406
+ result.filesCreated.push(promptPath);
407
+ if (skipped)
408
+ result.filesSkipped.push(promptPath);
409
+ }
410
+ const agentsPath = join(cwd, "AGENTS.md");
411
+ const { created: agentsCreated, skipped: agentsSkipped } = writeFileIfNotExists(agentsPath, agentsContent, force);
412
+ if (agentsCreated)
413
+ result.filesCreated.push(agentsPath);
414
+ if (agentsSkipped)
415
+ result.filesSkipped.push(agentsPath);
352
416
  const configPath = join(homedir(), ".codex", "config.toml");
353
417
  const mcpConfigSection = `
354
418
  # Harmony MCP Server
@@ -378,12 +442,26 @@ args = ["serve"]
378
442
  }
379
443
  return result;
380
444
  }
381
- function initCursor(cwd, force) {
445
+ function initCursor(cwd, force, installMode) {
382
446
  const result = {
383
447
  agent: "cursor",
384
448
  filesCreated: [],
385
- filesSkipped: []
449
+ filesSkipped: [],
450
+ symlinksCreated: []
386
451
  };
452
+ const ruleContent = `---
453
+ description: Harmony card workflow rule
454
+ globs:
455
+ - "**/*"
456
+ alwaysApply: false
457
+ ---
458
+
459
+ # Harmony Integration
460
+
461
+ When the user asks you to work on a Harmony card (references like #42, card names, or UUIDs):
462
+
463
+ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "cursor").replace("Your agent name", "Cursor AI")}
464
+ `;
387
465
  const mcpConfigPath = join(cwd, ".cursor", "mcp.json");
388
466
  const mcpConfig = {
389
467
  mcpServers: {
@@ -399,33 +477,47 @@ function initCursor(cwd, force) {
399
477
  } else if (mcpResult.skipped) {
400
478
  result.filesSkipped.push(mcpConfigPath);
401
479
  }
402
- const ruleContent = `---
403
- description: Harmony card workflow rule
404
- globs:
405
- - "**/*"
406
- alwaysApply: false
407
- ---
408
-
409
- # Harmony Integration
410
-
411
- When the user asks you to work on a Harmony card (references like #42, card names, or UUIDs):
412
-
413
- ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "cursor").replace("Your agent name", "Cursor AI")}
414
- `;
415
- const rulePath = join(cwd, ".cursor", "rules", "harmony.mdc");
416
- const { created: ruleCreated, skipped: ruleSkipped } = writeFileIfNotExists(rulePath, ruleContent, force);
417
- if (ruleCreated)
418
- result.filesCreated.push(rulePath);
419
- if (ruleSkipped)
420
- result.filesSkipped.push(rulePath);
480
+ if (installMode === "global") {
481
+ const centralRulePath = join(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc");
482
+ const { created, skipped } = writeFileIfNotExists(centralRulePath, ruleContent, force);
483
+ if (created)
484
+ result.filesCreated.push(centralRulePath);
485
+ if (skipped)
486
+ result.filesSkipped.push(centralRulePath);
487
+ const symlinkPath = join(homedir(), ".cursor", "rules", "harmony.mdc");
488
+ const symlinkResult = createSymlink(centralRulePath, symlinkPath, force);
489
+ if (symlinkResult.created)
490
+ result.symlinksCreated.push(`${symlinkPath} → ${centralRulePath}`);
491
+ if (symlinkResult.skipped)
492
+ result.filesSkipped.push(symlinkPath);
493
+ } else {
494
+ const rulePath = join(cwd, ".cursor", "rules", "harmony.mdc");
495
+ const { created, skipped } = writeFileIfNotExists(rulePath, ruleContent, force);
496
+ if (created)
497
+ result.filesCreated.push(rulePath);
498
+ if (skipped)
499
+ result.filesSkipped.push(rulePath);
500
+ }
421
501
  return result;
422
502
  }
423
- function initWindsurf(cwd, force) {
503
+ function initWindsurf(cwd, force, installMode) {
424
504
  const result = {
425
505
  agent: "windsurf",
426
506
  filesCreated: [],
427
- filesSkipped: []
507
+ filesSkipped: [],
508
+ symlinksCreated: []
428
509
  };
510
+ const ruleContent = `---
511
+ trigger: model_decision
512
+ description: Activate when user asks to work on a Harmony card (references like #42, card names, or task management)
513
+ ---
514
+
515
+ # Harmony Card Workflow
516
+
517
+ When working on a Harmony card:
518
+
519
+ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "windsurf").replace("Your agent name", "Windsurf AI")}
520
+ `;
429
521
  const globalMcpPath = join(homedir(), ".codeium", "windsurf", "mcp_config.json");
430
522
  const mcpConfig = {
431
523
  mcpServers: {
@@ -443,50 +535,56 @@ function initWindsurf(cwd, force) {
443
535
  } else if (mcpResult.skipped) {
444
536
  result.filesSkipped.push(globalMcpPath);
445
537
  }
446
- const ruleContent = `---
447
- trigger: model_decision
448
- description: Activate when user asks to work on a Harmony card (references like #42, card names, or task management)
449
- ---
450
-
451
- # Harmony Card Workflow
452
-
453
- When working on a Harmony card:
454
-
455
- ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "windsurf").replace("Your agent name", "Windsurf AI")}
456
- `;
457
- const rulePath = join(cwd, ".windsurf", "rules", "harmony.md");
458
- const { created: ruleCreated, skipped: ruleSkipped } = writeFileIfNotExists(rulePath, ruleContent, force);
459
- if (ruleCreated)
460
- result.filesCreated.push(rulePath);
461
- if (ruleSkipped)
462
- result.filesSkipped.push(rulePath);
538
+ if (installMode === "global") {
539
+ const centralRulePath = join(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md");
540
+ const { created, skipped } = writeFileIfNotExists(centralRulePath, ruleContent, force);
541
+ if (created)
542
+ result.filesCreated.push(centralRulePath);
543
+ if (skipped)
544
+ result.filesSkipped.push(centralRulePath);
545
+ const symlinkPath = join(homedir(), ".codeium", "windsurf", "rules", "harmony.md");
546
+ const symlinkResult = createSymlink(centralRulePath, symlinkPath, force);
547
+ if (symlinkResult.created)
548
+ result.symlinksCreated.push(`${symlinkPath} → ${centralRulePath}`);
549
+ if (symlinkResult.skipped)
550
+ result.filesSkipped.push(symlinkPath);
551
+ } else {
552
+ const rulePath = join(cwd, ".windsurf", "rules", "harmony.md");
553
+ const { created, skipped } = writeFileIfNotExists(rulePath, ruleContent, force);
554
+ if (created)
555
+ result.filesCreated.push(rulePath);
556
+ if (skipped)
557
+ result.filesSkipped.push(rulePath);
558
+ }
463
559
  return result;
464
560
  }
465
561
  function initHarmony(options = {}) {
466
562
  const cwd = options.cwd || process.cwd();
467
563
  const force = options.force || false;
468
564
  const agents = options.agents || ["claude", "codex", "cursor", "windsurf"];
565
+ const installMode = options.installMode || "local";
469
566
  const results = [];
470
567
  for (const agent of agents) {
471
568
  try {
472
569
  switch (agent) {
473
570
  case "claude":
474
- results.push(initClaude(cwd, force));
571
+ results.push(initClaude(cwd, force, installMode));
475
572
  break;
476
573
  case "codex":
477
- results.push(initCodex(cwd, force));
574
+ results.push(initCodex(cwd, force, installMode));
478
575
  break;
479
576
  case "cursor":
480
- results.push(initCursor(cwd, force));
577
+ results.push(initCursor(cwd, force, installMode));
481
578
  break;
482
579
  case "windsurf":
483
- results.push(initWindsurf(cwd, force));
580
+ results.push(initWindsurf(cwd, force, installMode));
484
581
  break;
485
582
  default:
486
583
  results.push({
487
584
  agent,
488
585
  filesCreated: [],
489
586
  filesSkipped: [],
587
+ symlinksCreated: [],
490
588
  error: `Unknown agent: ${agent}`
491
589
  });
492
590
  }
@@ -495,6 +593,7 @@ function initHarmony(options = {}) {
495
593
  agent,
496
594
  filesCreated: [],
497
595
  filesSkipped: [],
596
+ symlinksCreated: [],
498
597
  error: error instanceof Error ? error.message : String(error)
499
598
  });
500
599
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harmony-mcp",
3
- "version": "1.3.3",
3
+ "version": "1.5.1",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,22 +37,22 @@
37
37
  "node": ">=18.0.0"
38
38
  },
39
39
  "scripts": {
40
- "build": "bun build src/index.ts src/cli.ts src/init.ts --outdir dist --target node",
41
- "build:bun": "bun build src/index.ts src/http.ts src/cli.ts src/init.ts --outdir dist --target bun",
40
+ "build": "bun build src/index.ts src/cli.ts --outdir dist --target node",
41
+ "build:bun": "bun build src/index.ts src/http.ts src/cli.ts --outdir dist --target bun",
42
42
  "dev": "bun --watch src/index.ts",
43
43
  "typecheck": "tsc --noEmit",
44
44
  "prepublishOnly": "bun run build"
45
45
  },
46
46
  "dependencies": {
47
- "@clack/prompts": "^0.9.1",
48
- "@modelcontextprotocol/sdk": "^1.0.0",
49
- "commander": "^12.0.0",
50
- "hono": "^4.0.0",
47
+ "@clack/prompts": "^0.11.0",
48
+ "@modelcontextprotocol/sdk": "^1.25.3",
49
+ "commander": "^14.0.2",
50
+ "hono": "^4.11.5",
51
51
  "picocolors": "^1.1.1",
52
- "zod": "^3.23.0"
52
+ "zod": "^4.3.6"
53
53
  },
54
54
  "devDependencies": {
55
- "@types/node": "^22.0.0",
56
- "typescript": "^5.6.0"
55
+ "@types/node": "^25.0.10",
56
+ "typescript": "^5.9.3"
57
57
  }
58
58
  }