@gethmy/mcp 2.12.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +303 -41
- package/dist/index.js +301 -39
- package/dist/lib/api-client.js +21 -0
- package/package.json +1 -1
- package/src/api-client.ts +101 -0
- package/src/server.ts +394 -41
package/dist/index.js
CHANGED
|
@@ -985,6 +985,7 @@ var init_oauth_refresh = __esm(() => {
|
|
|
985
985
|
});
|
|
986
986
|
|
|
987
987
|
// src/server.ts
|
|
988
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
988
989
|
import { readFile } from "node:fs/promises";
|
|
989
990
|
import { basename } from "node:path";
|
|
990
991
|
// ../memory/dist/sync.js
|
|
@@ -1491,6 +1492,9 @@ var TIMINGS = {
|
|
|
1491
1492
|
QUERY_STALE_TIME: 1000 * 60 * 5,
|
|
1492
1493
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
1493
1494
|
};
|
|
1495
|
+
// ../harmony-shared/dist/stageHandoff.js
|
|
1496
|
+
var HANDOFF_MARKER = "harmony:stage-handoff";
|
|
1497
|
+
var HANDOFF_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + HANDOFF_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
|
|
1494
1498
|
// src/api-client.ts
|
|
1495
1499
|
init_config();
|
|
1496
1500
|
var RETRY_CONFIG = {
|
|
@@ -1850,12 +1854,24 @@ class HarmonyApiClient {
|
|
|
1850
1854
|
async uploadCardAttachment(cardId, data) {
|
|
1851
1855
|
return this.request("POST", `/cards/${cardId}/attachments`, data);
|
|
1852
1856
|
}
|
|
1857
|
+
async requestCardAttachmentUploadUrl(cardId, data) {
|
|
1858
|
+
return this.request("POST", `/cards/${cardId}/attachment-upload-url`, data);
|
|
1859
|
+
}
|
|
1860
|
+
async finalizeCardAttachment(cardId, data) {
|
|
1861
|
+
return this.request("POST", `/cards/${cardId}/attachments/finalize`, data);
|
|
1862
|
+
}
|
|
1853
1863
|
async getCardExternalLinks(cardId) {
|
|
1854
1864
|
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
1855
1865
|
}
|
|
1856
1866
|
async uploadArtifact(data) {
|
|
1857
1867
|
return this.request("POST", "/artifacts", data);
|
|
1858
1868
|
}
|
|
1869
|
+
async requestArtifactUploadUrl(data) {
|
|
1870
|
+
return this.request("POST", "/artifacts/upload-url", data);
|
|
1871
|
+
}
|
|
1872
|
+
async finalizeArtifact(data) {
|
|
1873
|
+
return this.request("POST", "/artifacts/finalize", data);
|
|
1874
|
+
}
|
|
1859
1875
|
async createArtifactShareLink(artifactId, expiresAt) {
|
|
1860
1876
|
return this.request("POST", `/artifacts/${artifactId}/share`, {
|
|
1861
1877
|
expires_at: expiresAt
|
|
@@ -2333,6 +2349,12 @@ ${planContent.trim()}`;
|
|
|
2333
2349
|
async getPlaybook(playbookId) {
|
|
2334
2350
|
return this.request("GET", `/playbooks/${playbookId}`);
|
|
2335
2351
|
}
|
|
2352
|
+
async getPlaybookVersion(playbookId, version) {
|
|
2353
|
+
return this.request("GET", `/playbooks/${encodeURIComponent(playbookId)}/versions/${version}`);
|
|
2354
|
+
}
|
|
2355
|
+
async recordStageGateEvidence(insert) {
|
|
2356
|
+
return this.request("POST", `/cards/${encodeURIComponent(insert.card_id)}/stage-gate-evidence`, insert);
|
|
2357
|
+
}
|
|
2336
2358
|
async createPlaybook(data) {
|
|
2337
2359
|
return this.request("POST", "/playbooks", data);
|
|
2338
2360
|
}
|
|
@@ -3431,6 +3453,52 @@ async function refreshSkills(opts = {}) {
|
|
|
3431
3453
|
}
|
|
3432
3454
|
|
|
3433
3455
|
// src/server.ts
|
|
3456
|
+
var MAX_ARTIFACT_SIZE = 2 * 1024 * 1024;
|
|
3457
|
+
var MAX_ATTACHMENT_SIZE = 5 * 1024 * 1024;
|
|
3458
|
+
function base64ByteLength(base64) {
|
|
3459
|
+
const stripped = base64.replace(/^data:[^,]*,/, "");
|
|
3460
|
+
return Buffer.from(stripped, "base64").byteLength;
|
|
3461
|
+
}
|
|
3462
|
+
function sha256Hex(bytes) {
|
|
3463
|
+
return createHash4("sha256").update(bytes).digest("hex");
|
|
3464
|
+
}
|
|
3465
|
+
async function readFileForUpload(filePath, maxSize, kind) {
|
|
3466
|
+
const bytes = await readFile(filePath);
|
|
3467
|
+
if (bytes.byteLength === 0) {
|
|
3468
|
+
throw new Error(`File is empty: ${filePath}`);
|
|
3469
|
+
}
|
|
3470
|
+
if (bytes.byteLength > maxSize) {
|
|
3471
|
+
const mb = Math.round(maxSize / (1024 * 1024));
|
|
3472
|
+
throw new Error(`File is ${bytes.byteLength} bytes, over the ${maxSize}-byte (${mb}MB) ${kind} limit.`);
|
|
3473
|
+
}
|
|
3474
|
+
return bytes;
|
|
3475
|
+
}
|
|
3476
|
+
function requireExactlyOneScope(scope) {
|
|
3477
|
+
if ([scope.cardId, scope.planId, scope.workspaceId].filter(Boolean).length !== 1) {
|
|
3478
|
+
throw new Error("Provide exactly one of cardId, planId, or workspaceId.");
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
var SIGNED_UPLOAD_TIMEOUT_MS = 30000;
|
|
3482
|
+
async function putToSignedUrl(uploadUrl, bytes, contentType) {
|
|
3483
|
+
let res;
|
|
3484
|
+
try {
|
|
3485
|
+
res = await fetch(uploadUrl, {
|
|
3486
|
+
method: "PUT",
|
|
3487
|
+
headers: { "Content-Type": contentType },
|
|
3488
|
+
body: bytes,
|
|
3489
|
+
signal: AbortSignal.timeout(SIGNED_UPLOAD_TIMEOUT_MS)
|
|
3490
|
+
});
|
|
3491
|
+
} catch (err) {
|
|
3492
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
3493
|
+
throw new Error(`Direct storage upload timed out after ${SIGNED_UPLOAD_TIMEOUT_MS}ms.`);
|
|
3494
|
+
}
|
|
3495
|
+
throw err;
|
|
3496
|
+
}
|
|
3497
|
+
if (!res.ok) {
|
|
3498
|
+
const detail = await res.text().catch(() => "");
|
|
3499
|
+
throw new Error(`Direct storage upload failed: ${res.status}${detail ? ` — ${detail}` : ""}`);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3434
3502
|
var memorySessions = new Map;
|
|
3435
3503
|
function parseLabelList(raw) {
|
|
3436
3504
|
if (raw === undefined || raw === null)
|
|
@@ -3891,7 +3959,7 @@ var TOOLS = {
|
|
|
3891
3959
|
}
|
|
3892
3960
|
},
|
|
3893
3961
|
harmony_upload_card_attachment: {
|
|
3894
|
-
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.",
|
|
3962
|
+
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.",
|
|
3895
3963
|
inputSchema: {
|
|
3896
3964
|
type: "object",
|
|
3897
3965
|
properties: {
|
|
@@ -3916,6 +3984,58 @@ var TOOLS = {
|
|
|
3916
3984
|
required: ["cardId"]
|
|
3917
3985
|
}
|
|
3918
3986
|
},
|
|
3987
|
+
harmony_request_card_attachment_upload_url: {
|
|
3988
|
+
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.",
|
|
3989
|
+
inputSchema: {
|
|
3990
|
+
type: "object",
|
|
3991
|
+
properties: {
|
|
3992
|
+
cardId: { type: "string", description: "Card UUID" },
|
|
3993
|
+
fileName: {
|
|
3994
|
+
type: "string",
|
|
3995
|
+
description: "File name including extension (e.g. 'screenshot.png')."
|
|
3996
|
+
},
|
|
3997
|
+
fileType: {
|
|
3998
|
+
type: "string",
|
|
3999
|
+
description: "Optional MIME type (e.g. 'image/png'). Inferred from the file extension when omitted."
|
|
4000
|
+
},
|
|
4001
|
+
size: {
|
|
4002
|
+
type: "number",
|
|
4003
|
+
description: "File size in bytes (rejected early if over 5MB)."
|
|
4004
|
+
}
|
|
4005
|
+
},
|
|
4006
|
+
required: ["cardId", "fileName", "size"]
|
|
4007
|
+
}
|
|
4008
|
+
},
|
|
4009
|
+
harmony_finalize_card_attachment: {
|
|
4010
|
+
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.",
|
|
4011
|
+
inputSchema: {
|
|
4012
|
+
type: "object",
|
|
4013
|
+
properties: {
|
|
4014
|
+
cardId: { type: "string", description: "Card UUID" },
|
|
4015
|
+
storagePath: {
|
|
4016
|
+
type: "string",
|
|
4017
|
+
description: "The storagePath returned by harmony_request_card_attachment_upload_url."
|
|
4018
|
+
},
|
|
4019
|
+
fileName: {
|
|
4020
|
+
type: "string",
|
|
4021
|
+
description: "File name including extension (e.g. 'screenshot.png')."
|
|
4022
|
+
},
|
|
4023
|
+
fileType: {
|
|
4024
|
+
type: "string",
|
|
4025
|
+
description: "Optional MIME type; inferred from the extension when omitted."
|
|
4026
|
+
},
|
|
4027
|
+
sha256: {
|
|
4028
|
+
type: "string",
|
|
4029
|
+
description: "Optional hex SHA-256 of the uploaded bytes; verified against the stored object."
|
|
4030
|
+
},
|
|
4031
|
+
size: {
|
|
4032
|
+
type: "number",
|
|
4033
|
+
description: "Optional byte size (advisory; re-validated server-side)."
|
|
4034
|
+
}
|
|
4035
|
+
},
|
|
4036
|
+
required: ["cardId", "storagePath", "fileName"]
|
|
4037
|
+
}
|
|
4038
|
+
},
|
|
3919
4039
|
harmony_classify_card: {
|
|
3920
4040
|
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`.",
|
|
3921
4041
|
inputSchema: {
|
|
@@ -3927,7 +4047,7 @@ var TOOLS = {
|
|
|
3927
4047
|
}
|
|
3928
4048
|
},
|
|
3929
4049
|
harmony_upload_artifact: {
|
|
3930
|
-
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
|
|
4050
|
+
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.",
|
|
3931
4051
|
inputSchema: {
|
|
3932
4052
|
type: "object",
|
|
3933
4053
|
properties: {
|
|
@@ -3953,6 +4073,61 @@ var TOOLS = {
|
|
|
3953
4073
|
required: []
|
|
3954
4074
|
}
|
|
3955
4075
|
},
|
|
4076
|
+
harmony_request_artifact_upload_url: {
|
|
4077
|
+
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.",
|
|
4078
|
+
inputSchema: {
|
|
4079
|
+
type: "object",
|
|
4080
|
+
properties: {
|
|
4081
|
+
title: {
|
|
4082
|
+
type: "string",
|
|
4083
|
+
description: "Display title (defaults to the file basename at finalize)."
|
|
4084
|
+
},
|
|
4085
|
+
cardId: { type: "string", description: "Link to this card (UUID)." },
|
|
4086
|
+
planId: { type: "string", description: "Link to this plan (UUID)." },
|
|
4087
|
+
workspaceId: {
|
|
4088
|
+
type: "string",
|
|
4089
|
+
description: "Attach to this workspace as a standalone artifact (UUID)."
|
|
4090
|
+
},
|
|
4091
|
+
contentType: {
|
|
4092
|
+
type: "string",
|
|
4093
|
+
description: "MIME type; only 'text/html' is accepted (the default)."
|
|
4094
|
+
},
|
|
4095
|
+
size: {
|
|
4096
|
+
type: "number",
|
|
4097
|
+
description: "File size in bytes (rejected early if over 2MB)."
|
|
4098
|
+
}
|
|
4099
|
+
},
|
|
4100
|
+
required: []
|
|
4101
|
+
}
|
|
4102
|
+
},
|
|
4103
|
+
harmony_finalize_artifact: {
|
|
4104
|
+
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.",
|
|
4105
|
+
inputSchema: {
|
|
4106
|
+
type: "object",
|
|
4107
|
+
properties: {
|
|
4108
|
+
storagePath: {
|
|
4109
|
+
type: "string",
|
|
4110
|
+
description: "The storagePath returned by harmony_request_artifact_upload_url."
|
|
4111
|
+
},
|
|
4112
|
+
title: { type: "string", description: "Display title." },
|
|
4113
|
+
cardId: { type: "string", description: "Link to this card (UUID)." },
|
|
4114
|
+
planId: { type: "string", description: "Link to this plan (UUID)." },
|
|
4115
|
+
workspaceId: {
|
|
4116
|
+
type: "string",
|
|
4117
|
+
description: "Attach to this workspace as a standalone artifact (UUID)."
|
|
4118
|
+
},
|
|
4119
|
+
sha256: {
|
|
4120
|
+
type: "string",
|
|
4121
|
+
description: "Optional hex SHA-256 of the uploaded bytes; verified against the stored object."
|
|
4122
|
+
},
|
|
4123
|
+
size: {
|
|
4124
|
+
type: "number",
|
|
4125
|
+
description: "Optional byte size (advisory; re-validated server-side)."
|
|
4126
|
+
}
|
|
4127
|
+
},
|
|
4128
|
+
required: ["storagePath"]
|
|
4129
|
+
}
|
|
4130
|
+
},
|
|
3956
4131
|
harmony_share_artifact: {
|
|
3957
4132
|
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.",
|
|
3958
4133
|
inputSchema: {
|
|
@@ -5550,70 +5725,157 @@ async function handleToolCall(name, args, deps) {
|
|
|
5550
5725
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
5551
5726
|
const filePath = args.filePath != null ? z.string().parse(args.filePath) : undefined;
|
|
5552
5727
|
const base64Data = args.base64Data != null ? z.string().parse(args.base64Data) : undefined;
|
|
5553
|
-
|
|
5728
|
+
const fileName = args.fileName != null ? z.string().parse(args.fileName) : undefined;
|
|
5554
5729
|
const contentType = args.contentType != null ? z.string().parse(args.contentType) : undefined;
|
|
5555
5730
|
if (filePath && base64Data) {
|
|
5556
5731
|
throw new Error("Provide either filePath or base64Data, not both.");
|
|
5557
5732
|
}
|
|
5558
|
-
let data;
|
|
5559
5733
|
if (filePath) {
|
|
5560
|
-
const bytes = await
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5734
|
+
const bytes = await readFileForUpload(filePath, MAX_ATTACHMENT_SIZE, "attachment");
|
|
5735
|
+
const resolvedName = fileName || basename(filePath);
|
|
5736
|
+
const signed = await client3.requestCardAttachmentUploadUrl(cardId, {
|
|
5737
|
+
fileName: resolvedName,
|
|
5738
|
+
fileType: contentType,
|
|
5739
|
+
size: bytes.byteLength
|
|
5740
|
+
});
|
|
5741
|
+
await putToSignedUrl(signed.uploadUrl, bytes, contentType || signed.fileType || "application/octet-stream");
|
|
5742
|
+
return await client3.finalizeCardAttachment(cardId, {
|
|
5743
|
+
storagePath: signed.storagePath,
|
|
5744
|
+
fileName: resolvedName,
|
|
5745
|
+
fileType: contentType || signed.fileType,
|
|
5746
|
+
sha256: sha256Hex(bytes),
|
|
5747
|
+
size: bytes.byteLength
|
|
5748
|
+
});
|
|
5749
|
+
}
|
|
5750
|
+
if (base64Data) {
|
|
5567
5751
|
if (!fileName) {
|
|
5568
5752
|
throw new Error("fileName is required when using base64Data.");
|
|
5569
5753
|
}
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5754
|
+
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
5755
|
+
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.`);
|
|
5756
|
+
}
|
|
5757
|
+
return await client3.uploadCardAttachment(cardId, {
|
|
5758
|
+
fileName,
|
|
5759
|
+
data: base64Data,
|
|
5760
|
+
fileType: contentType
|
|
5761
|
+
});
|
|
5573
5762
|
}
|
|
5574
|
-
|
|
5575
|
-
fileName,
|
|
5576
|
-
data,
|
|
5577
|
-
fileType: contentType
|
|
5578
|
-
});
|
|
5579
|
-
return result;
|
|
5763
|
+
throw new Error("Provide either filePath or base64Data.");
|
|
5580
5764
|
}
|
|
5581
5765
|
case "harmony_upload_artifact": {
|
|
5582
5766
|
const title = args.title != null ? z.string().parse(args.title) : undefined;
|
|
5583
5767
|
const cardId = args.cardId != null ? z.string().uuid().parse(args.cardId) : undefined;
|
|
5584
5768
|
const planId = args.planId != null ? z.string().uuid().parse(args.planId) : undefined;
|
|
5585
5769
|
const workspaceId = args.workspaceId != null ? z.string().uuid().parse(args.workspaceId) : undefined;
|
|
5586
|
-
|
|
5587
|
-
if (anchors.length !== 1) {
|
|
5588
|
-
throw new Error("Provide exactly one of cardId, planId, or workspaceId.");
|
|
5589
|
-
}
|
|
5770
|
+
requireExactlyOneScope({ cardId, planId, workspaceId });
|
|
5590
5771
|
const filePath = args.filePath != null ? z.string().parse(args.filePath) : undefined;
|
|
5591
5772
|
const base64Data = args.base64Data != null ? z.string().parse(args.base64Data) : undefined;
|
|
5592
5773
|
if (filePath && base64Data) {
|
|
5593
5774
|
throw new Error("Provide either filePath or base64Data, not both.");
|
|
5594
5775
|
}
|
|
5595
|
-
let data;
|
|
5596
|
-
let inferredTitle = title;
|
|
5597
5776
|
if (filePath) {
|
|
5598
|
-
const bytes = await
|
|
5599
|
-
|
|
5600
|
-
|
|
5777
|
+
const bytes = await readFileForUpload(filePath, MAX_ARTIFACT_SIZE, "artifact");
|
|
5778
|
+
const resolvedTitle = title || basename(filePath);
|
|
5779
|
+
const signed = await client3.requestArtifactUploadUrl({
|
|
5780
|
+
title: resolvedTitle,
|
|
5781
|
+
cardId,
|
|
5782
|
+
planId,
|
|
5783
|
+
workspaceId,
|
|
5784
|
+
contentType: "text/html",
|
|
5785
|
+
size: bytes.byteLength
|
|
5786
|
+
});
|
|
5787
|
+
await putToSignedUrl(signed.uploadUrl, bytes, "text/html");
|
|
5788
|
+
return await client3.finalizeArtifact({
|
|
5789
|
+
storagePath: signed.storagePath,
|
|
5790
|
+
sha256: sha256Hex(bytes),
|
|
5791
|
+
size: bytes.byteLength,
|
|
5792
|
+
title: resolvedTitle,
|
|
5793
|
+
cardId,
|
|
5794
|
+
planId,
|
|
5795
|
+
workspaceId
|
|
5796
|
+
});
|
|
5797
|
+
}
|
|
5798
|
+
if (base64Data) {
|
|
5799
|
+
if (base64ByteLength(base64Data) > MAX_ARTIFACT_SIZE) {
|
|
5800
|
+
throw new Error(`Artifact is over the 2MB limit. Use the harmony_request_artifact_upload_url + harmony_finalize_artifact handshake for large files.`);
|
|
5601
5801
|
}
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5802
|
+
return await client3.uploadArtifact({
|
|
5803
|
+
title,
|
|
5804
|
+
cardId,
|
|
5805
|
+
planId,
|
|
5806
|
+
workspaceId,
|
|
5807
|
+
data: base64Data
|
|
5808
|
+
});
|
|
5608
5809
|
}
|
|
5609
|
-
|
|
5610
|
-
|
|
5810
|
+
throw new Error("Provide either filePath or base64Data.");
|
|
5811
|
+
}
|
|
5812
|
+
case "harmony_request_artifact_upload_url": {
|
|
5813
|
+
const title = args.title != null ? z.string().parse(args.title) : undefined;
|
|
5814
|
+
const cardId = args.cardId != null ? z.string().uuid().parse(args.cardId) : undefined;
|
|
5815
|
+
const planId = args.planId != null ? z.string().uuid().parse(args.planId) : undefined;
|
|
5816
|
+
const workspaceId = args.workspaceId != null ? z.string().uuid().parse(args.workspaceId) : undefined;
|
|
5817
|
+
requireExactlyOneScope({ cardId, planId, workspaceId });
|
|
5818
|
+
const contentType = args.contentType != null ? z.string().parse(args.contentType) : undefined;
|
|
5819
|
+
const size = args.size != null ? z.number().positive().parse(args.size) : undefined;
|
|
5820
|
+
if (size != null && size > MAX_ARTIFACT_SIZE) {
|
|
5821
|
+
throw new Error(`Declared size ${size} bytes is over the ${MAX_ARTIFACT_SIZE}-byte (2MB) artifact limit.`);
|
|
5822
|
+
}
|
|
5823
|
+
return await client3.requestArtifactUploadUrl({
|
|
5824
|
+
title,
|
|
5611
5825
|
cardId,
|
|
5612
5826
|
planId,
|
|
5613
5827
|
workspaceId,
|
|
5614
|
-
|
|
5828
|
+
contentType,
|
|
5829
|
+
size
|
|
5830
|
+
});
|
|
5831
|
+
}
|
|
5832
|
+
case "harmony_finalize_artifact": {
|
|
5833
|
+
const storagePath = z.string().parse(args.storagePath);
|
|
5834
|
+
const title = args.title != null ? z.string().parse(args.title) : undefined;
|
|
5835
|
+
const cardId = args.cardId != null ? z.string().uuid().parse(args.cardId) : undefined;
|
|
5836
|
+
const planId = args.planId != null ? z.string().uuid().parse(args.planId) : undefined;
|
|
5837
|
+
const workspaceId = args.workspaceId != null ? z.string().uuid().parse(args.workspaceId) : undefined;
|
|
5838
|
+
requireExactlyOneScope({ cardId, planId, workspaceId });
|
|
5839
|
+
const sha256 = args.sha256 != null ? z.string().parse(args.sha256) : undefined;
|
|
5840
|
+
const size = args.size != null ? z.number().positive().parse(args.size) : undefined;
|
|
5841
|
+
return await client3.finalizeArtifact({
|
|
5842
|
+
storagePath,
|
|
5843
|
+
sha256,
|
|
5844
|
+
size,
|
|
5845
|
+
title,
|
|
5846
|
+
cardId,
|
|
5847
|
+
planId,
|
|
5848
|
+
workspaceId
|
|
5849
|
+
});
|
|
5850
|
+
}
|
|
5851
|
+
case "harmony_request_card_attachment_upload_url": {
|
|
5852
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
5853
|
+
const fileName = z.string().parse(args.fileName);
|
|
5854
|
+
const fileType = args.fileType != null ? z.string().parse(args.fileType) : undefined;
|
|
5855
|
+
const size = z.number().positive().parse(args.size);
|
|
5856
|
+
if (size > MAX_ATTACHMENT_SIZE) {
|
|
5857
|
+
throw new Error(`Declared size ${size} bytes is over the ${MAX_ATTACHMENT_SIZE}-byte (5MB) attachment limit.`);
|
|
5858
|
+
}
|
|
5859
|
+
return await client3.requestCardAttachmentUploadUrl(cardId, {
|
|
5860
|
+
fileName,
|
|
5861
|
+
fileType,
|
|
5862
|
+
size
|
|
5863
|
+
});
|
|
5864
|
+
}
|
|
5865
|
+
case "harmony_finalize_card_attachment": {
|
|
5866
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
5867
|
+
const storagePath = z.string().parse(args.storagePath);
|
|
5868
|
+
const fileName = z.string().parse(args.fileName);
|
|
5869
|
+
const fileType = args.fileType != null ? z.string().parse(args.fileType) : undefined;
|
|
5870
|
+
const sha256 = args.sha256 != null ? z.string().parse(args.sha256) : undefined;
|
|
5871
|
+
const size = args.size != null ? z.number().positive().parse(args.size) : undefined;
|
|
5872
|
+
return await client3.finalizeCardAttachment(cardId, {
|
|
5873
|
+
storagePath,
|
|
5874
|
+
fileName,
|
|
5875
|
+
fileType,
|
|
5876
|
+
sha256,
|
|
5877
|
+
size
|
|
5615
5878
|
});
|
|
5616
|
-
return result;
|
|
5617
5879
|
}
|
|
5618
5880
|
case "harmony_share_artifact": {
|
|
5619
5881
|
const artifactId = z.string().uuid().parse(args.artifactId);
|
package/dist/lib/api-client.js
CHANGED
|
@@ -944,6 +944,9 @@ var TIMINGS = {
|
|
|
944
944
|
QUERY_STALE_TIME: 1000 * 60 * 5,
|
|
945
945
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
946
946
|
};
|
|
947
|
+
// ../harmony-shared/dist/stageHandoff.js
|
|
948
|
+
var HANDOFF_MARKER = "harmony:stage-handoff";
|
|
949
|
+
var HANDOFF_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + HANDOFF_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
|
|
947
950
|
// src/api-client.ts
|
|
948
951
|
init_config();
|
|
949
952
|
var RETRY_CONFIG = {
|
|
@@ -1303,12 +1306,24 @@ class HarmonyApiClient {
|
|
|
1303
1306
|
async uploadCardAttachment(cardId, data) {
|
|
1304
1307
|
return this.request("POST", `/cards/${cardId}/attachments`, data);
|
|
1305
1308
|
}
|
|
1309
|
+
async requestCardAttachmentUploadUrl(cardId, data) {
|
|
1310
|
+
return this.request("POST", `/cards/${cardId}/attachment-upload-url`, data);
|
|
1311
|
+
}
|
|
1312
|
+
async finalizeCardAttachment(cardId, data) {
|
|
1313
|
+
return this.request("POST", `/cards/${cardId}/attachments/finalize`, data);
|
|
1314
|
+
}
|
|
1306
1315
|
async getCardExternalLinks(cardId) {
|
|
1307
1316
|
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
1308
1317
|
}
|
|
1309
1318
|
async uploadArtifact(data) {
|
|
1310
1319
|
return this.request("POST", "/artifacts", data);
|
|
1311
1320
|
}
|
|
1321
|
+
async requestArtifactUploadUrl(data) {
|
|
1322
|
+
return this.request("POST", "/artifacts/upload-url", data);
|
|
1323
|
+
}
|
|
1324
|
+
async finalizeArtifact(data) {
|
|
1325
|
+
return this.request("POST", "/artifacts/finalize", data);
|
|
1326
|
+
}
|
|
1312
1327
|
async createArtifactShareLink(artifactId, expiresAt) {
|
|
1313
1328
|
return this.request("POST", `/artifacts/${artifactId}/share`, {
|
|
1314
1329
|
expires_at: expiresAt
|
|
@@ -1786,6 +1801,12 @@ ${planContent.trim()}`;
|
|
|
1786
1801
|
async getPlaybook(playbookId) {
|
|
1787
1802
|
return this.request("GET", `/playbooks/${playbookId}`);
|
|
1788
1803
|
}
|
|
1804
|
+
async getPlaybookVersion(playbookId, version) {
|
|
1805
|
+
return this.request("GET", `/playbooks/${encodeURIComponent(playbookId)}/versions/${version}`);
|
|
1806
|
+
}
|
|
1807
|
+
async recordStageGateEvidence(insert) {
|
|
1808
|
+
return this.request("POST", `/cards/${encodeURIComponent(insert.card_id)}/stage-gate-evidence`, insert);
|
|
1809
|
+
}
|
|
1789
1810
|
async createPlaybook(data) {
|
|
1790
1811
|
return this.request("POST", "/playbooks", data);
|
|
1791
1812
|
}
|
package/package.json
CHANGED
package/src/api-client.ts
CHANGED
|
@@ -2,6 +2,9 @@ import {
|
|
|
2
2
|
type AgentRunEventDraft,
|
|
3
3
|
type Comment,
|
|
4
4
|
getDisplayLinkType,
|
|
5
|
+
type PlaybookVersionDef,
|
|
6
|
+
type StageGateEvidenceInsert,
|
|
7
|
+
type StageGateEvidenceRow,
|
|
5
8
|
serializeCommentThread,
|
|
6
9
|
type WorkspaceAgent,
|
|
7
10
|
} from "@harmony/shared";
|
|
@@ -143,6 +146,15 @@ export interface CardAttachment {
|
|
|
143
146
|
signed_url_expires_in: number;
|
|
144
147
|
}
|
|
145
148
|
|
|
149
|
+
/** Response of a `*-upload-url` endpoint: a one-shot signed Supabase Storage
|
|
150
|
+
* URL the caller PUTs raw bytes to, plus the service-chosen path that the
|
|
151
|
+
* matching `finalize` call validates and registers. */
|
|
152
|
+
export interface SignedUpload {
|
|
153
|
+
uploadUrl: string;
|
|
154
|
+
token: string;
|
|
155
|
+
storagePath: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
146
158
|
export interface CardExternalLinkRow {
|
|
147
159
|
id: string;
|
|
148
160
|
card_id: string;
|
|
@@ -675,6 +687,32 @@ export class HarmonyApiClient {
|
|
|
675
687
|
return this.request("POST", `/cards/${cardId}/attachments`, data);
|
|
676
688
|
}
|
|
677
689
|
|
|
690
|
+
// Step 1 of the direct-to-storage attachment handshake: mint a signed upload
|
|
691
|
+
// URL for a server-chosen path under the card. The caller PUTs raw bytes to
|
|
692
|
+
// `uploadUrl`, then calls finalizeCardAttachment with the returned storagePath.
|
|
693
|
+
async requestCardAttachmentUploadUrl(
|
|
694
|
+
cardId: string,
|
|
695
|
+
data: { fileName: string; fileType?: string; size: number },
|
|
696
|
+
): Promise<SignedUpload & { fileType: string }> {
|
|
697
|
+
return this.request("POST", `/cards/${cardId}/attachment-upload-url`, data);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Step 2: validate the uploaded object (size + magic-byte content-type +
|
|
701
|
+
// optional sha256) and insert the attachment row. The server deletes the
|
|
702
|
+
// object and rejects on any mismatch.
|
|
703
|
+
async finalizeCardAttachment(
|
|
704
|
+
cardId: string,
|
|
705
|
+
data: {
|
|
706
|
+
storagePath: string;
|
|
707
|
+
fileName: string;
|
|
708
|
+
fileType?: string;
|
|
709
|
+
sha256?: string;
|
|
710
|
+
size?: number;
|
|
711
|
+
},
|
|
712
|
+
): Promise<{ attachment: CardAttachment }> {
|
|
713
|
+
return this.request("POST", `/cards/${cardId}/attachments/finalize`, data);
|
|
714
|
+
}
|
|
715
|
+
|
|
678
716
|
async getCardExternalLinks(
|
|
679
717
|
cardId: string,
|
|
680
718
|
): Promise<{ external_links: CardExternalLinkRow[] }> {
|
|
@@ -694,6 +732,35 @@ export class HarmonyApiClient {
|
|
|
694
732
|
return this.request("POST", "/artifacts", data);
|
|
695
733
|
}
|
|
696
734
|
|
|
735
|
+
// Step 1 of the direct-to-storage artifact handshake: mint a signed upload
|
|
736
|
+
// URL for a server-chosen path. The caller PUTs raw HTML bytes to `uploadUrl`,
|
|
737
|
+
// then calls finalizeArtifact with the returned storagePath.
|
|
738
|
+
async requestArtifactUploadUrl(data: {
|
|
739
|
+
title?: string;
|
|
740
|
+
cardId?: string;
|
|
741
|
+
planId?: string;
|
|
742
|
+
workspaceId?: string;
|
|
743
|
+
contentType?: string;
|
|
744
|
+
size?: number;
|
|
745
|
+
}): Promise<SignedUpload> {
|
|
746
|
+
return this.request("POST", "/artifacts/upload-url", data);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Step 2: validate the uploaded object (size + HTML magic-byte sniff +
|
|
750
|
+
// optional sha256) and insert the artifact row. The server deletes the
|
|
751
|
+
// object and rejects on any mismatch.
|
|
752
|
+
async finalizeArtifact(data: {
|
|
753
|
+
storagePath: string;
|
|
754
|
+
sha256?: string;
|
|
755
|
+
size?: number;
|
|
756
|
+
title?: string;
|
|
757
|
+
cardId?: string;
|
|
758
|
+
planId?: string;
|
|
759
|
+
workspaceId?: string;
|
|
760
|
+
}): Promise<{ artifact: Record<string, unknown> }> {
|
|
761
|
+
return this.request("POST", "/artifacts/finalize", data);
|
|
762
|
+
}
|
|
763
|
+
|
|
697
764
|
async createArtifactShareLink(
|
|
698
765
|
artifactId: string,
|
|
699
766
|
expiresAt?: string,
|
|
@@ -1747,6 +1814,40 @@ export class HarmonyApiClient {
|
|
|
1747
1814
|
return this.request("GET", `/playbooks/${playbookId}`);
|
|
1748
1815
|
}
|
|
1749
1816
|
|
|
1817
|
+
/**
|
|
1818
|
+
* Fetch the pinned, immutable stage-def snapshot for `(playbookId, version)`.
|
|
1819
|
+
* The agent daemon's stage executor (#514) resolves a card's `current_stage`
|
|
1820
|
+
* against this frozen def — never live `playbooks.steps` — so editing a
|
|
1821
|
+
* Playbook never rewrites an in-flight card.
|
|
1822
|
+
*/
|
|
1823
|
+
async getPlaybookVersion(
|
|
1824
|
+
playbookId: string,
|
|
1825
|
+
version: number,
|
|
1826
|
+
): Promise<{
|
|
1827
|
+
version: PlaybookVersionDef & { playbook_id: string; version: number };
|
|
1828
|
+
}> {
|
|
1829
|
+
return this.request(
|
|
1830
|
+
"GET",
|
|
1831
|
+
`/playbooks/${encodeURIComponent(playbookId)}/versions/${version}`,
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Persist a stage gate evidence row (Playbooks P1 #3, card #516). The daemon
|
|
1837
|
+
* collects + evaluates a gate then POSTs the resulting evidence here; the edge
|
|
1838
|
+
* writes it with the service-role client (the table has no INSERT RLS policy,
|
|
1839
|
+
* so this is the only write path). Returns the inserted row.
|
|
1840
|
+
*/
|
|
1841
|
+
async recordStageGateEvidence(
|
|
1842
|
+
insert: StageGateEvidenceInsert,
|
|
1843
|
+
): Promise<{ evidence: StageGateEvidenceRow }> {
|
|
1844
|
+
return this.request(
|
|
1845
|
+
"POST",
|
|
1846
|
+
`/cards/${encodeURIComponent(insert.card_id)}/stage-gate-evidence`,
|
|
1847
|
+
insert,
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1750
1851
|
async createPlaybook(data: {
|
|
1751
1852
|
workspaceId: string;
|
|
1752
1853
|
name: string;
|