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.
- package/README.md +115 -140
- package/dist/cli.js +21398 -17412
- package/dist/index.js +19903 -15767
- package/dist/init.js +183 -84
- 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
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
+
"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
|
|
41
|
-
"build:bun": "bun build src/index.ts src/http.ts src/cli.ts
|
|
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.
|
|
48
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
49
|
-
"commander": "^
|
|
50
|
-
"hono": "^4.
|
|
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.
|
|
52
|
+
"zod": "^4.3.6"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@types/node": "^
|
|
56
|
-
"typescript": "^5.
|
|
55
|
+
"@types/node": "^25.0.10",
|
|
56
|
+
"typescript": "^5.9.3"
|
|
57
57
|
}
|
|
58
58
|
}
|