harmony-mcp 1.3.2 → 1.5.0

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 +24 -25
  2. package/dist/cli.js +22768 -17253
  3. package/dist/index.js +19930 -15858
  4. package/dist/init.js +281 -70
  5. package/package.json +11 -9
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
@@ -106,6 +106,94 @@ If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
106
106
 
107
107
  **AI:** \`harmony_generate_prompt\`, \`harmony_process_command\`
108
108
  `;
109
+ var HARMONY_STANDUP_PROMPT = `# Harmony Daily Standup
110
+
111
+ Generate a daily standup summary for the current project.
112
+
113
+ ## 1. Get Board State
114
+
115
+ Call \`harmony_get_board\` to get the full board state including:
116
+ - All columns and their cards
117
+ - Card priorities, assignees, and due dates
118
+ - Active agent sessions
119
+
120
+ ## 2. Analyze Board
121
+
122
+ Organize the information into standup categories:
123
+
124
+ ### What was completed recently
125
+ - Cards in "Done" or "Review" columns
126
+ - Cards with recent activity (moved, updated)
127
+
128
+ ### What's in progress
129
+ - Cards in "In Progress" column
130
+ - Cards with active agent sessions (show progress %)
131
+ - Who's working on what
132
+
133
+ ### What's blocked or at risk
134
+ - High-priority cards not in progress
135
+ - Overdue cards
136
+ - Cards with blockers
137
+
138
+ ### What's coming up
139
+ - Cards in "To Do" column
140
+ - Upcoming due dates
141
+
142
+ ## 3. Present Summary
143
+
144
+ Format the summary as a clean, readable standup report:
145
+ - Use bullet points for easy scanning
146
+ - Highlight priorities and blockers
147
+ - Include card short IDs for easy reference (e.g., #42)
148
+ - Note any agent work in progress
149
+ `;
150
+ var HARMONY_CLEANUP_PROMPT = `# Harmony Board Cleanup
151
+
152
+ Analyze the board and suggest cleanup actions.
153
+
154
+ ## 1. Get Board State
155
+
156
+ Call \`harmony_get_board\` to get the full board state.
157
+
158
+ ## 2. Identify Issues
159
+
160
+ Look for:
161
+
162
+ ### Stale cards
163
+ - Cards in "In Progress" for too long without updates
164
+ - Cards with past due dates
165
+ - Cards with no recent activity
166
+
167
+ ### Organizational issues
168
+ - Cards missing priorities
169
+ - Cards missing assignees in active columns
170
+ - Empty descriptions on complex cards
171
+
172
+ ### Potential duplicates
173
+ - Cards with similar titles
174
+ - Use \`harmony_search_cards\` if needed to find related cards
175
+
176
+ ## 3. Suggest Actions
177
+
178
+ For each issue found, suggest a specific action:
179
+ - Move stale cards back to backlog
180
+ - Archive completed cards
181
+ - Update missing information
182
+ - Merge or link duplicates
183
+
184
+ Present suggestions as a prioritized list with:
185
+ - Card reference (#ID)
186
+ - Current state
187
+ - Suggested action
188
+ - Why it matters
189
+
190
+ ## 4. Optional: Execute Cleanup
191
+
192
+ If the user approves, execute the suggested actions:
193
+ - Use \`harmony_move_card\` to reorganize
194
+ - Use \`harmony_update_card\` to add missing info
195
+ - Use \`harmony_add_link_to_card\` to link related cards
196
+ `;
109
197
  function ensureDir(dirPath) {
110
198
  if (!existsSync(dirPath)) {
111
199
  mkdirSync(dirPath, { recursive: true });
@@ -119,6 +207,28 @@ function writeFileIfNotExists(filePath, content, force) {
119
207
  writeFileSync(filePath, content, "utf-8");
120
208
  return { created: true, skipped: false };
121
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
+ }
122
232
  function mergeJsonFile(filePath, updates, force) {
123
233
  if (!existsSync(filePath)) {
124
234
  ensureDir(dirname(filePath));
@@ -145,25 +255,76 @@ function mergeJsonFile(filePath, updates, force) {
145
255
  return { created: false, skipped: true, merged: false };
146
256
  }
147
257
  }
148
- function initClaude(cwd, force) {
258
+ function initClaude(cwd, force, installMode) {
149
259
  const result = {
150
260
  agent: "claude",
151
261
  filesCreated: [],
152
- filesSkipped: []
262
+ filesSkipped: [],
263
+ symlinksCreated: []
153
264
  };
154
- const commandContent = `---
155
- 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.
156
268
  argument-hint: <card-reference>
157
269
  ---
158
270
 
159
271
  ${HARMONY_WORKFLOW_PROMPT.replace("Your agent identifier", "claude-code").replace("Your agent name", "Claude Code")}
160
272
  `;
161
- const commandPath = join(cwd, ".claude", "commands", "hmy.md");
162
- const { created, skipped } = writeFileIfNotExists(commandPath, commandContent, force);
163
- if (created)
164
- result.filesCreated.push(commandPath);
165
- if (skipped)
166
- result.filesSkipped.push(commandPath);
273
+ const standupContent = `---
274
+ name: hmy-standup
275
+ description: Generate a daily standup summary. Use when asked for project status, daily update, or standup report.
276
+ ---
277
+
278
+ ${HARMONY_STANDUP_PROMPT}
279
+ `;
280
+ const cleanupContent = `---
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.
283
+ ---
284
+
285
+ ${HARMONY_CLEANUP_PROMPT}
286
+ `;
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
+ }
167
328
  const globalConfigPath = join(homedir(), ".claude", "settings.json");
168
329
  const mcpConfig = {
169
330
  mcpServers: {
@@ -181,11 +342,12 @@ ${HARMONY_WORKFLOW_PROMPT.replace("Your agent identifier", "claude-code").replac
181
342
  }
182
343
  return result;
183
344
  }
184
- function initCodex(cwd, force) {
345
+ function initCodex(cwd, force, installMode) {
185
346
  const result = {
186
347
  agent: "codex",
187
348
  filesCreated: [],
188
- filesSkipped: []
349
+ filesSkipped: [],
350
+ symlinksCreated: []
189
351
  };
190
352
  const agentsContent = `# Harmony Integration
191
353
 
@@ -213,12 +375,6 @@ When given a card reference (e.g., #42 or a card name), follow this workflow:
213
375
  - \`harmony_get_board\` - Get board state
214
376
  - \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
215
377
  `;
216
- const agentsPath = join(cwd, "AGENTS.md");
217
- const { created: agentsCreated, skipped: agentsSkipped } = writeFileIfNotExists(agentsPath, agentsContent, force);
218
- if (agentsCreated)
219
- result.filesCreated.push(agentsPath);
220
- if (agentsSkipped)
221
- result.filesSkipped.push(agentsPath);
222
378
  const promptContent = `---
223
379
  name: hmy
224
380
  description: Start working on a Harmony card
@@ -230,13 +386,33 @@ arguments:
230
386
 
231
387
  ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "{{card}}").replace("Your agent identifier", "codex").replace("Your agent name", "OpenAI Codex")}
232
388
  `;
233
- const promptsDir = join(homedir(), ".codex", "prompts");
234
- const promptPath = join(promptsDir, "hmy.md");
235
- const { created: promptCreated, skipped: promptSkipped } = writeFileIfNotExists(promptPath, promptContent, force);
236
- if (promptCreated)
237
- result.filesCreated.push(promptPath);
238
- if (promptSkipped)
239
- 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);
240
416
  const configPath = join(homedir(), ".codex", "config.toml");
241
417
  const mcpConfigSection = `
242
418
  # Harmony MCP Server
@@ -266,12 +442,26 @@ args = ["serve"]
266
442
  }
267
443
  return result;
268
444
  }
269
- function initCursor(cwd, force) {
445
+ function initCursor(cwd, force, installMode) {
270
446
  const result = {
271
447
  agent: "cursor",
272
448
  filesCreated: [],
273
- filesSkipped: []
449
+ filesSkipped: [],
450
+ symlinksCreated: []
274
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
+ `;
275
465
  const mcpConfigPath = join(cwd, ".cursor", "mcp.json");
276
466
  const mcpConfig = {
277
467
  mcpServers: {
@@ -287,33 +477,47 @@ function initCursor(cwd, force) {
287
477
  } else if (mcpResult.skipped) {
288
478
  result.filesSkipped.push(mcpConfigPath);
289
479
  }
290
- const ruleContent = `---
291
- description: Harmony card workflow rule
292
- globs:
293
- - "**/*"
294
- alwaysApply: false
295
- ---
296
-
297
- # Harmony Integration
298
-
299
- When the user asks you to work on a Harmony card (references like #42, card names, or UUIDs):
300
-
301
- ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "cursor").replace("Your agent name", "Cursor AI")}
302
- `;
303
- const rulePath = join(cwd, ".cursor", "rules", "harmony.mdc");
304
- const { created: ruleCreated, skipped: ruleSkipped } = writeFileIfNotExists(rulePath, ruleContent, force);
305
- if (ruleCreated)
306
- result.filesCreated.push(rulePath);
307
- if (ruleSkipped)
308
- 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
+ }
309
501
  return result;
310
502
  }
311
- function initWindsurf(cwd, force) {
503
+ function initWindsurf(cwd, force, installMode) {
312
504
  const result = {
313
505
  agent: "windsurf",
314
506
  filesCreated: [],
315
- filesSkipped: []
507
+ filesSkipped: [],
508
+ symlinksCreated: []
316
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
+ `;
317
521
  const globalMcpPath = join(homedir(), ".codeium", "windsurf", "mcp_config.json");
318
522
  const mcpConfig = {
319
523
  mcpServers: {
@@ -331,50 +535,56 @@ function initWindsurf(cwd, force) {
331
535
  } else if (mcpResult.skipped) {
332
536
  result.filesSkipped.push(globalMcpPath);
333
537
  }
334
- const ruleContent = `---
335
- trigger: model_decision
336
- description: Activate when user asks to work on a Harmony card (references like #42, card names, or task management)
337
- ---
338
-
339
- # Harmony Card Workflow
340
-
341
- When working on a Harmony card:
342
-
343
- ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "windsurf").replace("Your agent name", "Windsurf AI")}
344
- `;
345
- const rulePath = join(cwd, ".windsurf", "rules", "harmony.md");
346
- const { created: ruleCreated, skipped: ruleSkipped } = writeFileIfNotExists(rulePath, ruleContent, force);
347
- if (ruleCreated)
348
- result.filesCreated.push(rulePath);
349
- if (ruleSkipped)
350
- 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
+ }
351
559
  return result;
352
560
  }
353
561
  function initHarmony(options = {}) {
354
562
  const cwd = options.cwd || process.cwd();
355
563
  const force = options.force || false;
356
564
  const agents = options.agents || ["claude", "codex", "cursor", "windsurf"];
565
+ const installMode = options.installMode || "local";
357
566
  const results = [];
358
567
  for (const agent of agents) {
359
568
  try {
360
569
  switch (agent) {
361
570
  case "claude":
362
- results.push(initClaude(cwd, force));
571
+ results.push(initClaude(cwd, force, installMode));
363
572
  break;
364
573
  case "codex":
365
- results.push(initCodex(cwd, force));
574
+ results.push(initCodex(cwd, force, installMode));
366
575
  break;
367
576
  case "cursor":
368
- results.push(initCursor(cwd, force));
577
+ results.push(initCursor(cwd, force, installMode));
369
578
  break;
370
579
  case "windsurf":
371
- results.push(initWindsurf(cwd, force));
580
+ results.push(initWindsurf(cwd, force, installMode));
372
581
  break;
373
582
  default:
374
583
  results.push({
375
584
  agent,
376
585
  filesCreated: [],
377
586
  filesSkipped: [],
587
+ symlinksCreated: [],
378
588
  error: `Unknown agent: ${agent}`
379
589
  });
380
590
  }
@@ -383,6 +593,7 @@ function initHarmony(options = {}) {
383
593
  agent,
384
594
  filesCreated: [],
385
595
  filesSkipped: [],
596
+ symlinksCreated: [],
386
597
  error: error instanceof Error ? error.message : String(error)
387
598
  });
388
599
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harmony-mcp",
3
- "version": "1.3.2",
3
+ "version": "1.5.0",
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,20 +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
- "@modelcontextprotocol/sdk": "^1.0.0",
48
- "commander": "^12.0.0",
49
- "hono": "^4.0.0",
50
- "zod": "^3.23.0"
47
+ "@clack/prompts": "^0.11.0",
48
+ "@modelcontextprotocol/sdk": "^1.25.3",
49
+ "commander": "^14.0.2",
50
+ "hono": "^4.11.5",
51
+ "picocolors": "^1.1.1",
52
+ "zod": "^4.3.6"
51
53
  },
52
54
  "devDependencies": {
53
- "@types/node": "^22.0.0",
54
- "typescript": "^5.6.0"
55
+ "@types/node": "^25.0.10",
56
+ "typescript": "^5.9.3"
55
57
  }
56
58
  }