@gethmy/mcp 2.9.1 → 2.9.3

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/dist/cli.js CHANGED
@@ -20,7 +20,6 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
  // src/prompt-builder.ts
21
21
  var exports_prompt_builder = {};
22
22
  __export(exports_prompt_builder, {
23
- proposePromptVariant: () => proposePromptVariant,
24
23
  inferCategoryFromLabels: () => inferCategoryFromLabels,
25
24
  getRoleFraming: () => getRoleFraming,
26
25
  getAvailableVariants: () => getAvailableVariants,
@@ -337,26 +336,7 @@ function getAvailableCategories() {
337
336
  function getAvailableVariants() {
338
337
  return ["analysis", "draft", "execute"];
339
338
  }
340
- async function proposePromptVariant(contentHash, fetchCohort) {
341
- if (!contentHash)
342
- return null;
343
- const cohort = await fetchCohort(contentHash);
344
- if (!cohort || cohort.length < VARIANT_MIN_COHORT)
345
- return null;
346
- const completed = cohort.filter((r) => r.status === "completed" && (r.progressPercent ?? 0) >= 100 && !r.hadBlockers).length;
347
- const completionRate = completed / cohort.length;
348
- if (completionRate >= VARIANT_COMPLETION_THRESHOLD)
349
- return null;
350
- const blockerRate = cohort.filter((r) => r.hadBlockers).length / cohort.length;
351
- const framingHint = blockerRate >= 0.4 ? "Cohort hits frequent blockers — try a more diagnostic framing (require root-cause + repro before any fix)." : "Cohort frequently stalls without finishing — try a more action-forcing framing (smaller subtasks, explicit DoD checklist).";
352
- return {
353
- contentHash,
354
- cohortSize: cohort.length,
355
- completionRate,
356
- framingHint
357
- };
358
- }
359
- var PROMPT_TEMPLATE_VERSION = 1, LABEL_CATEGORY_MAP, DEFAULT_ROLE_FRAMINGS, VARIANT_INSTRUCTIONS, VARIANT_MIN_COHORT = 10, VARIANT_COMPLETION_THRESHOLD = 0.4;
339
+ var PROMPT_TEMPLATE_VERSION = 1, LABEL_CATEGORY_MAP, DEFAULT_ROLE_FRAMINGS, VARIANT_INSTRUCTIONS;
360
340
  var init_prompt_builder = __esm(() => {
361
341
  LABEL_CATEGORY_MAP = {
362
342
  bug: "bug",
@@ -715,6 +695,10 @@ function getMemoryDir() {
715
695
  return config.memoryDir;
716
696
  return join(homedir(), ".harmony", "memory");
717
697
  }
698
+
699
+ // src/server.ts
700
+ import { readFile } from "node:fs/promises";
701
+ import { basename } from "node:path";
718
702
  // ../memory/dist/sync.js
719
703
  import { createHash } from "node:crypto";
720
704
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
@@ -1543,6 +1527,9 @@ class HarmonyApiClient {
1543
1527
  async getCardAttachments(cardId) {
1544
1528
  return this.request("GET", `/cards/${cardId}/attachments`);
1545
1529
  }
1530
+ async uploadCardAttachment(cardId, data) {
1531
+ return this.request("POST", `/cards/${cardId}/attachments`, data);
1532
+ }
1546
1533
  async getCardExternalLinks(cardId) {
1547
1534
  return this.request("GET", `/cards/${cardId}/external-links`);
1548
1535
  }
@@ -1860,16 +1847,6 @@ class HarmonyApiClient {
1860
1847
  async recordPromptHistory(data) {
1861
1848
  return this.request("POST", "/prompt-history", data);
1862
1849
  }
1863
- async recordPromptHistoryFeedback(sessionId, outcome) {
1864
- return this.request("POST", "/prompt-history/feedback", {
1865
- sessionId,
1866
- outcome
1867
- });
1868
- }
1869
- async getPromptHistoryCohort(contentHash) {
1870
- const params = new URLSearchParams({ content_hash: contentHash });
1871
- return this.request("GET", `/prompt-history/cohort?${params.toString()}`);
1872
- }
1873
1850
  async generateCardPrompt(options) {
1874
1851
  const { generatePrompt: generatePrompt2 } = await loadPromptModules();
1875
1852
  const cardResult = await this.getCard(options.cardId);
@@ -1974,6 +1951,23 @@ ${section}`;
1974
1951
  const msg = err instanceof Error ? err.message : String(err);
1975
1952
  console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
1976
1953
  }
1954
+ if (cardData.plan_id) {
1955
+ try {
1956
+ const { plan } = await this.getPlan(cardData.plan_id);
1957
+ const planContent = plan?.content;
1958
+ if (planContent?.trim()) {
1959
+ result.prompt = `${result.prompt}
1960
+
1961
+ ## Approved Plan
1962
+ This card has an approved implementation plan. Follow it unless you find a concrete reason it is wrong — if you must diverge, say so in a comment and explain why.
1963
+
1964
+ ${planContent.trim()}`;
1965
+ }
1966
+ } catch (err) {
1967
+ const msg = err instanceof Error ? err.message : String(err);
1968
+ console.debug(`[generateCardPrompt] plan fetch failed: ${msg}`);
1969
+ }
1970
+ }
1977
1971
  try {
1978
1972
  await this.recordPromptHistory({
1979
1973
  cardId: cardData.id,
@@ -1983,11 +1977,7 @@ ${section}`;
1983
1977
  assemblyId: result.assemblyId ?? null,
1984
1978
  tokenEstimate: result.tokenEstimate,
1985
1979
  contextSummary: result.contextSummary
1986
- },
1987
- sessionId: options.sessionId ?? null,
1988
- contentHash: result.contentHash,
1989
- templateVersion: result.version,
1990
- confidence: 0.5
1980
+ }
1991
1981
  });
1992
1982
  } catch (err) {
1993
1983
  const msg = err instanceof Error ? err.message : String(err);
@@ -3200,7 +3190,7 @@ var TOOLS = {
3200
3190
  title: { type: "string", description: "Card title" },
3201
3191
  columnId: {
3202
3192
  type: "string",
3203
- description: "Target column ID (optional, defaults to first column)"
3193
+ description: "Target column ID (optional, defaults to the project's default column, or the first column if none is set)"
3204
3194
  },
3205
3195
  projectId: {
3206
3196
  type: "string",
@@ -3502,6 +3492,32 @@ var TOOLS = {
3502
3492
  required: ["cardId"]
3503
3493
  }
3504
3494
  },
3495
+ harmony_upload_card_attachment: {
3496
+ description: "Upload a file attachment to a card (e.g. a pasted screenshot or a document). Provide the file either as `filePath` (a local path the MCP server can read — works in local/stdio mode) or as `base64Data` (raw base64 bytes — works everywhere, including remote mode). Max 5MB. Allowed: PNG, JPEG, GIF, WebP, HEIC/HEIF, PDF, DOC/DOCX, XLS/XLSX, TXT. Returns the stored attachment with a short-lived signed URL.",
3497
+ inputSchema: {
3498
+ type: "object",
3499
+ properties: {
3500
+ cardId: { type: "string", description: "Card UUID" },
3501
+ filePath: {
3502
+ type: "string",
3503
+ description: "Absolute path to a local file the MCP server process can read. Mutually exclusive with base64Data."
3504
+ },
3505
+ base64Data: {
3506
+ type: "string",
3507
+ description: "Base64-encoded file bytes (a `data:` URL prefix is accepted and stripped). Requires fileName. Mutually exclusive with filePath."
3508
+ },
3509
+ fileName: {
3510
+ type: "string",
3511
+ description: "File name including extension (e.g. 'screenshot.png'). Required with base64Data; defaults to the basename of filePath otherwise."
3512
+ },
3513
+ contentType: {
3514
+ type: "string",
3515
+ description: "Optional MIME type (e.g. 'image/png'). Inferred from the file extension when omitted."
3516
+ }
3517
+ },
3518
+ required: ["cardId"]
3519
+ }
3520
+ },
3505
3521
  harmony_get_card_external_links: {
3506
3522
  description: "Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
3507
3523
  inputSchema: {
@@ -4920,6 +4936,38 @@ async function handleToolCall(name, args, deps) {
4920
4936
  const result = await client3.getCardAttachments(cardId);
4921
4937
  return result;
4922
4938
  }
4939
+ case "harmony_upload_card_attachment": {
4940
+ const cardId = z.string().uuid().parse(args.cardId);
4941
+ const filePath = args.filePath != null ? z.string().parse(args.filePath) : undefined;
4942
+ const base64Data = args.base64Data != null ? z.string().parse(args.base64Data) : undefined;
4943
+ let fileName = args.fileName != null ? z.string().parse(args.fileName) : undefined;
4944
+ const contentType = args.contentType != null ? z.string().parse(args.contentType) : undefined;
4945
+ if (filePath && base64Data) {
4946
+ throw new Error("Provide either filePath or base64Data, not both.");
4947
+ }
4948
+ let data;
4949
+ if (filePath) {
4950
+ const bytes = await readFile(filePath);
4951
+ if (bytes.byteLength === 0) {
4952
+ throw new Error(`File is empty: ${filePath}`);
4953
+ }
4954
+ data = bytes.toString("base64");
4955
+ fileName = fileName || basename(filePath);
4956
+ } else if (base64Data) {
4957
+ if (!fileName) {
4958
+ throw new Error("fileName is required when using base64Data.");
4959
+ }
4960
+ data = base64Data;
4961
+ } else {
4962
+ throw new Error("Provide either filePath or base64Data.");
4963
+ }
4964
+ const result = await client3.uploadCardAttachment(cardId, {
4965
+ fileName,
4966
+ data,
4967
+ fileType: contentType
4968
+ });
4969
+ return result;
4970
+ }
4923
4971
  case "harmony_get_card_external_links": {
4924
4972
  const cardId = z.string().uuid().parse(args.cardId);
4925
4973
  const result = await client3.getCardExternalLinks(cardId);
@@ -5720,10 +5768,12 @@ async function handleToolCall(name, args, deps) {
5720
5768
  source: args.source || "agent",
5721
5769
  tasks: args.tasks
5722
5770
  });
5723
- const planUrl = `https://app.gethmy.com/plans/${result.plan.id}`;
5771
+ const planId = result.plan.id;
5772
+ const { workspaceSlug, projectSlug } = result;
5773
+ const planUrl = workspaceSlug && projectSlug ? `https://app.gethmy.com/${workspaceSlug}/${projectSlug}/plans/${planId}` : `https://app.gethmy.com/plans/${planId}`;
5724
5774
  return {
5725
5775
  success: true,
5726
- planId: result.plan.id,
5776
+ planId,
5727
5777
  planUrl,
5728
5778
  plan: result.plan,
5729
5779
  tasks: result.tasks
package/dist/index.js CHANGED
@@ -20,7 +20,6 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
  // src/prompt-builder.ts
21
21
  var exports_prompt_builder = {};
22
22
  __export(exports_prompt_builder, {
23
- proposePromptVariant: () => proposePromptVariant,
24
23
  inferCategoryFromLabels: () => inferCategoryFromLabels,
25
24
  getRoleFraming: () => getRoleFraming,
26
25
  getAvailableVariants: () => getAvailableVariants,
@@ -337,26 +336,7 @@ function getAvailableCategories() {
337
336
  function getAvailableVariants() {
338
337
  return ["analysis", "draft", "execute"];
339
338
  }
340
- async function proposePromptVariant(contentHash, fetchCohort) {
341
- if (!contentHash)
342
- return null;
343
- const cohort = await fetchCohort(contentHash);
344
- if (!cohort || cohort.length < VARIANT_MIN_COHORT)
345
- return null;
346
- const completed = cohort.filter((r) => r.status === "completed" && (r.progressPercent ?? 0) >= 100 && !r.hadBlockers).length;
347
- const completionRate = completed / cohort.length;
348
- if (completionRate >= VARIANT_COMPLETION_THRESHOLD)
349
- return null;
350
- const blockerRate = cohort.filter((r) => r.hadBlockers).length / cohort.length;
351
- const framingHint = blockerRate >= 0.4 ? "Cohort hits frequent blockers — try a more diagnostic framing (require root-cause + repro before any fix)." : "Cohort frequently stalls without finishing — try a more action-forcing framing (smaller subtasks, explicit DoD checklist).";
352
- return {
353
- contentHash,
354
- cohortSize: cohort.length,
355
- completionRate,
356
- framingHint
357
- };
358
- }
359
- var PROMPT_TEMPLATE_VERSION = 1, LABEL_CATEGORY_MAP, DEFAULT_ROLE_FRAMINGS, VARIANT_INSTRUCTIONS, VARIANT_MIN_COHORT = 10, VARIANT_COMPLETION_THRESHOLD = 0.4;
339
+ var PROMPT_TEMPLATE_VERSION = 1, LABEL_CATEGORY_MAP, DEFAULT_ROLE_FRAMINGS, VARIANT_INSTRUCTIONS;
360
340
  var init_prompt_builder = __esm(() => {
361
341
  LABEL_CATEGORY_MAP = {
362
342
  bug: "bug",
@@ -526,6 +506,10 @@ var init_prompt_builder = __esm(() => {
526
506
  execute: `EXECUTE MODE: Implement this task completely. Write production-ready code following best practices. Include necessary tests and documentation.`
527
507
  };
528
508
  });
509
+
510
+ // src/server.ts
511
+ import { readFile } from "node:fs/promises";
512
+ import { basename } from "node:path";
529
513
  // ../memory/dist/sync.js
530
514
  import { createHash } from "node:crypto";
531
515
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -1539,6 +1523,9 @@ class HarmonyApiClient {
1539
1523
  async getCardAttachments(cardId) {
1540
1524
  return this.request("GET", `/cards/${cardId}/attachments`);
1541
1525
  }
1526
+ async uploadCardAttachment(cardId, data) {
1527
+ return this.request("POST", `/cards/${cardId}/attachments`, data);
1528
+ }
1542
1529
  async getCardExternalLinks(cardId) {
1543
1530
  return this.request("GET", `/cards/${cardId}/external-links`);
1544
1531
  }
@@ -1856,16 +1843,6 @@ class HarmonyApiClient {
1856
1843
  async recordPromptHistory(data) {
1857
1844
  return this.request("POST", "/prompt-history", data);
1858
1845
  }
1859
- async recordPromptHistoryFeedback(sessionId, outcome) {
1860
- return this.request("POST", "/prompt-history/feedback", {
1861
- sessionId,
1862
- outcome
1863
- });
1864
- }
1865
- async getPromptHistoryCohort(contentHash) {
1866
- const params = new URLSearchParams({ content_hash: contentHash });
1867
- return this.request("GET", `/prompt-history/cohort?${params.toString()}`);
1868
- }
1869
1846
  async generateCardPrompt(options) {
1870
1847
  const { generatePrompt: generatePrompt2 } = await loadPromptModules();
1871
1848
  const cardResult = await this.getCard(options.cardId);
@@ -1970,6 +1947,23 @@ ${section}`;
1970
1947
  const msg = err instanceof Error ? err.message : String(err);
1971
1948
  console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
1972
1949
  }
1950
+ if (cardData.plan_id) {
1951
+ try {
1952
+ const { plan } = await this.getPlan(cardData.plan_id);
1953
+ const planContent = plan?.content;
1954
+ if (planContent?.trim()) {
1955
+ result.prompt = `${result.prompt}
1956
+
1957
+ ## Approved Plan
1958
+ This card has an approved implementation plan. Follow it unless you find a concrete reason it is wrong — if you must diverge, say so in a comment and explain why.
1959
+
1960
+ ${planContent.trim()}`;
1961
+ }
1962
+ } catch (err) {
1963
+ const msg = err instanceof Error ? err.message : String(err);
1964
+ console.debug(`[generateCardPrompt] plan fetch failed: ${msg}`);
1965
+ }
1966
+ }
1973
1967
  try {
1974
1968
  await this.recordPromptHistory({
1975
1969
  cardId: cardData.id,
@@ -1979,11 +1973,7 @@ ${section}`;
1979
1973
  assemblyId: result.assemblyId ?? null,
1980
1974
  tokenEstimate: result.tokenEstimate,
1981
1975
  contextSummary: result.contextSummary
1982
- },
1983
- sessionId: options.sessionId ?? null,
1984
- contentHash: result.contentHash,
1985
- templateVersion: result.version,
1986
- confidence: 0.5
1976
+ }
1987
1977
  });
1988
1978
  } catch (err) {
1989
1979
  const msg = err instanceof Error ? err.message : String(err);
@@ -3196,7 +3186,7 @@ var TOOLS = {
3196
3186
  title: { type: "string", description: "Card title" },
3197
3187
  columnId: {
3198
3188
  type: "string",
3199
- description: "Target column ID (optional, defaults to first column)"
3189
+ description: "Target column ID (optional, defaults to the project's default column, or the first column if none is set)"
3200
3190
  },
3201
3191
  projectId: {
3202
3192
  type: "string",
@@ -3498,6 +3488,32 @@ var TOOLS = {
3498
3488
  required: ["cardId"]
3499
3489
  }
3500
3490
  },
3491
+ harmony_upload_card_attachment: {
3492
+ description: "Upload a file attachment to a card (e.g. a pasted screenshot or a document). Provide the file either as `filePath` (a local path the MCP server can read — works in local/stdio mode) or as `base64Data` (raw base64 bytes — works everywhere, including remote mode). Max 5MB. Allowed: PNG, JPEG, GIF, WebP, HEIC/HEIF, PDF, DOC/DOCX, XLS/XLSX, TXT. Returns the stored attachment with a short-lived signed URL.",
3493
+ inputSchema: {
3494
+ type: "object",
3495
+ properties: {
3496
+ cardId: { type: "string", description: "Card UUID" },
3497
+ filePath: {
3498
+ type: "string",
3499
+ description: "Absolute path to a local file the MCP server process can read. Mutually exclusive with base64Data."
3500
+ },
3501
+ base64Data: {
3502
+ type: "string",
3503
+ description: "Base64-encoded file bytes (a `data:` URL prefix is accepted and stripped). Requires fileName. Mutually exclusive with filePath."
3504
+ },
3505
+ fileName: {
3506
+ type: "string",
3507
+ description: "File name including extension (e.g. 'screenshot.png'). Required with base64Data; defaults to the basename of filePath otherwise."
3508
+ },
3509
+ contentType: {
3510
+ type: "string",
3511
+ description: "Optional MIME type (e.g. 'image/png'). Inferred from the file extension when omitted."
3512
+ }
3513
+ },
3514
+ required: ["cardId"]
3515
+ }
3516
+ },
3501
3517
  harmony_get_card_external_links: {
3502
3518
  description: "Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
3503
3519
  inputSchema: {
@@ -4916,6 +4932,38 @@ async function handleToolCall(name, args, deps) {
4916
4932
  const result = await client3.getCardAttachments(cardId);
4917
4933
  return result;
4918
4934
  }
4935
+ case "harmony_upload_card_attachment": {
4936
+ const cardId = z.string().uuid().parse(args.cardId);
4937
+ const filePath = args.filePath != null ? z.string().parse(args.filePath) : undefined;
4938
+ const base64Data = args.base64Data != null ? z.string().parse(args.base64Data) : undefined;
4939
+ let fileName = args.fileName != null ? z.string().parse(args.fileName) : undefined;
4940
+ const contentType = args.contentType != null ? z.string().parse(args.contentType) : undefined;
4941
+ if (filePath && base64Data) {
4942
+ throw new Error("Provide either filePath or base64Data, not both.");
4943
+ }
4944
+ let data;
4945
+ if (filePath) {
4946
+ const bytes = await readFile(filePath);
4947
+ if (bytes.byteLength === 0) {
4948
+ throw new Error(`File is empty: ${filePath}`);
4949
+ }
4950
+ data = bytes.toString("base64");
4951
+ fileName = fileName || basename(filePath);
4952
+ } else if (base64Data) {
4953
+ if (!fileName) {
4954
+ throw new Error("fileName is required when using base64Data.");
4955
+ }
4956
+ data = base64Data;
4957
+ } else {
4958
+ throw new Error("Provide either filePath or base64Data.");
4959
+ }
4960
+ const result = await client3.uploadCardAttachment(cardId, {
4961
+ fileName,
4962
+ data,
4963
+ fileType: contentType
4964
+ });
4965
+ return result;
4966
+ }
4919
4967
  case "harmony_get_card_external_links": {
4920
4968
  const cardId = z.string().uuid().parse(args.cardId);
4921
4969
  const result = await client3.getCardExternalLinks(cardId);
@@ -5716,10 +5764,12 @@ async function handleToolCall(name, args, deps) {
5716
5764
  source: args.source || "agent",
5717
5765
  tasks: args.tasks
5718
5766
  });
5719
- const planUrl = `https://app.gethmy.com/plans/${result.plan.id}`;
5767
+ const planId = result.plan.id;
5768
+ const { workspaceSlug, projectSlug } = result;
5769
+ const planUrl = workspaceSlug && projectSlug ? `https://app.gethmy.com/${workspaceSlug}/${projectSlug}/plans/${planId}` : `https://app.gethmy.com/plans/${planId}`;
5720
5770
  return {
5721
5771
  success: true,
5722
- planId: result.plan.id,
5772
+ planId,
5723
5773
  planUrl,
5724
5774
  plan: result.plan,
5725
5775
  tasks: result.tasks
@@ -17,7 +17,6 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  // src/prompt-builder.ts
18
18
  var exports_prompt_builder = {};
19
19
  __export(exports_prompt_builder, {
20
- proposePromptVariant: () => proposePromptVariant,
21
20
  inferCategoryFromLabels: () => inferCategoryFromLabels,
22
21
  getRoleFraming: () => getRoleFraming,
23
22
  getAvailableVariants: () => getAvailableVariants,
@@ -334,26 +333,7 @@ function getAvailableCategories() {
334
333
  function getAvailableVariants() {
335
334
  return ["analysis", "draft", "execute"];
336
335
  }
337
- async function proposePromptVariant(contentHash, fetchCohort) {
338
- if (!contentHash)
339
- return null;
340
- const cohort = await fetchCohort(contentHash);
341
- if (!cohort || cohort.length < VARIANT_MIN_COHORT)
342
- return null;
343
- const completed = cohort.filter((r) => r.status === "completed" && (r.progressPercent ?? 0) >= 100 && !r.hadBlockers).length;
344
- const completionRate = completed / cohort.length;
345
- if (completionRate >= VARIANT_COMPLETION_THRESHOLD)
346
- return null;
347
- const blockerRate = cohort.filter((r) => r.hadBlockers).length / cohort.length;
348
- const framingHint = blockerRate >= 0.4 ? "Cohort hits frequent blockers — try a more diagnostic framing (require root-cause + repro before any fix)." : "Cohort frequently stalls without finishing — try a more action-forcing framing (smaller subtasks, explicit DoD checklist).";
349
- return {
350
- contentHash,
351
- cohortSize: cohort.length,
352
- completionRate,
353
- framingHint
354
- };
355
- }
356
- var PROMPT_TEMPLATE_VERSION = 1, LABEL_CATEGORY_MAP, DEFAULT_ROLE_FRAMINGS, VARIANT_INSTRUCTIONS, VARIANT_MIN_COHORT = 10, VARIANT_COMPLETION_THRESHOLD = 0.4;
336
+ var PROMPT_TEMPLATE_VERSION = 1, LABEL_CATEGORY_MAP, DEFAULT_ROLE_FRAMINGS, VARIANT_INSTRUCTIONS;
357
337
  var init_prompt_builder = __esm(() => {
358
338
  LABEL_CATEGORY_MAP = {
359
339
  bug: "bug",
@@ -1146,6 +1126,9 @@ class HarmonyApiClient {
1146
1126
  async getCardAttachments(cardId) {
1147
1127
  return this.request("GET", `/cards/${cardId}/attachments`);
1148
1128
  }
1129
+ async uploadCardAttachment(cardId, data) {
1130
+ return this.request("POST", `/cards/${cardId}/attachments`, data);
1131
+ }
1149
1132
  async getCardExternalLinks(cardId) {
1150
1133
  return this.request("GET", `/cards/${cardId}/external-links`);
1151
1134
  }
@@ -1463,16 +1446,6 @@ class HarmonyApiClient {
1463
1446
  async recordPromptHistory(data) {
1464
1447
  return this.request("POST", "/prompt-history", data);
1465
1448
  }
1466
- async recordPromptHistoryFeedback(sessionId, outcome) {
1467
- return this.request("POST", "/prompt-history/feedback", {
1468
- sessionId,
1469
- outcome
1470
- });
1471
- }
1472
- async getPromptHistoryCohort(contentHash) {
1473
- const params = new URLSearchParams({ content_hash: contentHash });
1474
- return this.request("GET", `/prompt-history/cohort?${params.toString()}`);
1475
- }
1476
1449
  async generateCardPrompt(options) {
1477
1450
  const { generatePrompt: generatePrompt2 } = await loadPromptModules();
1478
1451
  const cardResult = await this.getCard(options.cardId);
@@ -1577,6 +1550,23 @@ ${section}`;
1577
1550
  const msg = err instanceof Error ? err.message : String(err);
1578
1551
  console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
1579
1552
  }
1553
+ if (cardData.plan_id) {
1554
+ try {
1555
+ const { plan } = await this.getPlan(cardData.plan_id);
1556
+ const planContent = plan?.content;
1557
+ if (planContent?.trim()) {
1558
+ result.prompt = `${result.prompt}
1559
+
1560
+ ## Approved Plan
1561
+ This card has an approved implementation plan. Follow it unless you find a concrete reason it is wrong — if you must diverge, say so in a comment and explain why.
1562
+
1563
+ ${planContent.trim()}`;
1564
+ }
1565
+ } catch (err) {
1566
+ const msg = err instanceof Error ? err.message : String(err);
1567
+ console.debug(`[generateCardPrompt] plan fetch failed: ${msg}`);
1568
+ }
1569
+ }
1580
1570
  try {
1581
1571
  await this.recordPromptHistory({
1582
1572
  cardId: cardData.id,
@@ -1586,11 +1576,7 @@ ${section}`;
1586
1576
  assemblyId: result.assemblyId ?? null,
1587
1577
  tokenEstimate: result.tokenEstimate,
1588
1578
  contextSummary: result.contextSummary
1589
- },
1590
- sessionId: options.sessionId ?? null,
1591
- contentHash: result.contentHash,
1592
- templateVersion: result.version,
1593
- confidence: 0.5
1579
+ }
1594
1580
  });
1595
1581
  } catch (err) {
1596
1582
  const msg = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.9.1",
3
+ "version": "2.9.3",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/api-client.ts CHANGED
@@ -486,10 +486,12 @@ export class HarmonyApiClient {
486
486
  description?: string;
487
487
  priority?: string;
488
488
  assigneeId?: string | null;
489
+ assignedAgentId?: string | null;
489
490
  dueDate?: string | null;
490
491
  done?: boolean;
491
492
  archivedAt?: string | null;
492
493
  planId?: string | null;
494
+ needsPlanRefresh?: boolean;
493
495
  },
494
496
  ): Promise<{ card: unknown }> {
495
497
  return this.request("PATCH", `/cards/${cardId}`, updates);
@@ -581,6 +583,13 @@ export class HarmonyApiClient {
581
583
  return this.request("GET", `/cards/${cardId}/attachments`);
582
584
  }
583
585
 
586
+ async uploadCardAttachment(
587
+ cardId: string,
588
+ data: { fileName: string; data: string; fileType?: string },
589
+ ): Promise<{ attachment: CardAttachment }> {
590
+ return this.request("POST", `/cards/${cardId}/attachments`, data);
591
+ }
592
+
584
593
  async getCardExternalLinks(
585
594
  cardId: string,
586
595
  ): Promise<{ external_links: CardExternalLinkRow[] }> {
@@ -734,6 +743,10 @@ export class HarmonyApiClient {
734
743
  | "budget"
735
744
  | "timeout"
736
745
  | "stale"
746
+ | "rate_limit"
747
+ | "out_of_credits"
748
+ | "usage_limit"
749
+ | "auth"
737
750
  | "other";
738
751
  failureSummary?: string;
739
752
  recoveryBranch?: string;
@@ -762,6 +775,10 @@ export class HarmonyApiClient {
762
775
  | "budget"
763
776
  | "timeout"
764
777
  | "stale"
778
+ | "rate_limit"
779
+ | "out_of_credits"
780
+ | "usage_limit"
781
+ | "auth"
765
782
  | "other";
766
783
  failureSummary?: string;
767
784
  recoveryBranch?: string;
@@ -1215,7 +1232,12 @@ export class HarmonyApiClient {
1215
1232
  status?: "pending" | "in_progress" | "completed";
1216
1233
  }>;
1217
1234
  },
1218
- ): Promise<{ plan: unknown; tasks?: unknown[] }> {
1235
+ ): Promise<{
1236
+ plan: unknown;
1237
+ tasks?: unknown[];
1238
+ projectSlug?: string | null;
1239
+ workspaceSlug?: string | null;
1240
+ }> {
1219
1241
  return this.request("POST", "/plans", { projectId, ...data });
1220
1242
  }
1221
1243
 
@@ -1322,37 +1344,12 @@ export class HarmonyApiClient {
1322
1344
  generatedPrompt: string;
1323
1345
  variant: "analysis" | "draft" | "execute";
1324
1346
  contextIncluded?: Record<string, unknown>;
1325
- sessionId?: string | null;
1326
- contentHash?: string;
1327
- templateVersion?: number;
1328
- confidence?: number;
1329
1347
  templateId?: string | null;
1330
1348
  isPinned?: boolean;
1331
1349
  }): Promise<{ entry: unknown }> {
1332
1350
  return this.request("POST", "/prompt-history", data);
1333
1351
  }
1334
1352
 
1335
- async recordPromptHistoryFeedback(
1336
- sessionId: string,
1337
- outcome: "success" | "blocker" | "neutral",
1338
- ): Promise<{ adjusted: number }> {
1339
- return this.request("POST", "/prompt-history/feedback", {
1340
- sessionId,
1341
- outcome,
1342
- });
1343
- }
1344
-
1345
- async getPromptHistoryCohort(contentHash: string): Promise<{
1346
- cohort: Array<{
1347
- status: string | null;
1348
- progressPercent: number | null;
1349
- hadBlockers: boolean;
1350
- }>;
1351
- }> {
1352
- const params = new URLSearchParams({ content_hash: contentHash });
1353
- return this.request("GET", `/prompt-history/cohort?${params.toString()}`);
1354
- }
1355
-
1356
1353
  // ============ PROMPT GENERATION ============
1357
1354
 
1358
1355
  /**
@@ -1373,8 +1370,6 @@ export class HarmonyApiClient {
1373
1370
  includeLinks: boolean;
1374
1371
  includeDescription: boolean;
1375
1372
  }>;
1376
- /** Optional active session ID to associate with the prompt snapshot. */
1377
- sessionId?: string | null;
1378
1373
  }): Promise<{
1379
1374
  prompt: string;
1380
1375
  variant: string;
@@ -1546,8 +1541,30 @@ export class HarmonyApiClient {
1546
1541
  console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
1547
1542
  }
1548
1543
 
1549
- // AGP P2: persist a session-linked snapshot. Best-effort never fail
1550
- // prompt generation just because logging didn't land.
1544
+ // Surface a linked plan's full markdown (e.g. a plan the agent daemon
1545
+ // produced and a human approved in gated mode, or any plan attached via
1546
+ // cards.plan_id). This is the central injection point so EVERY caller —
1547
+ // daemon implement run, Claude Code, in-app builder — sees the approved
1548
+ // approach, not just a reference. Best-effort: never fail prompt generation
1549
+ // on a plan fetch error.
1550
+ if (cardData.plan_id) {
1551
+ try {
1552
+ const { plan } = await this.getPlan(cardData.plan_id);
1553
+ const planContent = (plan as { content?: string | null } | null)
1554
+ ?.content;
1555
+ if (planContent?.trim()) {
1556
+ result.prompt = `${result.prompt}\n\n## Approved Plan\nThis card has an approved implementation plan. Follow it unless you find a concrete reason it is wrong — if you must diverge, say so in a comment and explain why.\n\n${planContent.trim()}`;
1557
+ }
1558
+ } catch (err) {
1559
+ const msg = err instanceof Error ? err.message : String(err);
1560
+ console.debug(`[generateCardPrompt] plan fetch failed: ${msg}`);
1561
+ }
1562
+ }
1563
+
1564
+ // Persist a snapshot of the generated prompt. Best-effort — never fail
1565
+ // prompt generation just because logging didn't land. (The AGP-era
1566
+ // session_id / content_hash / template_version / confidence columns were
1567
+ // dropped in migration 20260508000000 — card #329.)
1551
1568
  try {
1552
1569
  await this.recordPromptHistory({
1553
1570
  cardId: cardData.id,
@@ -1558,10 +1575,6 @@ export class HarmonyApiClient {
1558
1575
  tokenEstimate: result.tokenEstimate,
1559
1576
  contextSummary: result.contextSummary,
1560
1577
  },
1561
- sessionId: options.sessionId ?? null,
1562
- contentHash: result.contentHash,
1563
- templateVersion: result.version,
1564
- confidence: 0.5,
1565
1578
  });
1566
1579
  } catch (err) {
1567
1580
  const msg = err instanceof Error ? err.message : String(err);
@@ -1610,6 +1623,8 @@ interface CardPromptData {
1610
1623
  }>;
1611
1624
  column_id?: string;
1612
1625
  project_id?: string;
1626
+ /** Linked plan (e.g. an agent-approved plan). Surfaced in the prompt. */
1627
+ plan_id?: string | null;
1613
1628
  }
1614
1629
 
1615
1630
  interface MemoryItem {
@@ -773,70 +773,3 @@ export function getAvailableCategories(): LabelCategory[] {
773
773
  export function getAvailableVariants(): PromptVariant[] {
774
774
  return ["analysis", "draft", "execute"];
775
775
  }
776
-
777
- // ─── Variant proposal (logged-only — no auto-commit) ──────────────────
778
-
779
- /** Cohort row shape consumed by {@link proposePromptVariant}. */
780
- export interface PromptCohortRow {
781
- /** Final agent session status — only "completed" is treated as success. */
782
- status: string | null;
783
- /** Final progress percent recorded on the linked session, when present. */
784
- progressPercent: number | null;
785
- /** Whether the linked session ended with non-empty blockers. */
786
- hadBlockers: boolean;
787
- }
788
-
789
- export interface PromptVariantSuggestion {
790
- contentHash: string;
791
- cohortSize: number;
792
- completionRate: number;
793
- framingHint: string;
794
- }
795
-
796
- const VARIANT_MIN_COHORT = 10;
797
- const VARIANT_COMPLETION_THRESHOLD = 0.4;
798
-
799
- /**
800
- * Propose an alternative framing for prompts with a given content hash, based
801
- * on observed session outcomes. Returns null when the cohort is too small or
802
- * the completion rate is acceptable.
803
- *
804
- * Per the AGP-P2 locked decision, this is logged-only — callers may surface
805
- * the suggestion to humans, but no auto-commit of new templates is allowed.
806
- *
807
- * @param fetchCohort — async loader that returns one row per session that
808
- * consumed a prompt with this hash. Keeps this module decoupled from the
809
- * API client so it stays pure-testable.
810
- */
811
- export async function proposePromptVariant(
812
- contentHash: string,
813
- fetchCohort: (hash: string) => Promise<PromptCohortRow[]>,
814
- ): Promise<PromptVariantSuggestion | null> {
815
- if (!contentHash) return null;
816
- const cohort = await fetchCohort(contentHash);
817
- if (!cohort || cohort.length < VARIANT_MIN_COHORT) return null;
818
-
819
- const completed = cohort.filter(
820
- (r) =>
821
- r.status === "completed" &&
822
- (r.progressPercent ?? 0) >= 100 &&
823
- !r.hadBlockers,
824
- ).length;
825
- const completionRate = completed / cohort.length;
826
-
827
- if (completionRate >= VARIANT_COMPLETION_THRESHOLD) return null;
828
-
829
- const blockerRate =
830
- cohort.filter((r) => r.hadBlockers).length / cohort.length;
831
- const framingHint =
832
- blockerRate >= 0.4
833
- ? "Cohort hits frequent blockers — try a more diagnostic framing (require root-cause + repro before any fix)."
834
- : "Cohort frequently stalls without finishing — try a more action-forcing framing (smaller subtasks, explicit DoD checklist).";
835
-
836
- return {
837
- contentHash,
838
- cohortSize: cohort.length,
839
- completionRate,
840
- framingHint,
841
- };
842
- }
package/src/server.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename } from "node:path";
1
3
  import { syncFull, syncPull, syncPush } from "@harmony/memory";
2
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -295,7 +297,8 @@ export const TOOLS = {
295
297
  title: { type: "string", description: "Card title" },
296
298
  columnId: {
297
299
  type: "string",
298
- description: "Target column ID (optional, defaults to first column)",
300
+ description:
301
+ "Target column ID (optional, defaults to the project's default column, or the first column if none is set)",
299
302
  },
300
303
  projectId: {
301
304
  type: "string",
@@ -615,6 +618,37 @@ export const TOOLS = {
615
618
  required: ["cardId"],
616
619
  },
617
620
  },
621
+ harmony_upload_card_attachment: {
622
+ description:
623
+ "Upload a file attachment to a card (e.g. a pasted screenshot or a document). Provide the file either as `filePath` (a local path the MCP server can read — works in local/stdio mode) or as `base64Data` (raw base64 bytes — works everywhere, including remote mode). Max 5MB. Allowed: PNG, JPEG, GIF, WebP, HEIC/HEIF, PDF, DOC/DOCX, XLS/XLSX, TXT. Returns the stored attachment with a short-lived signed URL.",
624
+ inputSchema: {
625
+ type: "object",
626
+ properties: {
627
+ cardId: { type: "string", description: "Card UUID" },
628
+ filePath: {
629
+ type: "string",
630
+ description:
631
+ "Absolute path to a local file the MCP server process can read. Mutually exclusive with base64Data.",
632
+ },
633
+ base64Data: {
634
+ type: "string",
635
+ description:
636
+ "Base64-encoded file bytes (a `data:` URL prefix is accepted and stripped). Requires fileName. Mutually exclusive with filePath.",
637
+ },
638
+ fileName: {
639
+ type: "string",
640
+ description:
641
+ "File name including extension (e.g. 'screenshot.png'). Required with base64Data; defaults to the basename of filePath otherwise.",
642
+ },
643
+ contentType: {
644
+ type: "string",
645
+ description:
646
+ "Optional MIME type (e.g. 'image/png'). Inferred from the file extension when omitted.",
647
+ },
648
+ },
649
+ required: ["cardId"],
650
+ },
651
+ },
618
652
  harmony_get_card_external_links: {
619
653
  description:
620
654
  "Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
@@ -2343,6 +2377,48 @@ async function handleToolCall(
2343
2377
  return result;
2344
2378
  }
2345
2379
 
2380
+ case "harmony_upload_card_attachment": {
2381
+ const cardId = z.string().uuid().parse(args.cardId);
2382
+ const filePath =
2383
+ args.filePath != null ? z.string().parse(args.filePath) : undefined;
2384
+ const base64Data =
2385
+ args.base64Data != null ? z.string().parse(args.base64Data) : undefined;
2386
+ let fileName =
2387
+ args.fileName != null ? z.string().parse(args.fileName) : undefined;
2388
+ const contentType =
2389
+ args.contentType != null
2390
+ ? z.string().parse(args.contentType)
2391
+ : undefined;
2392
+
2393
+ if (filePath && base64Data) {
2394
+ throw new Error("Provide either filePath or base64Data, not both.");
2395
+ }
2396
+
2397
+ let data: string;
2398
+ if (filePath) {
2399
+ const bytes = await readFile(filePath);
2400
+ if (bytes.byteLength === 0) {
2401
+ throw new Error(`File is empty: ${filePath}`);
2402
+ }
2403
+ data = bytes.toString("base64");
2404
+ fileName = fileName || basename(filePath);
2405
+ } else if (base64Data) {
2406
+ if (!fileName) {
2407
+ throw new Error("fileName is required when using base64Data.");
2408
+ }
2409
+ data = base64Data;
2410
+ } else {
2411
+ throw new Error("Provide either filePath or base64Data.");
2412
+ }
2413
+
2414
+ const result = await client.uploadCardAttachment(cardId, {
2415
+ fileName: fileName as string,
2416
+ data,
2417
+ fileType: contentType,
2418
+ });
2419
+ return result;
2420
+ }
2421
+
2346
2422
  case "harmony_get_card_external_links": {
2347
2423
  const cardId = z.string().uuid().parse(args.cardId);
2348
2424
  const result = await client.getCardExternalLinks(cardId);
@@ -3644,12 +3720,22 @@ async function handleToolCall(
3644
3720
  | undefined,
3645
3721
  });
3646
3722
 
3647
- // Build URL for viewing the plan
3648
- const planUrl = `https://app.gethmy.com/plans/${(result.plan as { id: string }).id}`;
3723
+ // Build URL for viewing the plan. The real route is project-scoped
3724
+ // (/{workspaceSlug}/{projectSlug}/plans/{planId}); fall back to the flat
3725
+ // path only if the API didn't return slugs (older deployments).
3726
+ const planId = (result.plan as { id: string }).id;
3727
+ const { workspaceSlug, projectSlug } = result as {
3728
+ workspaceSlug?: string | null;
3729
+ projectSlug?: string | null;
3730
+ };
3731
+ const planUrl =
3732
+ workspaceSlug && projectSlug
3733
+ ? `https://app.gethmy.com/${workspaceSlug}/${projectSlug}/plans/${planId}`
3734
+ : `https://app.gethmy.com/plans/${planId}`;
3649
3735
 
3650
3736
  return {
3651
3737
  success: true,
3652
- planId: (result.plan as { id: string }).id,
3738
+ planId,
3653
3739
  planUrl,
3654
3740
  plan: result.plan,
3655
3741
  tasks: result.tasks,