@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 +89 -39
- package/dist/index.js +89 -39
- package/dist/lib/api-client.js +22 -36
- package/package.json +1 -1
- package/src/api-client.ts +49 -34
- package/src/prompt-builder.ts +0 -67
- package/src/server.ts +90 -4
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
5772
|
+
planId,
|
|
5723
5773
|
planUrl,
|
|
5724
5774
|
plan: result.plan,
|
|
5725
5775
|
tasks: result.tasks
|
package/dist/lib/api-client.js
CHANGED
|
@@ -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
|
-
|
|
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
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<{
|
|
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
|
-
//
|
|
1550
|
-
//
|
|
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 {
|
package/src/prompt-builder.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
3738
|
+
planId,
|
|
3653
3739
|
planUrl,
|
|
3654
3740
|
plan: result.plan,
|
|
3655
3741
|
tasks: result.tasks,
|