@gethmy/mcp 2.12.0 → 2.13.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/dist/cli.js +357 -53
- package/dist/index.js +355 -51
- package/dist/lib/api-client.js +21 -0
- package/package.json +1 -1
- package/src/api-client.ts +101 -0
- package/src/server.ts +509 -72
package/dist/cli.js
CHANGED
|
@@ -990,6 +990,7 @@ import { createRequire as createRequire2 } from "node:module";
|
|
|
990
990
|
import { program } from "commander";
|
|
991
991
|
|
|
992
992
|
// src/server.ts
|
|
993
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
993
994
|
import { readFile } from "node:fs/promises";
|
|
994
995
|
import { basename } from "node:path";
|
|
995
996
|
// ../memory/dist/sync.js
|
|
@@ -1496,6 +1497,9 @@ var TIMINGS = {
|
|
|
1496
1497
|
QUERY_STALE_TIME: 1000 * 60 * 5,
|
|
1497
1498
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
1498
1499
|
};
|
|
1500
|
+
// ../harmony-shared/dist/stageHandoff.js
|
|
1501
|
+
var HANDOFF_MARKER = "harmony:stage-handoff";
|
|
1502
|
+
var HANDOFF_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + HANDOFF_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
|
|
1499
1503
|
// src/api-client.ts
|
|
1500
1504
|
init_config();
|
|
1501
1505
|
var RETRY_CONFIG = {
|
|
@@ -1855,12 +1859,24 @@ class HarmonyApiClient {
|
|
|
1855
1859
|
async uploadCardAttachment(cardId, data) {
|
|
1856
1860
|
return this.request("POST", `/cards/${cardId}/attachments`, data);
|
|
1857
1861
|
}
|
|
1862
|
+
async requestCardAttachmentUploadUrl(cardId, data) {
|
|
1863
|
+
return this.request("POST", `/cards/${cardId}/attachment-upload-url`, data);
|
|
1864
|
+
}
|
|
1865
|
+
async finalizeCardAttachment(cardId, data) {
|
|
1866
|
+
return this.request("POST", `/cards/${cardId}/attachments/finalize`, data);
|
|
1867
|
+
}
|
|
1858
1868
|
async getCardExternalLinks(cardId) {
|
|
1859
1869
|
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
1860
1870
|
}
|
|
1861
1871
|
async uploadArtifact(data) {
|
|
1862
1872
|
return this.request("POST", "/artifacts", data);
|
|
1863
1873
|
}
|
|
1874
|
+
async requestArtifactUploadUrl(data) {
|
|
1875
|
+
return this.request("POST", "/artifacts/upload-url", data);
|
|
1876
|
+
}
|
|
1877
|
+
async finalizeArtifact(data) {
|
|
1878
|
+
return this.request("POST", "/artifacts/finalize", data);
|
|
1879
|
+
}
|
|
1864
1880
|
async createArtifactShareLink(artifactId, expiresAt) {
|
|
1865
1881
|
return this.request("POST", `/artifacts/${artifactId}/share`, {
|
|
1866
1882
|
expires_at: expiresAt
|
|
@@ -2338,6 +2354,12 @@ ${planContent.trim()}`;
|
|
|
2338
2354
|
async getPlaybook(playbookId) {
|
|
2339
2355
|
return this.request("GET", `/playbooks/${playbookId}`);
|
|
2340
2356
|
}
|
|
2357
|
+
async getPlaybookVersion(playbookId, version) {
|
|
2358
|
+
return this.request("GET", `/playbooks/${encodeURIComponent(playbookId)}/versions/${version}`);
|
|
2359
|
+
}
|
|
2360
|
+
async recordStageGateEvidence(insert) {
|
|
2361
|
+
return this.request("POST", `/cards/${encodeURIComponent(insert.card_id)}/stage-gate-evidence`, insert);
|
|
2362
|
+
}
|
|
2341
2363
|
async createPlaybook(data) {
|
|
2342
2364
|
return this.request("POST", "/playbooks", data);
|
|
2343
2365
|
}
|
|
@@ -3436,6 +3458,52 @@ async function refreshSkills(opts = {}) {
|
|
|
3436
3458
|
}
|
|
3437
3459
|
|
|
3438
3460
|
// src/server.ts
|
|
3461
|
+
var MAX_ARTIFACT_SIZE = 2 * 1024 * 1024;
|
|
3462
|
+
var MAX_ATTACHMENT_SIZE = 5 * 1024 * 1024;
|
|
3463
|
+
function base64ByteLength(base64) {
|
|
3464
|
+
const stripped = base64.replace(/^data:[^,]*,/, "");
|
|
3465
|
+
return Buffer.from(stripped, "base64").byteLength;
|
|
3466
|
+
}
|
|
3467
|
+
function sha256Hex(bytes) {
|
|
3468
|
+
return createHash4("sha256").update(bytes).digest("hex");
|
|
3469
|
+
}
|
|
3470
|
+
async function readFileForUpload(filePath, maxSize, kind) {
|
|
3471
|
+
const bytes = await readFile(filePath);
|
|
3472
|
+
if (bytes.byteLength === 0) {
|
|
3473
|
+
throw new Error(`File is empty: ${filePath}`);
|
|
3474
|
+
}
|
|
3475
|
+
if (bytes.byteLength > maxSize) {
|
|
3476
|
+
const mb = Math.round(maxSize / (1024 * 1024));
|
|
3477
|
+
throw new Error(`File is ${bytes.byteLength} bytes, over the ${maxSize}-byte (${mb}MB) ${kind} limit.`);
|
|
3478
|
+
}
|
|
3479
|
+
return bytes;
|
|
3480
|
+
}
|
|
3481
|
+
function requireExactlyOneScope(scope) {
|
|
3482
|
+
if ([scope.cardId, scope.planId, scope.workspaceId].filter(Boolean).length !== 1) {
|
|
3483
|
+
throw new Error("Provide exactly one of cardId, planId, or workspaceId.");
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
var SIGNED_UPLOAD_TIMEOUT_MS = 30000;
|
|
3487
|
+
async function putToSignedUrl(uploadUrl, bytes, contentType) {
|
|
3488
|
+
let res;
|
|
3489
|
+
try {
|
|
3490
|
+
res = await fetch(uploadUrl, {
|
|
3491
|
+
method: "PUT",
|
|
3492
|
+
headers: { "Content-Type": contentType },
|
|
3493
|
+
body: bytes,
|
|
3494
|
+
signal: AbortSignal.timeout(SIGNED_UPLOAD_TIMEOUT_MS)
|
|
3495
|
+
});
|
|
3496
|
+
} catch (err) {
|
|
3497
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
3498
|
+
throw new Error(`Direct storage upload timed out after ${SIGNED_UPLOAD_TIMEOUT_MS}ms.`);
|
|
3499
|
+
}
|
|
3500
|
+
throw err;
|
|
3501
|
+
}
|
|
3502
|
+
if (!res.ok) {
|
|
3503
|
+
const detail = await res.text().catch(() => "");
|
|
3504
|
+
throw new Error(`Direct storage upload failed: ${res.status}${detail ? ` — ${detail}` : ""}`);
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3439
3507
|
var memorySessions = new Map;
|
|
3440
3508
|
function parseLabelList(raw) {
|
|
3441
3509
|
if (raw === undefined || raw === null)
|
|
@@ -3461,6 +3529,45 @@ function parseLabelList(raw) {
|
|
|
3461
3529
|
}
|
|
3462
3530
|
return;
|
|
3463
3531
|
}
|
|
3532
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3533
|
+
function requireStringArg(raw, field, opts = {}) {
|
|
3534
|
+
let value;
|
|
3535
|
+
if (typeof raw === "string")
|
|
3536
|
+
value = raw.trim();
|
|
3537
|
+
else if (typeof raw === "number" || typeof raw === "boolean")
|
|
3538
|
+
value = String(raw);
|
|
3539
|
+
else
|
|
3540
|
+
throw new Error(`${field} is required and must be a string (received ${raw === undefined || raw === null ? "nothing" : typeof raw}).`);
|
|
3541
|
+
if (value.length === 0)
|
|
3542
|
+
throw new Error(`${field} is required and must not be empty.`);
|
|
3543
|
+
if (opts.max && value.length > opts.max)
|
|
3544
|
+
throw new Error(`${field} must be at most ${opts.max} characters (received ${value.length}).`);
|
|
3545
|
+
return value;
|
|
3546
|
+
}
|
|
3547
|
+
function requireUuidArg(raw, field) {
|
|
3548
|
+
const value = requireStringArg(raw, field);
|
|
3549
|
+
if (!UUID_RE.test(value))
|
|
3550
|
+
throw new Error(`${field} must be a valid UUID (received "${value}").`);
|
|
3551
|
+
return value;
|
|
3552
|
+
}
|
|
3553
|
+
function optionalPercentArg(raw, field) {
|
|
3554
|
+
if (raw === undefined || raw === null)
|
|
3555
|
+
return;
|
|
3556
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
3557
|
+
if (!Number.isFinite(n))
|
|
3558
|
+
throw new Error(`${field} must be a number between 0 and 100.`);
|
|
3559
|
+
if (n < 0 || n > 100)
|
|
3560
|
+
throw new Error(`${field} must be between 0 and 100 (received ${n}).`);
|
|
3561
|
+
return n;
|
|
3562
|
+
}
|
|
3563
|
+
function optionalNonNegativeNumberArg(raw, field) {
|
|
3564
|
+
if (raw === undefined || raw === null)
|
|
3565
|
+
return;
|
|
3566
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
3567
|
+
if (!Number.isFinite(n) || n < 0)
|
|
3568
|
+
throw new Error(`${field} must be a non-negative number.`);
|
|
3569
|
+
return n;
|
|
3570
|
+
}
|
|
3464
3571
|
function initMemorySession(cardId, agentIdentifier, agentName, agentSessionId) {
|
|
3465
3572
|
memorySessions.set(cardId, {
|
|
3466
3573
|
cardId,
|
|
@@ -3896,7 +4003,7 @@ var TOOLS = {
|
|
|
3896
4003
|
}
|
|
3897
4004
|
},
|
|
3898
4005
|
harmony_upload_card_attachment: {
|
|
3899
|
-
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, CSV. Returns the stored attachment with a short-lived signed URL.",
|
|
4006
|
+
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). With `filePath` the server uploads direct-to-storage (no base64 through the model context); `base64Data` is the small-file fallback. Max 5MB. Allowed: PNG, JPEG, GIF, WebP, HEIC/HEIF, PDF, DOC/DOCX, XLS/XLSX, TXT, CSV. Returns the stored attachment with a short-lived signed URL. For large files on the hosted MCP server (where only base64Data works), prefer the two-step harmony_request_card_attachment_upload_url + harmony_finalize_card_attachment handshake, which keeps bytes out of the model context.",
|
|
3900
4007
|
inputSchema: {
|
|
3901
4008
|
type: "object",
|
|
3902
4009
|
properties: {
|
|
@@ -3921,6 +4028,58 @@ var TOOLS = {
|
|
|
3921
4028
|
required: ["cardId"]
|
|
3922
4029
|
}
|
|
3923
4030
|
},
|
|
4031
|
+
harmony_request_card_attachment_upload_url: {
|
|
4032
|
+
description: "Step 1 of the agent-driven card-attachment upload — use this for large files or when running against the hosted MCP server (which cannot read your local disk). Mints a one-shot signed Supabase Storage upload URL for a server-chosen path under the card. Provide cardId, fileName (with extension), the byte size, and optionally fileType. Returns { uploadUrl, token, storagePath, fileType }. Next: PUT the raw bytes straight to uploadUrl (e.g. `curl -X PUT --data-binary @file.png '<uploadUrl>'`) — no bytes pass through the model context — then call harmony_finalize_card_attachment with the returned storagePath. Max 5MB; allowed: PNG, JPEG, GIF, WebP, HEIC/HEIF, PDF, DOC/DOCX, XLS/XLSX, TXT, CSV.",
|
|
4033
|
+
inputSchema: {
|
|
4034
|
+
type: "object",
|
|
4035
|
+
properties: {
|
|
4036
|
+
cardId: { type: "string", description: "Card UUID" },
|
|
4037
|
+
fileName: {
|
|
4038
|
+
type: "string",
|
|
4039
|
+
description: "File name including extension (e.g. 'screenshot.png')."
|
|
4040
|
+
},
|
|
4041
|
+
fileType: {
|
|
4042
|
+
type: "string",
|
|
4043
|
+
description: "Optional MIME type (e.g. 'image/png'). Inferred from the file extension when omitted."
|
|
4044
|
+
},
|
|
4045
|
+
size: {
|
|
4046
|
+
type: "number",
|
|
4047
|
+
description: "File size in bytes (rejected early if over 5MB)."
|
|
4048
|
+
}
|
|
4049
|
+
},
|
|
4050
|
+
required: ["cardId", "fileName", "size"]
|
|
4051
|
+
}
|
|
4052
|
+
},
|
|
4053
|
+
harmony_finalize_card_attachment: {
|
|
4054
|
+
description: "Step 2 of the agent-driven card-attachment upload. After PUTting the bytes to the signed uploadUrl from harmony_request_card_attachment_upload_url, call this with the returned storagePath to validate and register the attachment. The server re-downloads the object and enforces size, an allowlisted content-type (magic-byte sniff, never the declared type), and — when you pass sha256 — an integrity check, deleting the object and failing on any mismatch. Returns the stored attachment with a short-lived signed URL.",
|
|
4055
|
+
inputSchema: {
|
|
4056
|
+
type: "object",
|
|
4057
|
+
properties: {
|
|
4058
|
+
cardId: { type: "string", description: "Card UUID" },
|
|
4059
|
+
storagePath: {
|
|
4060
|
+
type: "string",
|
|
4061
|
+
description: "The storagePath returned by harmony_request_card_attachment_upload_url."
|
|
4062
|
+
},
|
|
4063
|
+
fileName: {
|
|
4064
|
+
type: "string",
|
|
4065
|
+
description: "File name including extension (e.g. 'screenshot.png')."
|
|
4066
|
+
},
|
|
4067
|
+
fileType: {
|
|
4068
|
+
type: "string",
|
|
4069
|
+
description: "Optional MIME type; inferred from the extension when omitted."
|
|
4070
|
+
},
|
|
4071
|
+
sha256: {
|
|
4072
|
+
type: "string",
|
|
4073
|
+
description: "Optional hex SHA-256 of the uploaded bytes; verified against the stored object."
|
|
4074
|
+
},
|
|
4075
|
+
size: {
|
|
4076
|
+
type: "number",
|
|
4077
|
+
description: "Optional byte size (advisory; re-validated server-side)."
|
|
4078
|
+
}
|
|
4079
|
+
},
|
|
4080
|
+
required: ["cardId", "storagePath", "fileName"]
|
|
4081
|
+
}
|
|
4082
|
+
},
|
|
3924
4083
|
harmony_classify_card: {
|
|
3925
4084
|
description: "Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), and `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the canonical type label (feature/bug/idea). Use this right after creating a card (e.g. in the `hmy-new` flow) so it's classified in-flow instead of waiting for it to surface on the web board. Idempotent — safe to re-run. Never touches the user-owned `model_override`.",
|
|
3926
4085
|
inputSchema: {
|
|
@@ -3932,7 +4091,7 @@ var TOOLS = {
|
|
|
3932
4091
|
}
|
|
3933
4092
|
},
|
|
3934
4093
|
harmony_upload_artifact: {
|
|
3935
|
-
description: "Host a self-contained HTML document (e.g. a visual design draft or diagram) in Harmony and link it to a card, a plan, or a workspace. The file is stored privately and rendered in-app inside a sandboxed cross-origin iframe. Provide exactly one of cardId, planId, or workspaceId. Supply the HTML as `filePath` (a local path the MCP server can read) or `base64Data
|
|
4094
|
+
description: "Host a self-contained HTML document (e.g. a visual design draft or diagram) in Harmony and link it to a card, a plan, or a workspace. The file is stored privately and rendered in-app inside a sandboxed cross-origin iframe. Provide exactly one of cardId, planId, or workspaceId. Supply the HTML as `filePath` (a local path the MCP server can read — uploaded direct-to-storage, no base64 through the model context) or `base64Data` (the small-file fallback). Only text/html, max 2MB. Returns the stored artifact with a short-lived signed URL; call harmony_share_artifact to mint a public link. For large files on the hosted MCP server (where only base64Data works), prefer the two-step harmony_request_artifact_upload_url + harmony_finalize_artifact handshake, which keeps bytes out of the model context.",
|
|
3936
4095
|
inputSchema: {
|
|
3937
4096
|
type: "object",
|
|
3938
4097
|
properties: {
|
|
@@ -3958,6 +4117,61 @@ var TOOLS = {
|
|
|
3958
4117
|
required: []
|
|
3959
4118
|
}
|
|
3960
4119
|
},
|
|
4120
|
+
harmony_request_artifact_upload_url: {
|
|
4121
|
+
description: "Step 1 of the agent-driven artifact upload — use this for large files or when running against the hosted MCP server (which cannot read your local disk), instead of streaming base64 through the model context. Mints a one-shot signed Supabase Storage upload URL for a server-chosen path. Provide exactly one of cardId/planId/workspaceId, optionally a title and the byte size. Returns { uploadUrl, token, storagePath }. Next: PUT the raw HTML bytes straight to uploadUrl (e.g. `curl -X PUT --data-binary @doc.html '<uploadUrl>'`), then call harmony_finalize_artifact with the returned storagePath. Only text/html, max 2MB.",
|
|
4122
|
+
inputSchema: {
|
|
4123
|
+
type: "object",
|
|
4124
|
+
properties: {
|
|
4125
|
+
title: {
|
|
4126
|
+
type: "string",
|
|
4127
|
+
description: "Display title (defaults to the file basename at finalize)."
|
|
4128
|
+
},
|
|
4129
|
+
cardId: { type: "string", description: "Link to this card (UUID)." },
|
|
4130
|
+
planId: { type: "string", description: "Link to this plan (UUID)." },
|
|
4131
|
+
workspaceId: {
|
|
4132
|
+
type: "string",
|
|
4133
|
+
description: "Attach to this workspace as a standalone artifact (UUID)."
|
|
4134
|
+
},
|
|
4135
|
+
contentType: {
|
|
4136
|
+
type: "string",
|
|
4137
|
+
description: "MIME type; only 'text/html' is accepted (the default)."
|
|
4138
|
+
},
|
|
4139
|
+
size: {
|
|
4140
|
+
type: "number",
|
|
4141
|
+
description: "File size in bytes (rejected early if over 2MB)."
|
|
4142
|
+
}
|
|
4143
|
+
},
|
|
4144
|
+
required: []
|
|
4145
|
+
}
|
|
4146
|
+
},
|
|
4147
|
+
harmony_finalize_artifact: {
|
|
4148
|
+
description: "Step 2 of the agent-driven artifact upload. After PUTting the HTML bytes to the signed uploadUrl from harmony_request_artifact_upload_url, call this with the returned storagePath to validate and register the artifact. The server re-downloads the object and enforces size, the text/html content-type (magic-byte sniff), and — when you pass sha256 — an integrity check, deleting the object and failing on any mismatch. Provide the same one of cardId/planId/workspaceId used for the upload URL. Returns the stored artifact with a short-lived signed URL; call harmony_share_artifact to mint a public link.",
|
|
4149
|
+
inputSchema: {
|
|
4150
|
+
type: "object",
|
|
4151
|
+
properties: {
|
|
4152
|
+
storagePath: {
|
|
4153
|
+
type: "string",
|
|
4154
|
+
description: "The storagePath returned by harmony_request_artifact_upload_url."
|
|
4155
|
+
},
|
|
4156
|
+
title: { type: "string", description: "Display title." },
|
|
4157
|
+
cardId: { type: "string", description: "Link to this card (UUID)." },
|
|
4158
|
+
planId: { type: "string", description: "Link to this plan (UUID)." },
|
|
4159
|
+
workspaceId: {
|
|
4160
|
+
type: "string",
|
|
4161
|
+
description: "Attach to this workspace as a standalone artifact (UUID)."
|
|
4162
|
+
},
|
|
4163
|
+
sha256: {
|
|
4164
|
+
type: "string",
|
|
4165
|
+
description: "Optional hex SHA-256 of the uploaded bytes; verified against the stored object."
|
|
4166
|
+
},
|
|
4167
|
+
size: {
|
|
4168
|
+
type: "number",
|
|
4169
|
+
description: "Optional byte size (advisory; re-validated server-side)."
|
|
4170
|
+
}
|
|
4171
|
+
},
|
|
4172
|
+
required: ["storagePath"]
|
|
4173
|
+
}
|
|
4174
|
+
},
|
|
3961
4175
|
harmony_share_artifact: {
|
|
3962
4176
|
description: "Create a public, unauthenticated share link for a hosted artifact. Anyone with the link can view the rendered HTML without a Harmony account. Returns the share token and the full public URL.",
|
|
3963
4177
|
inputSchema: {
|
|
@@ -5201,7 +5415,6 @@ function registerHandlers(server, deps) {
|
|
|
5201
5415
|
const { name, arguments: args } = request.params;
|
|
5202
5416
|
const toolArgs = args || {};
|
|
5203
5417
|
const cardIdArg = toolArgs.cardId;
|
|
5204
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5205
5418
|
if (cardIdArg && UUID_RE.test(cardIdArg) && deps.isConfigured()) {
|
|
5206
5419
|
const isAutoStartTrigger = AUTO_START_TRIGGERS.has(name);
|
|
5207
5420
|
const cv = server.getClientVersion?.();
|
|
@@ -5555,70 +5768,157 @@ async function handleToolCall(name, args, deps) {
|
|
|
5555
5768
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
5556
5769
|
const filePath = args.filePath != null ? z.string().parse(args.filePath) : undefined;
|
|
5557
5770
|
const base64Data = args.base64Data != null ? z.string().parse(args.base64Data) : undefined;
|
|
5558
|
-
|
|
5771
|
+
const fileName = args.fileName != null ? z.string().parse(args.fileName) : undefined;
|
|
5559
5772
|
const contentType = args.contentType != null ? z.string().parse(args.contentType) : undefined;
|
|
5560
5773
|
if (filePath && base64Data) {
|
|
5561
5774
|
throw new Error("Provide either filePath or base64Data, not both.");
|
|
5562
5775
|
}
|
|
5563
|
-
let data;
|
|
5564
5776
|
if (filePath) {
|
|
5565
|
-
const bytes = await
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5777
|
+
const bytes = await readFileForUpload(filePath, MAX_ATTACHMENT_SIZE, "attachment");
|
|
5778
|
+
const resolvedName = fileName || basename(filePath);
|
|
5779
|
+
const signed = await client3.requestCardAttachmentUploadUrl(cardId, {
|
|
5780
|
+
fileName: resolvedName,
|
|
5781
|
+
fileType: contentType,
|
|
5782
|
+
size: bytes.byteLength
|
|
5783
|
+
});
|
|
5784
|
+
await putToSignedUrl(signed.uploadUrl, bytes, contentType || signed.fileType || "application/octet-stream");
|
|
5785
|
+
return await client3.finalizeCardAttachment(cardId, {
|
|
5786
|
+
storagePath: signed.storagePath,
|
|
5787
|
+
fileName: resolvedName,
|
|
5788
|
+
fileType: contentType || signed.fileType,
|
|
5789
|
+
sha256: sha256Hex(bytes),
|
|
5790
|
+
size: bytes.byteLength
|
|
5791
|
+
});
|
|
5792
|
+
}
|
|
5793
|
+
if (base64Data) {
|
|
5572
5794
|
if (!fileName) {
|
|
5573
5795
|
throw new Error("fileName is required when using base64Data.");
|
|
5574
5796
|
}
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5797
|
+
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
5798
|
+
throw new Error(`File is over the 5MB attachment limit. Use the harmony_request_card_attachment_upload_url + harmony_finalize_card_attachment handshake for large files.`);
|
|
5799
|
+
}
|
|
5800
|
+
return await client3.uploadCardAttachment(cardId, {
|
|
5801
|
+
fileName,
|
|
5802
|
+
data: base64Data,
|
|
5803
|
+
fileType: contentType
|
|
5804
|
+
});
|
|
5578
5805
|
}
|
|
5579
|
-
|
|
5580
|
-
fileName,
|
|
5581
|
-
data,
|
|
5582
|
-
fileType: contentType
|
|
5583
|
-
});
|
|
5584
|
-
return result;
|
|
5806
|
+
throw new Error("Provide either filePath or base64Data.");
|
|
5585
5807
|
}
|
|
5586
5808
|
case "harmony_upload_artifact": {
|
|
5587
5809
|
const title = args.title != null ? z.string().parse(args.title) : undefined;
|
|
5588
5810
|
const cardId = args.cardId != null ? z.string().uuid().parse(args.cardId) : undefined;
|
|
5589
5811
|
const planId = args.planId != null ? z.string().uuid().parse(args.planId) : undefined;
|
|
5590
5812
|
const workspaceId = args.workspaceId != null ? z.string().uuid().parse(args.workspaceId) : undefined;
|
|
5591
|
-
|
|
5592
|
-
if (anchors.length !== 1) {
|
|
5593
|
-
throw new Error("Provide exactly one of cardId, planId, or workspaceId.");
|
|
5594
|
-
}
|
|
5813
|
+
requireExactlyOneScope({ cardId, planId, workspaceId });
|
|
5595
5814
|
const filePath = args.filePath != null ? z.string().parse(args.filePath) : undefined;
|
|
5596
5815
|
const base64Data = args.base64Data != null ? z.string().parse(args.base64Data) : undefined;
|
|
5597
5816
|
if (filePath && base64Data) {
|
|
5598
5817
|
throw new Error("Provide either filePath or base64Data, not both.");
|
|
5599
5818
|
}
|
|
5600
|
-
let data;
|
|
5601
|
-
let inferredTitle = title;
|
|
5602
5819
|
if (filePath) {
|
|
5603
|
-
const bytes = await
|
|
5604
|
-
|
|
5605
|
-
|
|
5820
|
+
const bytes = await readFileForUpload(filePath, MAX_ARTIFACT_SIZE, "artifact");
|
|
5821
|
+
const resolvedTitle = title || basename(filePath);
|
|
5822
|
+
const signed = await client3.requestArtifactUploadUrl({
|
|
5823
|
+
title: resolvedTitle,
|
|
5824
|
+
cardId,
|
|
5825
|
+
planId,
|
|
5826
|
+
workspaceId,
|
|
5827
|
+
contentType: "text/html",
|
|
5828
|
+
size: bytes.byteLength
|
|
5829
|
+
});
|
|
5830
|
+
await putToSignedUrl(signed.uploadUrl, bytes, "text/html");
|
|
5831
|
+
return await client3.finalizeArtifact({
|
|
5832
|
+
storagePath: signed.storagePath,
|
|
5833
|
+
sha256: sha256Hex(bytes),
|
|
5834
|
+
size: bytes.byteLength,
|
|
5835
|
+
title: resolvedTitle,
|
|
5836
|
+
cardId,
|
|
5837
|
+
planId,
|
|
5838
|
+
workspaceId
|
|
5839
|
+
});
|
|
5840
|
+
}
|
|
5841
|
+
if (base64Data) {
|
|
5842
|
+
if (base64ByteLength(base64Data) > MAX_ARTIFACT_SIZE) {
|
|
5843
|
+
throw new Error(`Artifact is over the 2MB limit. Use the harmony_request_artifact_upload_url + harmony_finalize_artifact handshake for large files.`);
|
|
5606
5844
|
}
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5845
|
+
return await client3.uploadArtifact({
|
|
5846
|
+
title,
|
|
5847
|
+
cardId,
|
|
5848
|
+
planId,
|
|
5849
|
+
workspaceId,
|
|
5850
|
+
data: base64Data
|
|
5851
|
+
});
|
|
5613
5852
|
}
|
|
5614
|
-
|
|
5615
|
-
|
|
5853
|
+
throw new Error("Provide either filePath or base64Data.");
|
|
5854
|
+
}
|
|
5855
|
+
case "harmony_request_artifact_upload_url": {
|
|
5856
|
+
const title = args.title != null ? z.string().parse(args.title) : undefined;
|
|
5857
|
+
const cardId = args.cardId != null ? z.string().uuid().parse(args.cardId) : undefined;
|
|
5858
|
+
const planId = args.planId != null ? z.string().uuid().parse(args.planId) : undefined;
|
|
5859
|
+
const workspaceId = args.workspaceId != null ? z.string().uuid().parse(args.workspaceId) : undefined;
|
|
5860
|
+
requireExactlyOneScope({ cardId, planId, workspaceId });
|
|
5861
|
+
const contentType = args.contentType != null ? z.string().parse(args.contentType) : undefined;
|
|
5862
|
+
const size = args.size != null ? z.number().positive().parse(args.size) : undefined;
|
|
5863
|
+
if (size != null && size > MAX_ARTIFACT_SIZE) {
|
|
5864
|
+
throw new Error(`Declared size ${size} bytes is over the ${MAX_ARTIFACT_SIZE}-byte (2MB) artifact limit.`);
|
|
5865
|
+
}
|
|
5866
|
+
return await client3.requestArtifactUploadUrl({
|
|
5867
|
+
title,
|
|
5616
5868
|
cardId,
|
|
5617
5869
|
planId,
|
|
5618
5870
|
workspaceId,
|
|
5619
|
-
|
|
5871
|
+
contentType,
|
|
5872
|
+
size
|
|
5873
|
+
});
|
|
5874
|
+
}
|
|
5875
|
+
case "harmony_finalize_artifact": {
|
|
5876
|
+
const storagePath = z.string().parse(args.storagePath);
|
|
5877
|
+
const title = args.title != null ? z.string().parse(args.title) : undefined;
|
|
5878
|
+
const cardId = args.cardId != null ? z.string().uuid().parse(args.cardId) : undefined;
|
|
5879
|
+
const planId = args.planId != null ? z.string().uuid().parse(args.planId) : undefined;
|
|
5880
|
+
const workspaceId = args.workspaceId != null ? z.string().uuid().parse(args.workspaceId) : undefined;
|
|
5881
|
+
requireExactlyOneScope({ cardId, planId, workspaceId });
|
|
5882
|
+
const sha256 = args.sha256 != null ? z.string().parse(args.sha256) : undefined;
|
|
5883
|
+
const size = args.size != null ? z.number().positive().parse(args.size) : undefined;
|
|
5884
|
+
return await client3.finalizeArtifact({
|
|
5885
|
+
storagePath,
|
|
5886
|
+
sha256,
|
|
5887
|
+
size,
|
|
5888
|
+
title,
|
|
5889
|
+
cardId,
|
|
5890
|
+
planId,
|
|
5891
|
+
workspaceId
|
|
5892
|
+
});
|
|
5893
|
+
}
|
|
5894
|
+
case "harmony_request_card_attachment_upload_url": {
|
|
5895
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
5896
|
+
const fileName = z.string().parse(args.fileName);
|
|
5897
|
+
const fileType = args.fileType != null ? z.string().parse(args.fileType) : undefined;
|
|
5898
|
+
const size = z.number().positive().parse(args.size);
|
|
5899
|
+
if (size > MAX_ATTACHMENT_SIZE) {
|
|
5900
|
+
throw new Error(`Declared size ${size} bytes is over the ${MAX_ATTACHMENT_SIZE}-byte (5MB) attachment limit.`);
|
|
5901
|
+
}
|
|
5902
|
+
return await client3.requestCardAttachmentUploadUrl(cardId, {
|
|
5903
|
+
fileName,
|
|
5904
|
+
fileType,
|
|
5905
|
+
size
|
|
5906
|
+
});
|
|
5907
|
+
}
|
|
5908
|
+
case "harmony_finalize_card_attachment": {
|
|
5909
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
5910
|
+
const storagePath = z.string().parse(args.storagePath);
|
|
5911
|
+
const fileName = z.string().parse(args.fileName);
|
|
5912
|
+
const fileType = args.fileType != null ? z.string().parse(args.fileType) : undefined;
|
|
5913
|
+
const sha256 = args.sha256 != null ? z.string().parse(args.sha256) : undefined;
|
|
5914
|
+
const size = args.size != null ? z.number().positive().parse(args.size) : undefined;
|
|
5915
|
+
return await client3.finalizeCardAttachment(cardId, {
|
|
5916
|
+
storagePath,
|
|
5917
|
+
fileName,
|
|
5918
|
+
fileType,
|
|
5919
|
+
sha256,
|
|
5920
|
+
size
|
|
5620
5921
|
});
|
|
5621
|
-
return result;
|
|
5622
5922
|
}
|
|
5623
5923
|
case "harmony_share_artifact": {
|
|
5624
5924
|
const artifactId = z.string().uuid().parse(args.artifactId);
|
|
@@ -5780,9 +6080,11 @@ async function handleToolCall(name, args, deps) {
|
|
|
5780
6080
|
return { success: true, ...result };
|
|
5781
6081
|
}
|
|
5782
6082
|
case "harmony_start_agent_session": {
|
|
5783
|
-
const cardId =
|
|
5784
|
-
const agentIdentifier =
|
|
5785
|
-
const agentName =
|
|
6083
|
+
const cardId = requireUuidArg(args.cardId, "cardId");
|
|
6084
|
+
const agentIdentifier = requireStringArg(args.agentIdentifier, "agentIdentifier", { max: 100 });
|
|
6085
|
+
const agentName = requireStringArg(args.agentName, "agentName", {
|
|
6086
|
+
max: 100
|
|
6087
|
+
});
|
|
5786
6088
|
const moveToColumn = args.moveToColumn;
|
|
5787
6089
|
const addLabels = parseLabelList(args.addLabels);
|
|
5788
6090
|
let movedTo = null;
|
|
@@ -5843,7 +6145,7 @@ async function handleToolCall(name, args, deps) {
|
|
|
5843
6145
|
agentName,
|
|
5844
6146
|
status: "working",
|
|
5845
6147
|
currentTask: args.currentTask,
|
|
5846
|
-
estimatedMinutesRemaining: args.estimatedMinutesRemaining,
|
|
6148
|
+
estimatedMinutesRemaining: optionalNonNegativeNumberArg(args.estimatedMinutesRemaining, "estimatedMinutesRemaining"),
|
|
5847
6149
|
steerable: args.steerable === true || args.steerable === "true" ? true : undefined
|
|
5848
6150
|
});
|
|
5849
6151
|
markExplicit(cardId, {
|
|
@@ -5863,10 +6165,12 @@ async function handleToolCall(name, args, deps) {
|
|
|
5863
6165
|
};
|
|
5864
6166
|
}
|
|
5865
6167
|
case "harmony_update_agent_progress": {
|
|
5866
|
-
const cardId =
|
|
5867
|
-
const agentIdentifier =
|
|
5868
|
-
const agentName =
|
|
5869
|
-
|
|
6168
|
+
const cardId = requireUuidArg(args.cardId, "cardId");
|
|
6169
|
+
const agentIdentifier = requireStringArg(args.agentIdentifier, "agentIdentifier", { max: 100 });
|
|
6170
|
+
const agentName = requireStringArg(args.agentName, "agentName", {
|
|
6171
|
+
max: 100
|
|
6172
|
+
});
|
|
6173
|
+
const progressPercent = optionalPercentArg(args.progressPercent, "progressPercent");
|
|
5870
6174
|
const callerActions = args.actions;
|
|
5871
6175
|
const now = new Date().toISOString();
|
|
5872
6176
|
const callerRecentActions = [
|
|
@@ -5891,17 +6195,17 @@ async function handleToolCall(name, args, deps) {
|
|
|
5891
6195
|
progressPercent,
|
|
5892
6196
|
currentTask: args.currentTask,
|
|
5893
6197
|
blockers: args.blockers,
|
|
5894
|
-
estimatedMinutesRemaining: args.estimatedMinutesRemaining,
|
|
6198
|
+
estimatedMinutesRemaining: optionalNonNegativeNumberArg(args.estimatedMinutesRemaining, "estimatedMinutesRemaining"),
|
|
5895
6199
|
...mergedRecentActions && { recentActions: mergedRecentActions },
|
|
5896
6200
|
...runActivity.length > 0 && { runActivity }
|
|
5897
6201
|
});
|
|
5898
6202
|
return { success: true, midSessionLearnings: 0, ...result };
|
|
5899
6203
|
}
|
|
5900
6204
|
case "harmony_end_agent_session": {
|
|
5901
|
-
const cardId =
|
|
6205
|
+
const cardId = requireUuidArg(args.cardId, "cardId");
|
|
5902
6206
|
const moveToColumn = args.moveToColumn;
|
|
5903
6207
|
const sessionStatus = args.status || "completed";
|
|
5904
|
-
const endProgressPercent = args.progressPercent
|
|
6208
|
+
const endProgressPercent = optionalPercentArg(args.progressPercent, "progressPercent");
|
|
5905
6209
|
await flushMemoryActions(client3, cardId);
|
|
5906
6210
|
cleanupMemorySession(cardId);
|
|
5907
6211
|
let result = { session: null };
|
|
@@ -6745,7 +7049,7 @@ class HarmonyMCPServer {
|
|
|
6745
7049
|
}
|
|
6746
7050
|
|
|
6747
7051
|
// src/tui/setup.ts
|
|
6748
|
-
import { createHash as
|
|
7052
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
6749
7053
|
import {
|
|
6750
7054
|
existsSync as existsSync8,
|
|
6751
7055
|
lstatSync,
|
|
@@ -7940,7 +8244,7 @@ ${summary}`);
|
|
|
7940
8244
|
}
|
|
7941
8245
|
try {
|
|
7942
8246
|
const updateCheckFetched = await client3.fetchSkill("hmy-update-check");
|
|
7943
|
-
const actualHash =
|
|
8247
|
+
const actualHash = createHash5("sha256").update(updateCheckFetched.content).digest("hex");
|
|
7944
8248
|
if (actualHash !== updateCheckFetched.sha256) {
|
|
7945
8249
|
throw new Error(`hmy-update-check integrity check failed: expected ${updateCheckFetched.sha256}, got ${actualHash}`);
|
|
7946
8250
|
}
|