@gethmy/mcp 2.1.0 → 2.1.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 CHANGED
@@ -20,6 +20,11 @@ Enables AI coding agents (Claude Code, OpenAI Codex, Cursor, Windsurf) to intera
20
20
  - **Smart Setup** - one command configures everything
21
21
  - **API Key Authentication** - no database credentials required
22
22
 
23
+ ## Prerequisites
24
+
25
+ - [Node.js](https://nodejs.org) >= 20 or [Bun](https://bun.sh) >= 1.0
26
+ - A [Harmony](https://gethmy.com) account with an API key
27
+
23
28
  ## Quick Start
24
29
 
25
30
  ### 1. Get an API Key
package/dist/cli.js CHANGED
@@ -26301,6 +26301,8 @@ class HarmonyApiClient {
26301
26301
  params.set("summary", "true");
26302
26302
  if (options?.includeArchived)
26303
26303
  params.set("include_archived", "true");
26304
+ if (options?.labelName)
26305
+ params.set("label_name", options.labelName);
26304
26306
  const query = params.toString() ? `?${params.toString()}` : "";
26305
26307
  return this.request("GET", `/board/${projectId}${query}`);
26306
26308
  }
@@ -31520,13 +31522,8 @@ function detectAgents(cwd = process.cwd()) {
31520
31522
  }
31521
31523
 
31522
31524
  // src/tui/docs.ts
31523
- import {
31524
- existsSync as existsSync4,
31525
- readFileSync as readFileSync3,
31526
- readdirSync as readdirSync2,
31527
- statSync
31528
- } from "node:fs";
31529
- import { join as join4 } from "node:path";
31525
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync } from "node:fs";
31526
+ import { isAbsolute, join as join4, resolve, sep as sep2 } from "node:path";
31530
31527
 
31531
31528
  // src/tui/theme.ts
31532
31529
  var pc = __toESM(require_picocolors(), 1);
@@ -31932,16 +31929,52 @@ function generateArchitectureMd(info, _cwd) {
31932
31929
  return lines.join(`
31933
31930
  `);
31934
31931
  }
31932
+ var VAGUE_STANDARDS = [
31933
+ "follow best practices",
31934
+ "use best practices",
31935
+ "keep it clean",
31936
+ "write clean code",
31937
+ "maintain code quality",
31938
+ "ensure quality",
31939
+ "use proper naming",
31940
+ "follow conventions",
31941
+ "be consistent"
31942
+ ];
31935
31943
  function verifyDocs(cwd) {
31936
31944
  const issues = [];
31937
31945
  const claudeMd = readText(join4(cwd, "CLAUDE.md"));
31946
+ const agentsMd = readText(join4(cwd, "AGENTS.md"));
31947
+ const pkg = readJson(join4(cwd, "package.json"));
31948
+ const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
31949
+ const projectRoot = resolve(cwd);
31938
31950
  if (claudeMd) {
31951
+ const importedFiles = [];
31939
31952
  for (const line of claudeMd.split(`
31940
31953
  `)) {
31941
31954
  const match = line.match(/^@(.+)$/);
31942
31955
  if (match) {
31943
31956
  const refPath = match[1].trim();
31944
- if (!existsSync4(join4(cwd, refPath))) {
31957
+ if (isAbsolute(refPath)) {
31958
+ issues.push({
31959
+ severity: "error",
31960
+ file: "CLAUDE.md",
31961
+ message: `@ reference uses an absolute path: ${refPath}`,
31962
+ fix: "Use a project-relative path under the repository root"
31963
+ });
31964
+ continue;
31965
+ }
31966
+ const resolvedPath = resolve(projectRoot, refPath);
31967
+ if (resolvedPath !== projectRoot && !resolvedPath.startsWith(projectRoot + sep2)) {
31968
+ issues.push({
31969
+ severity: "error",
31970
+ file: "CLAUDE.md",
31971
+ message: `@ reference escapes project root: ${refPath}`,
31972
+ fix: "Remove traversal segments (../) and keep references inside the repo"
31973
+ });
31974
+ continue;
31975
+ }
31976
+ importedFiles.push({ ref: refPath, resolved: resolvedPath });
31977
+ if (!existsSync4(resolvedPath)) {
31945
31978
  issues.push({
31946
31979
  severity: "error",
31947
31980
  file: "CLAUDE.md",
@@ -31951,11 +31984,72 @@ function verifyDocs(cwd) {
31951
31984
  }
31952
31985
  }
31953
31986
  }
31987
+ const claudeLines = claudeMd.split(`
31988
+ `).length;
31989
+ if (claudeLines > 100) {
31990
+ issues.push({
31991
+ severity: "warning",
31992
+ file: "CLAUDE.md",
31993
+ message: `CLAUDE.md is ${claudeLines} lines (recommended: under 100)`,
31994
+ fix: "Move detailed content to AGENTS.md or docs/ files and use @imports"
31995
+ });
31996
+ }
31997
+ if (importedFiles.length > 0) {
31998
+ const claudeHeadings = extractHeadings(claudeMd);
31999
+ for (const { ref: refPath, resolved: resolvedPath } of importedFiles) {
32000
+ const refContent = readText(resolvedPath);
32001
+ if (!refContent)
32002
+ continue;
32003
+ const refHeadings = extractHeadings(refContent);
32004
+ for (const heading of claudeHeadings) {
32005
+ if (refHeadings.has(heading)) {
32006
+ issues.push({
32007
+ severity: "warning",
32008
+ file: "CLAUDE.md",
32009
+ message: `Section "${heading}" duplicates content from @${refPath}`,
32010
+ fix: `Remove the "${heading}" section — it's already included via @import`
32011
+ });
32012
+ }
32013
+ }
32014
+ }
32015
+ }
31954
32016
  }
31955
- const agentsMd = readText(join4(cwd, "AGENTS.md"));
31956
- const pkg = readJson(join4(cwd, "package.json"));
31957
- const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
31958
32017
  if (agentsMd) {
32018
+ const agentsLines = agentsMd.split(`
32019
+ `);
32020
+ const contextLines = [];
32021
+ let pastFirstHeading = false;
32022
+ let hitNextSection = false;
32023
+ for (const line of agentsLines) {
32024
+ if (!pastFirstHeading) {
32025
+ if (line.startsWith("# ")) {
32026
+ pastFirstHeading = true;
32027
+ }
32028
+ continue;
32029
+ }
32030
+ if (line.startsWith("## ")) {
32031
+ hitNextSection = true;
32032
+ break;
32033
+ }
32034
+ const trimmed = line.trim();
32035
+ if (trimmed)
32036
+ contextLines.push(trimmed);
32037
+ }
32038
+ if (pastFirstHeading && !hitNextSection && contextLines.length === 0) {
32039
+ issues.push({
32040
+ severity: "warning",
32041
+ file: "AGENTS.md",
32042
+ message: "Missing project context line after the title heading",
32043
+ fix: "Add a single-line description: stack + what the project does"
32044
+ });
32045
+ } else if (contextLines.length > 1) {
32046
+ issues.push({
32047
+ severity: "warning",
32048
+ file: "AGENTS.md",
32049
+ message: `Project context should be exactly 1 line, found ${contextLines.length}`,
32050
+ fix: "Condense to a single line: stack + what the project does"
32051
+ });
32052
+ }
31959
32053
  const codeBlockRe = /```[\s\S]*?```/g;
31960
32054
  let blockMatch;
31961
32055
  while ((blockMatch = codeBlockRe.exec(agentsMd)) !== null) {
@@ -31964,7 +32058,16 @@ function verifyDocs(cwd) {
31964
32058
  let cmdMatch;
31965
32059
  while ((cmdMatch = cmdRe.exec(block)) !== null) {
31966
32060
  const scriptName = cmdMatch[1];
31967
- const builtins = new Set(["install", "init", "create", "exec", "dlx", "x", "test", "start"]);
32061
+ const builtins = new Set([
32062
+ "install",
32063
+ "init",
32064
+ "create",
32065
+ "exec",
32066
+ "dlx",
32067
+ "x",
32068
+ "test",
32069
+ "start"
32070
+ ]);
31968
32071
  if (builtins.has(scriptName))
31969
32072
  continue;
31970
32073
  if (Object.keys(pkgScripts).length > 0 && !(scriptName in pkgScripts)) {
@@ -31977,6 +32080,34 @@ function verifyDocs(cwd) {
31977
32080
  }
31978
32081
  }
31979
32082
  }
32083
+ const standardsSection = extractSection(agentsMd, "Code Standards");
32084
+ if (standardsSection) {
32085
+ const lower = standardsSection.toLowerCase();
32086
+ for (const phrase of VAGUE_STANDARDS) {
32087
+ if (lower.includes(phrase)) {
32088
+ issues.push({
32089
+ severity: "warning",
32090
+ file: "AGENTS.md",
32091
+ message: `Code Standards contains vague phrase: "${phrase}"`,
32092
+ fix: "Replace with specific, verifiable conventions derived from config files"
32093
+ });
32094
+ }
32095
+ }
32096
+ }
32097
+ if (Object.keys(pkgScripts).length > 0) {
32098
+ const hasTestScript = Object.keys(pkgScripts).some((k3) => k3 === "test" || k3.startsWith("test:"));
32099
+ if (!hasTestScript) {
32100
+ const mentionsNoTest = agentsMd.toLowerCase().includes("no test");
32101
+ if (!mentionsNoTest) {
32102
+ issues.push({
32103
+ severity: "warning",
32104
+ file: "AGENTS.md",
32105
+ message: "No test script in package.json and AGENTS.md doesn't mention it",
32106
+ fix: 'Add a note like "No test framework. Verify changes with `bun run build`."'
32107
+ });
32108
+ }
32109
+ }
32110
+ }
31980
32111
  checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
31981
32112
  }
31982
32113
  const archMd = readText(join4(cwd, "docs", "architecture.md"));
@@ -31985,16 +32116,48 @@ function verifyDocs(cwd) {
31985
32116
  }
31986
32117
  return issues;
31987
32118
  }
32119
+ function extractHeadings(content) {
32120
+ const headings = new Set;
32121
+ for (const line of content.split(`
32122
+ `)) {
32123
+ const match = line.match(/^#{2,3}\s+(.+)$/);
32124
+ if (match) {
32125
+ headings.add(match[1].trim());
32126
+ }
32127
+ }
32128
+ return headings;
32129
+ }
32130
+ function extractSection(content, heading) {
32131
+ const lines = content.split(`
32132
+ `);
32133
+ let capturing = false;
32134
+ const result = [];
32135
+ for (const line of lines) {
32136
+ if (capturing) {
32137
+ if (line.match(/^#{1,2}\s/))
32138
+ break;
32139
+ result.push(line);
32140
+ } else if (line.match(new RegExp(`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i"))) {
32141
+ capturing = true;
32142
+ }
32143
+ }
32144
+ return result.length > 0 ? result.join(`
32145
+ `) : null;
32146
+ }
31988
32147
  function checkBacktickPaths(content, file2, cwd, issues) {
31989
32148
  const pathRe = /`((?:src\/|packages\/|apps\/|supabase\/|docs\/)[^`]+)`/g;
31990
32149
  let match;
31991
32150
  const checked = new Set;
32151
+ const root = resolve(cwd);
31992
32152
  while ((match = pathRe.exec(content)) !== null) {
31993
32153
  const refPath = match[1].replace(/\/$/, "");
31994
32154
  if (checked.has(refPath))
31995
32155
  continue;
31996
32156
  checked.add(refPath);
31997
- if (!existsSync4(join4(cwd, refPath))) {
32157
+ const resolvedRef = resolve(root, refPath);
32158
+ if (resolvedRef !== root && !resolvedRef.startsWith(root + sep2))
32159
+ continue;
32160
+ if (!existsSync4(resolvedRef)) {
31998
32161
  issues.push({
31999
32162
  severity: "warning",
32000
32163
  file: file2,
@@ -32194,7 +32357,7 @@ async function writeFilesWithProgress(files, options = {}) {
32194
32357
  result = writeFile(file2.path, file2.content, options);
32195
32358
  }
32196
32359
  results.push(result);
32197
- await new Promise((resolve) => setTimeout(resolve, 50));
32360
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
32198
32361
  }
32199
32362
  spinner.stop("Files written");
32200
32363
  for (const result of results) {
package/dist/index.js CHANGED
@@ -24061,6 +24061,8 @@ class HarmonyApiClient {
24061
24061
  params.set("summary", "true");
24062
24062
  if (options?.includeArchived)
24063
24063
  params.set("include_archived", "true");
24064
+ if (options?.labelName)
24065
+ params.set("label_name", options.labelName);
24064
24066
  const query = params.toString() ? `?${params.toString()}` : "";
24065
24067
  return this.request("GET", `/board/${projectId}${query}`);
24066
24068
  }
@@ -221,6 +221,8 @@ export class HarmonyApiClient {
221
221
  params.set("summary", "true");
222
222
  if (options?.includeArchived)
223
223
  params.set("include_archived", "true");
224
+ if (options?.labelName)
225
+ params.set("label_name", options.labelName);
224
226
  const query = params.toString() ? `?${params.toString()}` : "";
225
227
  return this.request("GET", `/board/${projectId}${query}`);
226
228
  }
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync, readdirSync, statSync, } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { isAbsolute, join, resolve, sep } from "node:path";
3
3
  import * as p from "@clack/prompts";
4
4
  import { colors, symbols } from "./theme.js";
5
5
  // ── Helpers ─────────────────────────────────────────────────────────────────
@@ -141,8 +141,12 @@ export function scanProject(cwd) {
141
141
  let framework = null;
142
142
  if (pkg) {
143
143
  const deps = {
144
- ...(typeof pkg.dependencies === "object" ? pkg.dependencies : {}),
145
- ...(typeof pkg.devDependencies === "object" ? pkg.devDependencies : {}),
144
+ ...(typeof pkg.dependencies === "object"
145
+ ? pkg.dependencies
146
+ : {}),
147
+ ...(typeof pkg.devDependencies === "object"
148
+ ? pkg.devDependencies
149
+ : {}),
146
150
  };
147
151
  if (deps.next) {
148
152
  framework = "next";
@@ -305,7 +309,11 @@ function describeScript(name) {
305
309
  * Generate a scaffold AGENTS.md from project metadata.
306
310
  */
307
311
  export function generateAgentsMd(info, _cwd) {
308
- const lang = info.language === "typescript" ? "TypeScript" : info.language === "javascript" ? "JavaScript" : info.language;
312
+ const lang = info.language === "typescript"
313
+ ? "TypeScript"
314
+ : info.language === "javascript"
315
+ ? "JavaScript"
316
+ : info.language;
309
317
  const frameworkLabel = info.framework ? `${info.framework} ` : "";
310
318
  const monoLabel = info.monorepo ? " (monorepo)" : "";
311
319
  const lines = [];
@@ -404,19 +412,63 @@ export function generateArchitectureMd(info, _cwd) {
404
412
  return lines.join("\n");
405
413
  }
406
414
  // ── Verification ────────────────────────────────────────────────────────────
415
+ /** Vague phrases that provide no actionable guidance to agents. */
416
+ const VAGUE_STANDARDS = [
417
+ "follow best practices",
418
+ "use best practices",
419
+ "keep it clean",
420
+ "write clean code",
421
+ "maintain code quality",
422
+ "ensure quality",
423
+ "use proper naming",
424
+ "follow conventions",
425
+ "be consistent",
426
+ ];
407
427
  /**
408
- * Verify existing docs for broken references, stale commands, and dead paths.
428
+ * Verify existing docs for broken references, stale commands, dead paths,
429
+ * and structural quality issues from the setup-agent-docs quality checks.
409
430
  */
410
431
  export function verifyDocs(cwd) {
411
432
  const issues = [];
412
- // 1. CLAUDE.md — check @-references
413
433
  const claudeMd = readText(join(cwd, "CLAUDE.md"));
434
+ const agentsMd = readText(join(cwd, "AGENTS.md"));
435
+ const pkg = readJson(join(cwd, "package.json"));
436
+ const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
437
+ ? pkg.scripts
438
+ : {};
439
+ // ── CLAUDE.md checks ──────────────────────────────────────────────────
440
+ const projectRoot = resolve(cwd);
414
441
  if (claudeMd) {
442
+ // Check @-references exist on disk (with path traversal protection)
443
+ const importedFiles = [];
415
444
  for (const line of claudeMd.split("\n")) {
416
445
  const match = line.match(/^@(.+)$/);
417
446
  if (match) {
418
447
  const refPath = match[1].trim();
419
- if (!existsSync(join(cwd, refPath))) {
448
+ // Reject absolute paths
449
+ if (isAbsolute(refPath)) {
450
+ issues.push({
451
+ severity: "error",
452
+ file: "CLAUDE.md",
453
+ message: `@ reference uses an absolute path: ${refPath}`,
454
+ fix: "Use a project-relative path under the repository root",
455
+ });
456
+ continue;
457
+ }
458
+ // Resolve and ensure the path stays inside the project root
459
+ const resolvedPath = resolve(projectRoot, refPath);
460
+ if (resolvedPath !== projectRoot &&
461
+ !resolvedPath.startsWith(projectRoot + sep)) {
462
+ issues.push({
463
+ severity: "error",
464
+ file: "CLAUDE.md",
465
+ message: `@ reference escapes project root: ${refPath}`,
466
+ fix: "Remove traversal segments (../) and keep references inside the repo",
467
+ });
468
+ continue;
469
+ }
470
+ importedFiles.push({ ref: refPath, resolved: resolvedPath });
471
+ if (!existsSync(resolvedPath)) {
420
472
  issues.push({
421
473
  severity: "error",
422
474
  file: "CLAUDE.md",
@@ -426,26 +478,96 @@ export function verifyDocs(cwd) {
426
478
  }
427
479
  }
428
480
  }
481
+ // Line count — CLAUDE.md should be under 100 lines (lean, @imports only)
482
+ const claudeLines = claudeMd.split("\n").length;
483
+ if (claudeLines > 100) {
484
+ issues.push({
485
+ severity: "warning",
486
+ file: "CLAUDE.md",
487
+ message: `CLAUDE.md is ${claudeLines} lines (recommended: under 100)`,
488
+ fix: "Move detailed content to AGENTS.md or docs/ files and use @imports",
489
+ });
490
+ }
491
+ // Duplication — check if CLAUDE.md duplicates content from imported files
492
+ if (importedFiles.length > 0) {
493
+ const claudeHeadings = extractHeadings(claudeMd);
494
+ for (const { ref: refPath, resolved: resolvedPath } of importedFiles) {
495
+ const refContent = readText(resolvedPath);
496
+ if (!refContent)
497
+ continue;
498
+ const refHeadings = extractHeadings(refContent);
499
+ // Flag if CLAUDE.md repeats section headings from imported files
500
+ for (const heading of claudeHeadings) {
501
+ if (refHeadings.has(heading)) {
502
+ issues.push({
503
+ severity: "warning",
504
+ file: "CLAUDE.md",
505
+ message: `Section "${heading}" duplicates content from @${refPath}`,
506
+ fix: `Remove the "${heading}" section — it's already included via @import`,
507
+ });
508
+ }
509
+ }
510
+ }
511
+ }
429
512
  }
430
- // 2. AGENTS.md check commands against package.json scripts
431
- const agentsMd = readText(join(cwd, "AGENTS.md"));
432
- const pkg = readJson(join(cwd, "package.json"));
433
- const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
434
- ? pkg.scripts
435
- : {};
513
+ // ── AGENTS.md checks ──────────────────────────────────────────────────
436
514
  if (agentsMd) {
437
- // Extract commands from code blocks
515
+ // Project Context first non-heading, non-blank line should be exactly one line
516
+ const agentsLines = agentsMd.split("\n");
517
+ const contextLines = [];
518
+ let pastFirstHeading = false;
519
+ let hitNextSection = false;
520
+ for (const line of agentsLines) {
521
+ if (!pastFirstHeading) {
522
+ if (line.startsWith("# ")) {
523
+ pastFirstHeading = true;
524
+ }
525
+ continue;
526
+ }
527
+ // Stop at next ## heading
528
+ if (line.startsWith("## ")) {
529
+ hitNextSection = true;
530
+ break;
531
+ }
532
+ const trimmed = line.trim();
533
+ if (trimmed)
534
+ contextLines.push(trimmed);
535
+ }
536
+ if (pastFirstHeading && !hitNextSection && contextLines.length === 0) {
537
+ issues.push({
538
+ severity: "warning",
539
+ file: "AGENTS.md",
540
+ message: "Missing project context line after the title heading",
541
+ fix: "Add a single-line description: stack + what the project does",
542
+ });
543
+ }
544
+ else if (contextLines.length > 1) {
545
+ issues.push({
546
+ severity: "warning",
547
+ file: "AGENTS.md",
548
+ message: `Project context should be exactly 1 line, found ${contextLines.length}`,
549
+ fix: "Condense to a single line: stack + what the project does",
550
+ });
551
+ }
552
+ // Commands — check against package.json scripts
438
553
  const codeBlockRe = /```[\s\S]*?```/g;
439
554
  let blockMatch;
440
555
  while ((blockMatch = codeBlockRe.exec(agentsMd)) !== null) {
441
556
  const block = blockMatch[0];
442
- // Match lines that look like package manager commands
443
557
  const cmdRe = /(?:bun|npm|pnpm|yarn)\s+(?:run\s+)?(\S+)/g;
444
558
  let cmdMatch;
445
559
  while ((cmdMatch = cmdRe.exec(block)) !== null) {
446
560
  const scriptName = cmdMatch[1];
447
- // Skip if the script name is a built-in (install, init, etc.)
448
- const builtins = new Set(["install", "init", "create", "exec", "dlx", "x", "test", "start"]);
561
+ const builtins = new Set([
562
+ "install",
563
+ "init",
564
+ "create",
565
+ "exec",
566
+ "dlx",
567
+ "x",
568
+ "test",
569
+ "start",
570
+ ]);
449
571
  if (builtins.has(scriptName))
450
572
  continue;
451
573
  if (Object.keys(pkgScripts).length > 0 && !(scriptName in pkgScripts)) {
@@ -458,27 +580,91 @@ export function verifyDocs(cwd) {
458
580
  }
459
581
  }
460
582
  }
461
- // 3. Check backtick-quoted paths in AGENTS.md
583
+ // Code Standards flag vague, non-actionable phrases
584
+ const standardsSection = extractSection(agentsMd, "Code Standards");
585
+ if (standardsSection) {
586
+ const lower = standardsSection.toLowerCase();
587
+ for (const phrase of VAGUE_STANDARDS) {
588
+ if (lower.includes(phrase)) {
589
+ issues.push({
590
+ severity: "warning",
591
+ file: "AGENTS.md",
592
+ message: `Code Standards contains vague phrase: "${phrase}"`,
593
+ fix: "Replace with specific, verifiable conventions derived from config files",
594
+ });
595
+ }
596
+ }
597
+ }
598
+ // Missing test command — if no test script exists, AGENTS.md should say so
599
+ if (Object.keys(pkgScripts).length > 0) {
600
+ const hasTestScript = Object.keys(pkgScripts).some((k) => k === "test" || k.startsWith("test:"));
601
+ if (!hasTestScript) {
602
+ const mentionsNoTest = agentsMd.toLowerCase().includes("no test");
603
+ if (!mentionsNoTest) {
604
+ issues.push({
605
+ severity: "warning",
606
+ file: "AGENTS.md",
607
+ message: "No test script in package.json and AGENTS.md doesn't mention it",
608
+ fix: 'Add a note like "No test framework. Verify changes with `bun run build`."',
609
+ });
610
+ }
611
+ }
612
+ }
613
+ // Check backtick-quoted paths
462
614
  checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
463
615
  }
464
- // 3b. Check backtick-quoted paths in docs/architecture.md
616
+ // ── docs/architecture.md checks ───────────────────────────────────────
465
617
  const archMd = readText(join(cwd, "docs", "architecture.md"));
466
618
  if (archMd) {
467
619
  checkBacktickPaths(archMd, "docs/architecture.md", cwd, issues);
468
620
  }
469
621
  return issues;
470
622
  }
623
+ /** Extract ## headings from markdown content. */
624
+ function extractHeadings(content) {
625
+ const headings = new Set();
626
+ for (const line of content.split("\n")) {
627
+ const match = line.match(/^#{2,3}\s+(.+)$/);
628
+ if (match) {
629
+ headings.add(match[1].trim());
630
+ }
631
+ }
632
+ return headings;
633
+ }
634
+ /** Extract content under a specific ## section heading. */
635
+ function extractSection(content, heading) {
636
+ const lines = content.split("\n");
637
+ let capturing = false;
638
+ const result = [];
639
+ for (const line of lines) {
640
+ if (capturing) {
641
+ // Stop at next ## heading
642
+ if (line.match(/^#{1,2}\s/))
643
+ break;
644
+ result.push(line);
645
+ }
646
+ else if (line.match(new RegExp(`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i"))) {
647
+ capturing = true;
648
+ }
649
+ }
650
+ return result.length > 0 ? result.join("\n") : null;
651
+ }
471
652
  /** Scan markdown for backtick-quoted paths and check they exist. */
472
653
  function checkBacktickPaths(content, file, cwd, issues) {
473
654
  const pathRe = /`((?:src\/|packages\/|apps\/|supabase\/|docs\/)[^`]+)`/g;
474
655
  let match;
475
656
  const checked = new Set();
657
+ const root = resolve(cwd);
476
658
  while ((match = pathRe.exec(content)) !== null) {
477
659
  const refPath = match[1].replace(/\/$/, ""); // strip trailing slash
478
660
  if (checked.has(refPath))
479
661
  continue;
480
662
  checked.add(refPath);
481
- if (!existsSync(join(cwd, refPath))) {
663
+ // Skip paths that escape the project root (e.g. src/../../etc/hosts)
664
+ const resolvedRef = resolve(root, refPath);
665
+ if (resolvedRef !== root && !resolvedRef.startsWith(root + sep))
666
+ continue;
667
+ if (!existsSync(resolvedRef)) {
482
668
  issues.push({
483
669
  severity: "warning",
484
670
  file,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -46,7 +46,8 @@
46
46
  "coding-assistant"
47
47
  ],
48
48
  "engines": {
49
- "node": ">=18.0.0"
49
+ "node": ">=20.0.0",
50
+ "bun": ">=1.0.0"
50
51
  },
51
52
  "scripts": {
52
53
  "build": "bun build src/index.ts src/cli.ts --outdir dist --target node && tsc --outDir dist/lib --declaration false --skipLibCheck --noCheck",
@@ -68,7 +69,7 @@
68
69
  "zod": "^4.3.6"
69
70
  },
70
71
  "devDependencies": {
71
- "@types/node": "^25.2.0",
72
- "typescript": "^5.9.3"
72
+ "@types/node": "^25.5.0",
73
+ "typescript": "^6.0.1"
73
74
  }
74
75
  }
package/src/api-client.ts CHANGED
@@ -286,6 +286,7 @@ export class HarmonyApiClient {
286
286
  columnId?: string;
287
287
  summary?: boolean;
288
288
  includeArchived?: boolean;
289
+ labelName?: string;
289
290
  },
290
291
  ): Promise<{
291
292
  project: unknown;
@@ -309,6 +310,7 @@ export class HarmonyApiClient {
309
310
  if (options?.columnId) params.set("column_id", options.columnId);
310
311
  if (options?.summary) params.set("summary", "true");
311
312
  if (options?.includeArchived) params.set("include_archived", "true");
313
+ if (options?.labelName) params.set("label_name", options.labelName);
312
314
 
313
315
  const query = params.toString() ? `?${params.toString()}` : "";
314
316
  return this.request("GET", `/board/${projectId}${query}`);
@@ -497,6 +499,10 @@ export class HarmonyApiClient {
497
499
  currentTask?: string;
498
500
  blockers?: string[];
499
501
  estimatedMinutesRemaining?: number;
502
+ phase?: string;
503
+ filesChanged?: number;
504
+ costCents?: number;
505
+ recentActions?: { action: string; ts: string }[];
500
506
  },
501
507
  ): Promise<{ session: unknown; created: boolean }> {
502
508
  return this.request("POST", `/cards/${cardId}/agent-context`, data);
package/src/tui/docs.ts CHANGED
@@ -1,10 +1,5 @@
1
- import {
2
- existsSync,
3
- readFileSync,
4
- readdirSync,
5
- statSync,
6
- } from "node:fs";
7
- import { join } from "node:path";
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { isAbsolute, join, resolve, sep } from "node:path";
8
3
  import * as p from "@clack/prompts";
9
4
  import { colors, symbols } from "./theme.js";
10
5
 
@@ -182,8 +177,12 @@ export function scanProject(cwd: string): ProjectInfo {
182
177
  let framework: string | null = null;
183
178
  if (pkg) {
184
179
  const deps: Record<string, string> = {
185
- ...(typeof pkg.dependencies === "object" ? (pkg.dependencies as Record<string, string>) : {}),
186
- ...(typeof pkg.devDependencies === "object" ? (pkg.devDependencies as Record<string, string>) : {}),
180
+ ...(typeof pkg.dependencies === "object"
181
+ ? (pkg.dependencies as Record<string, string>)
182
+ : {}),
183
+ ...(typeof pkg.devDependencies === "object"
184
+ ? (pkg.devDependencies as Record<string, string>)
185
+ : {}),
187
186
  };
188
187
 
189
188
  if (deps.next) {
@@ -347,7 +346,12 @@ function describeScript(name: string): string {
347
346
  * Generate a scaffold AGENTS.md from project metadata.
348
347
  */
349
348
  export function generateAgentsMd(info: ProjectInfo, _cwd: string): string {
350
- const lang = info.language === "typescript" ? "TypeScript" : info.language === "javascript" ? "JavaScript" : info.language;
349
+ const lang =
350
+ info.language === "typescript"
351
+ ? "TypeScript"
352
+ : info.language === "javascript"
353
+ ? "JavaScript"
354
+ : info.language;
351
355
  const frameworkLabel = info.framework ? `${info.framework} ` : "";
352
356
  const monoLabel = info.monorepo ? " (monorepo)" : "";
353
357
 
@@ -388,7 +392,8 @@ export function generateAgentsMd(info: ProjectInfo, _cwd: string): string {
388
392
  lines.push("## Code Standards");
389
393
  lines.push("");
390
394
 
391
- const langLabel = info.language === "typescript" ? "TypeScript" : "JavaScript";
395
+ const langLabel =
396
+ info.language === "typescript" ? "TypeScript" : "JavaScript";
392
397
  if (info.language === "typescript" || info.language === "javascript") {
393
398
  lines.push(`- ${langLabel} with ES modules`);
394
399
  }
@@ -442,7 +447,10 @@ export function generateClaudeMd(info: ProjectInfo): string {
442
447
  /**
443
448
  * Generate an architecture.md scaffold.
444
449
  */
445
- export function generateArchitectureMd(info: ProjectInfo, _cwd: string): string {
450
+ export function generateArchitectureMd(
451
+ info: ProjectInfo,
452
+ _cwd: string,
453
+ ): string {
446
454
  const lines: string[] = [];
447
455
  lines.push("# Architecture");
448
456
  lines.push("");
@@ -468,20 +476,74 @@ export function generateArchitectureMd(info: ProjectInfo, _cwd: string): string
468
476
 
469
477
  // ── Verification ────────────────────────────────────────────────────────────
470
478
 
479
+ /** Vague phrases that provide no actionable guidance to agents. */
480
+ const VAGUE_STANDARDS = [
481
+ "follow best practices",
482
+ "use best practices",
483
+ "keep it clean",
484
+ "write clean code",
485
+ "maintain code quality",
486
+ "ensure quality",
487
+ "use proper naming",
488
+ "follow conventions",
489
+ "be consistent",
490
+ ];
491
+
471
492
  /**
472
- * Verify existing docs for broken references, stale commands, and dead paths.
493
+ * Verify existing docs for broken references, stale commands, dead paths,
494
+ * and structural quality issues from the setup-agent-docs quality checks.
473
495
  */
474
496
  export function verifyDocs(cwd: string): DocsIssue[] {
475
497
  const issues: DocsIssue[] = [];
476
498
 
477
- // 1. CLAUDE.md — check @-references
478
499
  const claudeMd = readText(join(cwd, "CLAUDE.md"));
500
+ const agentsMd = readText(join(cwd, "AGENTS.md"));
501
+ const pkg = readJson(join(cwd, "package.json"));
502
+ const pkgScripts =
503
+ pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
504
+ ? (pkg.scripts as Record<string, string>)
505
+ : {};
506
+
507
+ // ── CLAUDE.md checks ──────────────────────────────────────────────────
508
+
509
+ const projectRoot = resolve(cwd);
510
+
479
511
  if (claudeMd) {
512
+ // Check @-references exist on disk (with path traversal protection)
513
+ const importedFiles: { ref: string; resolved: string }[] = [];
480
514
  for (const line of claudeMd.split("\n")) {
481
515
  const match = line.match(/^@(.+)$/);
482
516
  if (match) {
483
517
  const refPath = match[1].trim();
484
- if (!existsSync(join(cwd, refPath))) {
518
+
519
+ // Reject absolute paths
520
+ if (isAbsolute(refPath)) {
521
+ issues.push({
522
+ severity: "error",
523
+ file: "CLAUDE.md",
524
+ message: `@ reference uses an absolute path: ${refPath}`,
525
+ fix: "Use a project-relative path under the repository root",
526
+ });
527
+ continue;
528
+ }
529
+
530
+ // Resolve and ensure the path stays inside the project root
531
+ const resolvedPath = resolve(projectRoot, refPath);
532
+ if (
533
+ resolvedPath !== projectRoot &&
534
+ !resolvedPath.startsWith(projectRoot + sep)
535
+ ) {
536
+ issues.push({
537
+ severity: "error",
538
+ file: "CLAUDE.md",
539
+ message: `@ reference escapes project root: ${refPath}`,
540
+ fix: "Remove traversal segments (../) and keep references inside the repo",
541
+ });
542
+ continue;
543
+ }
544
+
545
+ importedFiles.push({ ref: refPath, resolved: resolvedPath });
546
+ if (!existsSync(resolvedPath)) {
485
547
  issues.push({
486
548
  severity: "error",
487
549
  file: "CLAUDE.md",
@@ -491,28 +553,99 @@ export function verifyDocs(cwd: string): DocsIssue[] {
491
553
  }
492
554
  }
493
555
  }
556
+
557
+ // Line count — CLAUDE.md should be under 100 lines (lean, @imports only)
558
+ const claudeLines = claudeMd.split("\n").length;
559
+ if (claudeLines > 100) {
560
+ issues.push({
561
+ severity: "warning",
562
+ file: "CLAUDE.md",
563
+ message: `CLAUDE.md is ${claudeLines} lines (recommended: under 100)`,
564
+ fix: "Move detailed content to AGENTS.md or docs/ files and use @imports",
565
+ });
566
+ }
567
+
568
+ // Duplication — check if CLAUDE.md duplicates content from imported files
569
+ if (importedFiles.length > 0) {
570
+ const claudeHeadings = extractHeadings(claudeMd);
571
+ for (const { ref: refPath, resolved: resolvedPath } of importedFiles) {
572
+ const refContent = readText(resolvedPath);
573
+ if (!refContent) continue;
574
+ const refHeadings = extractHeadings(refContent);
575
+ // Flag if CLAUDE.md repeats section headings from imported files
576
+ for (const heading of claudeHeadings) {
577
+ if (refHeadings.has(heading)) {
578
+ issues.push({
579
+ severity: "warning",
580
+ file: "CLAUDE.md",
581
+ message: `Section "${heading}" duplicates content from @${refPath}`,
582
+ fix: `Remove the "${heading}" section — it's already included via @import`,
583
+ });
584
+ }
585
+ }
586
+ }
587
+ }
494
588
  }
495
589
 
496
- // 2. AGENTS.md check commands against package.json scripts
497
- const agentsMd = readText(join(cwd, "AGENTS.md"));
498
- const pkg = readJson(join(cwd, "package.json"));
499
- const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
500
- ? (pkg.scripts as Record<string, string>)
501
- : {};
590
+ // ── AGENTS.md checks ──────────────────────────────────────────────────
502
591
 
503
592
  if (agentsMd) {
504
- // Extract commands from code blocks
593
+ // Project Context first non-heading, non-blank line should be exactly one line
594
+ const agentsLines = agentsMd.split("\n");
595
+ const contextLines: string[] = [];
596
+ let pastFirstHeading = false;
597
+ let hitNextSection = false;
598
+ for (const line of agentsLines) {
599
+ if (!pastFirstHeading) {
600
+ if (line.startsWith("# ")) {
601
+ pastFirstHeading = true;
602
+ }
603
+ continue;
604
+ }
605
+ // Stop at next ## heading
606
+ if (line.startsWith("## ")) {
607
+ hitNextSection = true;
608
+ break;
609
+ }
610
+ const trimmed = line.trim();
611
+ if (trimmed) contextLines.push(trimmed);
612
+ }
613
+
614
+ if (pastFirstHeading && !hitNextSection && contextLines.length === 0) {
615
+ issues.push({
616
+ severity: "warning",
617
+ file: "AGENTS.md",
618
+ message: "Missing project context line after the title heading",
619
+ fix: "Add a single-line description: stack + what the project does",
620
+ });
621
+ } else if (contextLines.length > 1) {
622
+ issues.push({
623
+ severity: "warning",
624
+ file: "AGENTS.md",
625
+ message: `Project context should be exactly 1 line, found ${contextLines.length}`,
626
+ fix: "Condense to a single line: stack + what the project does",
627
+ });
628
+ }
629
+
630
+ // Commands — check against package.json scripts
505
631
  const codeBlockRe = /```[\s\S]*?```/g;
506
632
  let blockMatch: RegExpExecArray | null;
507
633
  while ((blockMatch = codeBlockRe.exec(agentsMd)) !== null) {
508
634
  const block = blockMatch[0];
509
- // Match lines that look like package manager commands
510
635
  const cmdRe = /(?:bun|npm|pnpm|yarn)\s+(?:run\s+)?(\S+)/g;
511
636
  let cmdMatch: RegExpExecArray | null;
512
637
  while ((cmdMatch = cmdRe.exec(block)) !== null) {
513
638
  const scriptName = cmdMatch[1];
514
- // Skip if the script name is a built-in (install, init, etc.)
515
- const builtins = new Set(["install", "init", "create", "exec", "dlx", "x", "test", "start"]);
639
+ const builtins = new Set([
640
+ "install",
641
+ "init",
642
+ "create",
643
+ "exec",
644
+ "dlx",
645
+ "x",
646
+ "test",
647
+ "start",
648
+ ]);
516
649
  if (builtins.has(scriptName)) continue;
517
650
  if (Object.keys(pkgScripts).length > 0 && !(scriptName in pkgScripts)) {
518
651
  issues.push({
@@ -525,11 +658,47 @@ export function verifyDocs(cwd: string): DocsIssue[] {
525
658
  }
526
659
  }
527
660
 
528
- // 3. Check backtick-quoted paths in AGENTS.md
661
+ // Code Standards flag vague, non-actionable phrases
662
+ const standardsSection = extractSection(agentsMd, "Code Standards");
663
+ if (standardsSection) {
664
+ const lower = standardsSection.toLowerCase();
665
+ for (const phrase of VAGUE_STANDARDS) {
666
+ if (lower.includes(phrase)) {
667
+ issues.push({
668
+ severity: "warning",
669
+ file: "AGENTS.md",
670
+ message: `Code Standards contains vague phrase: "${phrase}"`,
671
+ fix: "Replace with specific, verifiable conventions derived from config files",
672
+ });
673
+ }
674
+ }
675
+ }
676
+
677
+ // Missing test command — if no test script exists, AGENTS.md should say so
678
+ if (Object.keys(pkgScripts).length > 0) {
679
+ const hasTestScript = Object.keys(pkgScripts).some(
680
+ (k) => k === "test" || k.startsWith("test:"),
681
+ );
682
+ if (!hasTestScript) {
683
+ const mentionsNoTest = agentsMd.toLowerCase().includes("no test");
684
+ if (!mentionsNoTest) {
685
+ issues.push({
686
+ severity: "warning",
687
+ file: "AGENTS.md",
688
+ message:
689
+ "No test script in package.json and AGENTS.md doesn't mention it",
690
+ fix: 'Add a note like "No test framework. Verify changes with `bun run build`."',
691
+ });
692
+ }
693
+ }
694
+ }
695
+
696
+ // Check backtick-quoted paths
529
697
  checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
530
698
  }
531
699
 
532
- // 3b. Check backtick-quoted paths in docs/architecture.md
700
+ // ── docs/architecture.md checks ───────────────────────────────────────
701
+
533
702
  const archMd = readText(join(cwd, "docs", "architecture.md"));
534
703
  if (archMd) {
535
704
  checkBacktickPaths(archMd, "docs/architecture.md", cwd, issues);
@@ -538,6 +707,44 @@ export function verifyDocs(cwd: string): DocsIssue[] {
538
707
  return issues;
539
708
  }
540
709
 
710
+ /** Extract ## headings from markdown content. */
711
+ function extractHeadings(content: string): Set<string> {
712
+ const headings = new Set<string>();
713
+ for (const line of content.split("\n")) {
714
+ const match = line.match(/^#{2,3}\s+(.+)$/);
715
+ if (match) {
716
+ headings.add(match[1].trim());
717
+ }
718
+ }
719
+ return headings;
720
+ }
721
+
722
+ /** Extract content under a specific ## section heading. */
723
+ function extractSection(content: string, heading: string): string | null {
724
+ const lines = content.split("\n");
725
+ let capturing = false;
726
+ const result: string[] = [];
727
+
728
+ for (const line of lines) {
729
+ if (capturing) {
730
+ // Stop at next ## heading
731
+ if (line.match(/^#{1,2}\s/)) break;
732
+ result.push(line);
733
+ } else if (
734
+ line.match(
735
+ new RegExp(
736
+ `^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
737
+ "i",
738
+ ),
739
+ )
740
+ ) {
741
+ capturing = true;
742
+ }
743
+ }
744
+
745
+ return result.length > 0 ? result.join("\n") : null;
746
+ }
747
+
541
748
  /** Scan markdown for backtick-quoted paths and check they exist. */
542
749
  function checkBacktickPaths(
543
750
  content: string,
@@ -549,12 +756,18 @@ function checkBacktickPaths(
549
756
  let match: RegExpExecArray | null;
550
757
  const checked = new Set<string>();
551
758
 
759
+ const root = resolve(cwd);
760
+
552
761
  while ((match = pathRe.exec(content)) !== null) {
553
762
  const refPath = match[1].replace(/\/$/, ""); // strip trailing slash
554
763
  if (checked.has(refPath)) continue;
555
764
  checked.add(refPath);
556
765
 
557
- if (!existsSync(join(cwd, refPath))) {
766
+ // Skip paths that escape the project root (e.g. src/../../etc/hosts)
767
+ const resolvedRef = resolve(root, refPath);
768
+ if (resolvedRef !== root && !resolvedRef.startsWith(root + sep)) continue;
769
+
770
+ if (!existsSync(resolvedRef)) {
558
771
  issues.push({
559
772
  severity: "warning",
560
773
  file,