@gethmy/mcp 2.19.0 → 2.20.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 +121 -39
- package/dist/index.js +102 -36
- package/package.json +1 -1
- package/src/server.ts +178 -53
- package/src/tui/setup.ts +19 -3
package/dist/cli.js
CHANGED
|
@@ -3644,6 +3644,43 @@ async function putToSignedUrl(uploadUrl, bytes, contentType) {
|
|
|
3644
3644
|
throw new Error(`Direct storage upload failed: ${res.status}${detail ? ` — ${detail}` : ""}`);
|
|
3645
3645
|
}
|
|
3646
3646
|
}
|
|
3647
|
+
async function attachFileToCard(client3, cardId, file) {
|
|
3648
|
+
const { filePath, base64Data, fileName, contentType } = file;
|
|
3649
|
+
if (filePath && base64Data) {
|
|
3650
|
+
throw new Error("Provide either filePath or base64Data, not both.");
|
|
3651
|
+
}
|
|
3652
|
+
if (filePath) {
|
|
3653
|
+
const bytes = await readFileForUpload(filePath, MAX_ATTACHMENT_SIZE, "attachment");
|
|
3654
|
+
const resolvedName = fileName || basename(filePath);
|
|
3655
|
+
const signed = await client3.requestCardAttachmentUploadUrl(cardId, {
|
|
3656
|
+
fileName: resolvedName,
|
|
3657
|
+
fileType: contentType,
|
|
3658
|
+
size: bytes.byteLength
|
|
3659
|
+
});
|
|
3660
|
+
await putToSignedUrl(signed.uploadUrl, bytes, contentType || signed.fileType || "application/octet-stream");
|
|
3661
|
+
return await client3.finalizeCardAttachment(cardId, {
|
|
3662
|
+
storagePath: signed.storagePath,
|
|
3663
|
+
fileName: resolvedName,
|
|
3664
|
+
fileType: contentType || signed.fileType,
|
|
3665
|
+
sha256: sha256Hex(bytes),
|
|
3666
|
+
size: bytes.byteLength
|
|
3667
|
+
});
|
|
3668
|
+
}
|
|
3669
|
+
if (base64Data) {
|
|
3670
|
+
if (!fileName) {
|
|
3671
|
+
throw new Error("fileName is required when using base64Data.");
|
|
3672
|
+
}
|
|
3673
|
+
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
3674
|
+
throw new Error(`File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`);
|
|
3675
|
+
}
|
|
3676
|
+
return await client3.uploadCardAttachment(cardId, {
|
|
3677
|
+
fileName,
|
|
3678
|
+
data: base64Data,
|
|
3679
|
+
fileType: contentType
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
throw new Error("Provide either filePath or base64Data.");
|
|
3683
|
+
}
|
|
3647
3684
|
var memorySessions = new Map;
|
|
3648
3685
|
function parseLabelList(raw) {
|
|
3649
3686
|
if (raw === undefined || raw === null)
|
|
@@ -3800,7 +3837,7 @@ function cleanupMemorySession(cardId) {
|
|
|
3800
3837
|
}
|
|
3801
3838
|
var TOOLS = {
|
|
3802
3839
|
harmony_create_card: {
|
|
3803
|
-
description: "Create a new card in a Kanban column",
|
|
3840
|
+
description: "Create a new card in a Kanban column. Optionally attach reference files " + "(e.g. a screenshot from the prompt) at creation time via `attachments` — " + "the card is created first, then each file is uploaded to it.",
|
|
3804
3841
|
inputSchema: {
|
|
3805
3842
|
type: "object",
|
|
3806
3843
|
properties: {
|
|
@@ -3823,6 +3860,31 @@ var TOOLS = {
|
|
|
3823
3860
|
planId: {
|
|
3824
3861
|
type: "string",
|
|
3825
3862
|
description: "Plan ID to link this card to (optional). Links the card to that plan via its plan_id."
|
|
3863
|
+
},
|
|
3864
|
+
attachments: {
|
|
3865
|
+
type: "array",
|
|
3866
|
+
description: "Optional reference files to attach to the new card (e.g. a screenshot from the prompt). " + "Max 5MB each; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV. Each file's bytes " + "come from `filePath` (absolute local path the server reads, preferred) or `base64Data` " + "(small-file fallback; requires fileName). NOTE: a pasted image only attaches if your " + "harness has written it to a local file you can pass as filePath — a model cannot re-emit " + "pasted image bytes into base64Data. Per-file failures never block card creation; they are " + "reported back in the result's `attachments` array so you can retry via harmony_upload.",
|
|
3867
|
+
items: {
|
|
3868
|
+
type: "object",
|
|
3869
|
+
properties: {
|
|
3870
|
+
filePath: {
|
|
3871
|
+
type: "string",
|
|
3872
|
+
description: "Absolute path to a local file the server can read. Mutually exclusive with base64Data."
|
|
3873
|
+
},
|
|
3874
|
+
base64Data: {
|
|
3875
|
+
type: "string",
|
|
3876
|
+
description: "Base64-encoded bytes (a `data:` URL prefix is accepted and stripped). Requires fileName. Mutually exclusive with filePath."
|
|
3877
|
+
},
|
|
3878
|
+
fileName: {
|
|
3879
|
+
type: "string",
|
|
3880
|
+
description: "File name including extension (required with base64Data; else defaults to the filePath basename)."
|
|
3881
|
+
},
|
|
3882
|
+
contentType: {
|
|
3883
|
+
type: "string",
|
|
3884
|
+
description: "Optional MIME type (inferred from the extension when omitted)."
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3826
3888
|
}
|
|
3827
3889
|
},
|
|
3828
3890
|
required: ["title"]
|
|
@@ -4128,7 +4190,7 @@ var TOOLS = {
|
|
|
4128
4190
|
}
|
|
4129
4191
|
},
|
|
4130
4192
|
harmony_upload: {
|
|
4131
|
-
description: 'Upload a file in one call. `target: "card_attachment"` attaches a file to a card ' + "(max 5MB; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV) — requires cardId. " + '`target: "artifact"` hosts a self-contained HTML doc (text/html, max 2MB) linked to ' + "exactly one of cardId/planId/workspaceId, rendered in-app in a sandboxed iframe. Provide " + "the bytes as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). " + "Returns the attachment/artifact + a
|
|
4193
|
+
description: 'Upload a file in one call. `target: "card_attachment"` attaches a file to a card ' + "(max 5MB; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV) — requires cardId. " + '`target: "artifact"` hosts a self-contained HTML doc (text/html, max 2MB) linked to ' + "exactly one of cardId/planId/workspaceId, rendered in-app in a sandboxed iframe. Provide " + "the bytes as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). " + "Returns the attachment/artifact. An artifact comes back with TWO URLs, and the difference " + "matters: `app_url` is the durable in-app permalink — this is the one to hand a person, it " + "never expires and requires them to log in as a workspace member — while `signed_url` only " + "renders the document in-app and dies within the hour, so never pass it on. To share with " + "someone OUTSIDE the workspace, mint a public link with harmony_share_artifact. Large files " + "on the hosted MCP server: use harmony_request_upload_url + harmony_finalize_upload instead.",
|
|
4132
4194
|
inputSchema: {
|
|
4133
4195
|
type: "object",
|
|
4134
4196
|
properties: {
|
|
@@ -4214,7 +4276,7 @@ var TOOLS = {
|
|
|
4214
4276
|
}
|
|
4215
4277
|
},
|
|
4216
4278
|
harmony_finalize_upload: {
|
|
4217
|
-
description: "Step 2 of the upload handshake. After PUTting the bytes to the signed uploadUrl, call this " + "with the storagePath to validate and register. The server re-downloads the object and " + "enforces size + content-type (magic-byte sniff, never the declared type) + an optional " + 'sha256 integrity check, deleting and failing on any mismatch. `target: "card_attachment"` ' + '(cardId, storagePath, fileName) or `target: "artifact"` (storagePath + the same one of ' + "cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed URL
|
|
4279
|
+
description: "Step 2 of the upload handshake. After PUTting the bytes to the signed uploadUrl, call this " + "with the storagePath to validate and register. The server re-downloads the object and " + "enforces size + content-type (magic-byte sniff, never the declared type) + an optional " + 'sha256 integrity check, deleting and failing on any mismatch. `target: "card_attachment"` ' + '(cardId, storagePath, fileName) or `target: "artifact"` (storagePath + the same one of ' + "cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed " + "URL; an artifact also carries `app_url`, the durable in-app permalink — hand that one to a " + "person, never the short-lived `signed_url`.",
|
|
4218
4280
|
inputSchema: {
|
|
4219
4281
|
type: "object",
|
|
4220
4282
|
properties: {
|
|
@@ -4268,7 +4330,7 @@ var TOOLS = {
|
|
|
4268
4330
|
}
|
|
4269
4331
|
},
|
|
4270
4332
|
harmony_share_artifact: {
|
|
4271
|
-
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.",
|
|
4333
|
+
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. Only needed for a recipient OUTSIDE the workspace — for a teammate, hand over the `app_url` the upload already returned instead of exposing the document publicly.",
|
|
4272
4334
|
inputSchema: {
|
|
4273
4335
|
type: "object",
|
|
4274
4336
|
properties: {
|
|
@@ -5637,6 +5699,12 @@ async function handleToolCall(name, args, deps) {
|
|
|
5637
5699
|
case "harmony_create_card": {
|
|
5638
5700
|
const title = z.string().min(1).max(500).parse(args.title);
|
|
5639
5701
|
const projectId = args.projectId || getProjectId();
|
|
5702
|
+
const attachments = args.attachments != null ? z.array(z.object({
|
|
5703
|
+
filePath: z.string().optional(),
|
|
5704
|
+
base64Data: z.string().optional(),
|
|
5705
|
+
fileName: z.string().optional(),
|
|
5706
|
+
contentType: z.string().optional()
|
|
5707
|
+
})).parse(args.attachments) : [];
|
|
5640
5708
|
const result = await client3.createCard(projectId, {
|
|
5641
5709
|
title,
|
|
5642
5710
|
columnId: args.columnId,
|
|
@@ -5645,7 +5713,30 @@ async function handleToolCall(name, args, deps) {
|
|
|
5645
5713
|
assigneeId: args.assigneeId,
|
|
5646
5714
|
planId: args.planId
|
|
5647
5715
|
});
|
|
5648
|
-
|
|
5716
|
+
if (attachments.length === 0) {
|
|
5717
|
+
return { success: true, ...result };
|
|
5718
|
+
}
|
|
5719
|
+
const cardId = result.card?.id;
|
|
5720
|
+
if (!cardId) {
|
|
5721
|
+
return {
|
|
5722
|
+
success: true,
|
|
5723
|
+
...result,
|
|
5724
|
+
attachmentWarning: "Card created, but attachments were skipped: no card id was returned to upload against."
|
|
5725
|
+
};
|
|
5726
|
+
}
|
|
5727
|
+
const attachmentResults = await Promise.all(attachments.map(async (file) => {
|
|
5728
|
+
try {
|
|
5729
|
+
const uploaded = await attachFileToCard(client3, cardId, file);
|
|
5730
|
+
return { ok: true, attachment: uploaded.attachment };
|
|
5731
|
+
} catch (err) {
|
|
5732
|
+
return {
|
|
5733
|
+
ok: false,
|
|
5734
|
+
fileName: file.fileName ?? file.filePath ?? "(unnamed)",
|
|
5735
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5736
|
+
};
|
|
5737
|
+
}
|
|
5738
|
+
}));
|
|
5739
|
+
return { success: true, ...result, attachments: attachmentResults };
|
|
5649
5740
|
}
|
|
5650
5741
|
case "harmony_update_card": {
|
|
5651
5742
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
@@ -5931,37 +6022,12 @@ ${list}
|
|
|
5931
6022
|
const cardId2 = z.string().uuid().parse(args.cardId);
|
|
5932
6023
|
const fileName = args.fileName != null ? z.string().parse(args.fileName) : undefined;
|
|
5933
6024
|
const contentType = args.contentType != null ? z.string().parse(args.contentType) : undefined;
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
size: bytes.byteLength
|
|
5941
|
-
});
|
|
5942
|
-
await putToSignedUrl(signed.uploadUrl, bytes, contentType || signed.fileType || "application/octet-stream");
|
|
5943
|
-
return await client3.finalizeCardAttachment(cardId2, {
|
|
5944
|
-
storagePath: signed.storagePath,
|
|
5945
|
-
fileName: resolvedName,
|
|
5946
|
-
fileType: contentType || signed.fileType,
|
|
5947
|
-
sha256: sha256Hex(bytes),
|
|
5948
|
-
size: bytes.byteLength
|
|
5949
|
-
});
|
|
5950
|
-
}
|
|
5951
|
-
if (base64Data) {
|
|
5952
|
-
if (!fileName) {
|
|
5953
|
-
throw new Error("fileName is required when using base64Data.");
|
|
5954
|
-
}
|
|
5955
|
-
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
5956
|
-
throw new Error(`File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`);
|
|
5957
|
-
}
|
|
5958
|
-
return await client3.uploadCardAttachment(cardId2, {
|
|
5959
|
-
fileName,
|
|
5960
|
-
data: base64Data,
|
|
5961
|
-
fileType: contentType
|
|
5962
|
-
});
|
|
5963
|
-
}
|
|
5964
|
-
throw new Error("Provide either filePath or base64Data.");
|
|
6025
|
+
return await attachFileToCard(client3, cardId2, {
|
|
6026
|
+
filePath,
|
|
6027
|
+
base64Data,
|
|
6028
|
+
fileName,
|
|
6029
|
+
contentType
|
|
6030
|
+
});
|
|
5965
6031
|
}
|
|
5966
6032
|
const title = args.title != null ? z.string().parse(args.title) : undefined;
|
|
5967
6033
|
const cardId = args.cardId != null ? z.string().uuid().parse(args.cardId) : undefined;
|
|
@@ -8480,14 +8546,29 @@ ${summary}`);
|
|
|
8480
8546
|
|
|
8481
8547
|
This project uses Harmony for task management. When working on tasks:
|
|
8482
8548
|
|
|
8549
|
+
## Agent identity — always identify as yourself
|
|
8550
|
+
|
|
8551
|
+
Every \`harmony_start_agent_session\` call passes \`agentIdentifier\` + \`agentName\`. **Use your own
|
|
8552
|
+
identity, never a hardcoded one from this file.** AGENTS.md is a cross-runtime convention file, so
|
|
8553
|
+
more than one kind of agent will read it; the board shows agents as teammates, and a session
|
|
8554
|
+
attributed to the wrong runtime misattributes the work in front of the whole team.
|
|
8555
|
+
|
|
8556
|
+
- \`agentIdentifier\` — a stable kebab-case id for the runtime you actually are
|
|
8557
|
+
- \`agentName\` — its human-readable name
|
|
8558
|
+
|
|
8559
|
+
Known values: \`claude-code\` / "Claude Code", \`codex\` / "OpenAI Codex", \`cursor\` / "Cursor",
|
|
8560
|
+
\`claude-desktop\` / "Claude Desktop". If you are a runtime not listed here, use your own name rather
|
|
8561
|
+
than borrowing the closest entry.
|
|
8562
|
+
|
|
8483
8563
|
## Starting Work on a Card
|
|
8484
8564
|
|
|
8485
8565
|
When given a card reference (e.g., #42 or a card name), follow this workflow:
|
|
8486
8566
|
|
|
8487
|
-
1. Use \`
|
|
8567
|
+
1. Use \`harmony_get_card\` or \`harmony_search_cards\` to find the card
|
|
8488
8568
|
2. Move the card to "In Progress" using \`harmony_move_card\`
|
|
8489
8569
|
3. Add the "agent" label using \`harmony_add_label_to_card\`
|
|
8490
|
-
4. Start a session with \`harmony_start_agent_session
|
|
8570
|
+
4. Start a session with \`harmony_start_agent_session\`, passing **your own** \`agentIdentifier\` +
|
|
8571
|
+
\`agentName\` (see "Agent identity" above)
|
|
8491
8572
|
5. Show the card details to the user
|
|
8492
8573
|
6. Use \`harmony_generate_prompt\` to get guidance, then implement the solution
|
|
8493
8574
|
7. Update progress periodically with \`harmony_update_agent_progress\`
|
|
@@ -8498,7 +8579,8 @@ When given a card reference (e.g., #42 or a card name), follow this workflow:
|
|
|
8498
8579
|
Before implementing a plan or feature, check if it maps to an existing Harmony card:
|
|
8499
8580
|
|
|
8500
8581
|
1. Use \`harmony_search_cards\` with keywords from the task description
|
|
8501
|
-
2. If a match is found, call \`harmony_start_agent_session\`
|
|
8582
|
+
2. If a match is found, call \`harmony_start_agent_session\` with **your own** \`agentIdentifier\` +
|
|
8583
|
+
\`agentName\` (see "Agent identity" above), plus \`moveToColumn: "In Progress"\`, \`addLabels: ["agent"]\`
|
|
8502
8584
|
3. Update progress with \`harmony_update_agent_progress\` at milestones
|
|
8503
8585
|
4. When done, call \`harmony_end_agent_session\` with status: "completed", moveToColumn: "Review"
|
|
8504
8586
|
|
package/dist/index.js
CHANGED
|
@@ -3639,6 +3639,43 @@ async function putToSignedUrl(uploadUrl, bytes, contentType) {
|
|
|
3639
3639
|
throw new Error(`Direct storage upload failed: ${res.status}${detail ? ` — ${detail}` : ""}`);
|
|
3640
3640
|
}
|
|
3641
3641
|
}
|
|
3642
|
+
async function attachFileToCard(client3, cardId, file) {
|
|
3643
|
+
const { filePath, base64Data, fileName, contentType } = file;
|
|
3644
|
+
if (filePath && base64Data) {
|
|
3645
|
+
throw new Error("Provide either filePath or base64Data, not both.");
|
|
3646
|
+
}
|
|
3647
|
+
if (filePath) {
|
|
3648
|
+
const bytes = await readFileForUpload(filePath, MAX_ATTACHMENT_SIZE, "attachment");
|
|
3649
|
+
const resolvedName = fileName || basename(filePath);
|
|
3650
|
+
const signed = await client3.requestCardAttachmentUploadUrl(cardId, {
|
|
3651
|
+
fileName: resolvedName,
|
|
3652
|
+
fileType: contentType,
|
|
3653
|
+
size: bytes.byteLength
|
|
3654
|
+
});
|
|
3655
|
+
await putToSignedUrl(signed.uploadUrl, bytes, contentType || signed.fileType || "application/octet-stream");
|
|
3656
|
+
return await client3.finalizeCardAttachment(cardId, {
|
|
3657
|
+
storagePath: signed.storagePath,
|
|
3658
|
+
fileName: resolvedName,
|
|
3659
|
+
fileType: contentType || signed.fileType,
|
|
3660
|
+
sha256: sha256Hex(bytes),
|
|
3661
|
+
size: bytes.byteLength
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
if (base64Data) {
|
|
3665
|
+
if (!fileName) {
|
|
3666
|
+
throw new Error("fileName is required when using base64Data.");
|
|
3667
|
+
}
|
|
3668
|
+
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
3669
|
+
throw new Error(`File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`);
|
|
3670
|
+
}
|
|
3671
|
+
return await client3.uploadCardAttachment(cardId, {
|
|
3672
|
+
fileName,
|
|
3673
|
+
data: base64Data,
|
|
3674
|
+
fileType: contentType
|
|
3675
|
+
});
|
|
3676
|
+
}
|
|
3677
|
+
throw new Error("Provide either filePath or base64Data.");
|
|
3678
|
+
}
|
|
3642
3679
|
var memorySessions = new Map;
|
|
3643
3680
|
function parseLabelList(raw) {
|
|
3644
3681
|
if (raw === undefined || raw === null)
|
|
@@ -3795,7 +3832,7 @@ function cleanupMemorySession(cardId) {
|
|
|
3795
3832
|
}
|
|
3796
3833
|
var TOOLS = {
|
|
3797
3834
|
harmony_create_card: {
|
|
3798
|
-
description: "Create a new card in a Kanban column",
|
|
3835
|
+
description: "Create a new card in a Kanban column. Optionally attach reference files " + "(e.g. a screenshot from the prompt) at creation time via `attachments` — " + "the card is created first, then each file is uploaded to it.",
|
|
3799
3836
|
inputSchema: {
|
|
3800
3837
|
type: "object",
|
|
3801
3838
|
properties: {
|
|
@@ -3818,6 +3855,31 @@ var TOOLS = {
|
|
|
3818
3855
|
planId: {
|
|
3819
3856
|
type: "string",
|
|
3820
3857
|
description: "Plan ID to link this card to (optional). Links the card to that plan via its plan_id."
|
|
3858
|
+
},
|
|
3859
|
+
attachments: {
|
|
3860
|
+
type: "array",
|
|
3861
|
+
description: "Optional reference files to attach to the new card (e.g. a screenshot from the prompt). " + "Max 5MB each; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV. Each file's bytes " + "come from `filePath` (absolute local path the server reads, preferred) or `base64Data` " + "(small-file fallback; requires fileName). NOTE: a pasted image only attaches if your " + "harness has written it to a local file you can pass as filePath — a model cannot re-emit " + "pasted image bytes into base64Data. Per-file failures never block card creation; they are " + "reported back in the result's `attachments` array so you can retry via harmony_upload.",
|
|
3862
|
+
items: {
|
|
3863
|
+
type: "object",
|
|
3864
|
+
properties: {
|
|
3865
|
+
filePath: {
|
|
3866
|
+
type: "string",
|
|
3867
|
+
description: "Absolute path to a local file the server can read. Mutually exclusive with base64Data."
|
|
3868
|
+
},
|
|
3869
|
+
base64Data: {
|
|
3870
|
+
type: "string",
|
|
3871
|
+
description: "Base64-encoded bytes (a `data:` URL prefix is accepted and stripped). Requires fileName. Mutually exclusive with filePath."
|
|
3872
|
+
},
|
|
3873
|
+
fileName: {
|
|
3874
|
+
type: "string",
|
|
3875
|
+
description: "File name including extension (required with base64Data; else defaults to the filePath basename)."
|
|
3876
|
+
},
|
|
3877
|
+
contentType: {
|
|
3878
|
+
type: "string",
|
|
3879
|
+
description: "Optional MIME type (inferred from the extension when omitted)."
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3821
3883
|
}
|
|
3822
3884
|
},
|
|
3823
3885
|
required: ["title"]
|
|
@@ -4123,7 +4185,7 @@ var TOOLS = {
|
|
|
4123
4185
|
}
|
|
4124
4186
|
},
|
|
4125
4187
|
harmony_upload: {
|
|
4126
|
-
description: 'Upload a file in one call. `target: "card_attachment"` attaches a file to a card ' + "(max 5MB; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV) — requires cardId. " + '`target: "artifact"` hosts a self-contained HTML doc (text/html, max 2MB) linked to ' + "exactly one of cardId/planId/workspaceId, rendered in-app in a sandboxed iframe. Provide " + "the bytes as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). " + "Returns the attachment/artifact + a
|
|
4188
|
+
description: 'Upload a file in one call. `target: "card_attachment"` attaches a file to a card ' + "(max 5MB; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV) — requires cardId. " + '`target: "artifact"` hosts a self-contained HTML doc (text/html, max 2MB) linked to ' + "exactly one of cardId/planId/workspaceId, rendered in-app in a sandboxed iframe. Provide " + "the bytes as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). " + "Returns the attachment/artifact. An artifact comes back with TWO URLs, and the difference " + "matters: `app_url` is the durable in-app permalink — this is the one to hand a person, it " + "never expires and requires them to log in as a workspace member — while `signed_url` only " + "renders the document in-app and dies within the hour, so never pass it on. To share with " + "someone OUTSIDE the workspace, mint a public link with harmony_share_artifact. Large files " + "on the hosted MCP server: use harmony_request_upload_url + harmony_finalize_upload instead.",
|
|
4127
4189
|
inputSchema: {
|
|
4128
4190
|
type: "object",
|
|
4129
4191
|
properties: {
|
|
@@ -4209,7 +4271,7 @@ var TOOLS = {
|
|
|
4209
4271
|
}
|
|
4210
4272
|
},
|
|
4211
4273
|
harmony_finalize_upload: {
|
|
4212
|
-
description: "Step 2 of the upload handshake. After PUTting the bytes to the signed uploadUrl, call this " + "with the storagePath to validate and register. The server re-downloads the object and " + "enforces size + content-type (magic-byte sniff, never the declared type) + an optional " + 'sha256 integrity check, deleting and failing on any mismatch. `target: "card_attachment"` ' + '(cardId, storagePath, fileName) or `target: "artifact"` (storagePath + the same one of ' + "cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed URL
|
|
4274
|
+
description: "Step 2 of the upload handshake. After PUTting the bytes to the signed uploadUrl, call this " + "with the storagePath to validate and register. The server re-downloads the object and " + "enforces size + content-type (magic-byte sniff, never the declared type) + an optional " + 'sha256 integrity check, deleting and failing on any mismatch. `target: "card_attachment"` ' + '(cardId, storagePath, fileName) or `target: "artifact"` (storagePath + the same one of ' + "cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed " + "URL; an artifact also carries `app_url`, the durable in-app permalink — hand that one to a " + "person, never the short-lived `signed_url`.",
|
|
4213
4275
|
inputSchema: {
|
|
4214
4276
|
type: "object",
|
|
4215
4277
|
properties: {
|
|
@@ -4263,7 +4325,7 @@ var TOOLS = {
|
|
|
4263
4325
|
}
|
|
4264
4326
|
},
|
|
4265
4327
|
harmony_share_artifact: {
|
|
4266
|
-
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.",
|
|
4328
|
+
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. Only needed for a recipient OUTSIDE the workspace — for a teammate, hand over the `app_url` the upload already returned instead of exposing the document publicly.",
|
|
4267
4329
|
inputSchema: {
|
|
4268
4330
|
type: "object",
|
|
4269
4331
|
properties: {
|
|
@@ -5632,6 +5694,12 @@ async function handleToolCall(name, args, deps) {
|
|
|
5632
5694
|
case "harmony_create_card": {
|
|
5633
5695
|
const title = z.string().min(1).max(500).parse(args.title);
|
|
5634
5696
|
const projectId = args.projectId || getProjectId();
|
|
5697
|
+
const attachments = args.attachments != null ? z.array(z.object({
|
|
5698
|
+
filePath: z.string().optional(),
|
|
5699
|
+
base64Data: z.string().optional(),
|
|
5700
|
+
fileName: z.string().optional(),
|
|
5701
|
+
contentType: z.string().optional()
|
|
5702
|
+
})).parse(args.attachments) : [];
|
|
5635
5703
|
const result = await client3.createCard(projectId, {
|
|
5636
5704
|
title,
|
|
5637
5705
|
columnId: args.columnId,
|
|
@@ -5640,7 +5708,30 @@ async function handleToolCall(name, args, deps) {
|
|
|
5640
5708
|
assigneeId: args.assigneeId,
|
|
5641
5709
|
planId: args.planId
|
|
5642
5710
|
});
|
|
5643
|
-
|
|
5711
|
+
if (attachments.length === 0) {
|
|
5712
|
+
return { success: true, ...result };
|
|
5713
|
+
}
|
|
5714
|
+
const cardId = result.card?.id;
|
|
5715
|
+
if (!cardId) {
|
|
5716
|
+
return {
|
|
5717
|
+
success: true,
|
|
5718
|
+
...result,
|
|
5719
|
+
attachmentWarning: "Card created, but attachments were skipped: no card id was returned to upload against."
|
|
5720
|
+
};
|
|
5721
|
+
}
|
|
5722
|
+
const attachmentResults = await Promise.all(attachments.map(async (file) => {
|
|
5723
|
+
try {
|
|
5724
|
+
const uploaded = await attachFileToCard(client3, cardId, file);
|
|
5725
|
+
return { ok: true, attachment: uploaded.attachment };
|
|
5726
|
+
} catch (err) {
|
|
5727
|
+
return {
|
|
5728
|
+
ok: false,
|
|
5729
|
+
fileName: file.fileName ?? file.filePath ?? "(unnamed)",
|
|
5730
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5731
|
+
};
|
|
5732
|
+
}
|
|
5733
|
+
}));
|
|
5734
|
+
return { success: true, ...result, attachments: attachmentResults };
|
|
5644
5735
|
}
|
|
5645
5736
|
case "harmony_update_card": {
|
|
5646
5737
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
@@ -5926,37 +6017,12 @@ ${list}
|
|
|
5926
6017
|
const cardId2 = z.string().uuid().parse(args.cardId);
|
|
5927
6018
|
const fileName = args.fileName != null ? z.string().parse(args.fileName) : undefined;
|
|
5928
6019
|
const contentType = args.contentType != null ? z.string().parse(args.contentType) : undefined;
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
size: bytes.byteLength
|
|
5936
|
-
});
|
|
5937
|
-
await putToSignedUrl(signed.uploadUrl, bytes, contentType || signed.fileType || "application/octet-stream");
|
|
5938
|
-
return await client3.finalizeCardAttachment(cardId2, {
|
|
5939
|
-
storagePath: signed.storagePath,
|
|
5940
|
-
fileName: resolvedName,
|
|
5941
|
-
fileType: contentType || signed.fileType,
|
|
5942
|
-
sha256: sha256Hex(bytes),
|
|
5943
|
-
size: bytes.byteLength
|
|
5944
|
-
});
|
|
5945
|
-
}
|
|
5946
|
-
if (base64Data) {
|
|
5947
|
-
if (!fileName) {
|
|
5948
|
-
throw new Error("fileName is required when using base64Data.");
|
|
5949
|
-
}
|
|
5950
|
-
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
5951
|
-
throw new Error(`File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`);
|
|
5952
|
-
}
|
|
5953
|
-
return await client3.uploadCardAttachment(cardId2, {
|
|
5954
|
-
fileName,
|
|
5955
|
-
data: base64Data,
|
|
5956
|
-
fileType: contentType
|
|
5957
|
-
});
|
|
5958
|
-
}
|
|
5959
|
-
throw new Error("Provide either filePath or base64Data.");
|
|
6020
|
+
return await attachFileToCard(client3, cardId2, {
|
|
6021
|
+
filePath,
|
|
6022
|
+
base64Data,
|
|
6023
|
+
fileName,
|
|
6024
|
+
contentType
|
|
6025
|
+
});
|
|
5960
6026
|
}
|
|
5961
6027
|
const title = args.title != null ? z.string().parse(args.title) : undefined;
|
|
5962
6028
|
const cardId = args.cardId != null ? z.string().uuid().parse(args.cardId) : undefined;
|
package/package.json
CHANGED
package/src/server.ts
CHANGED
|
@@ -221,6 +221,80 @@ async function putToSignedUrl(
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
/** One card-attachment input: bytes as a local `filePath` (preferred — read
|
|
225
|
+
* direct-to-storage) or a small `base64Data` blob (requires `fileName`). */
|
|
226
|
+
export interface CardAttachmentInput {
|
|
227
|
+
filePath?: string;
|
|
228
|
+
base64Data?: string;
|
|
229
|
+
fileName?: string;
|
|
230
|
+
contentType?: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Attach one file to an existing card, running the same direct-to-storage
|
|
235
|
+
* handshake as `harmony_upload` `{target:"card_attachment"}`. Extracted so
|
|
236
|
+
* `harmony_create_card` can attach reference files (e.g. a prompt screenshot)
|
|
237
|
+
* at creation time without duplicating the orchestration. Requires an existing
|
|
238
|
+
* `cardId` — attachment upload is inherently a second step after the card row
|
|
239
|
+
* exists, so the create path calls this only after `createCard` returns.
|
|
240
|
+
*/
|
|
241
|
+
async function attachFileToCard(
|
|
242
|
+
client: HarmonyApiClient,
|
|
243
|
+
cardId: string,
|
|
244
|
+
file: CardAttachmentInput,
|
|
245
|
+
) {
|
|
246
|
+
const { filePath, base64Data, fileName, contentType } = file;
|
|
247
|
+
if (filePath && base64Data) {
|
|
248
|
+
throw new Error("Provide either filePath or base64Data, not both.");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (filePath) {
|
|
252
|
+
// Server can read the file → upload direct-to-storage via the handshake
|
|
253
|
+
// (no base64 through the model context or edge-fn body).
|
|
254
|
+
const bytes = await readFileForUpload(
|
|
255
|
+
filePath,
|
|
256
|
+
MAX_ATTACHMENT_SIZE,
|
|
257
|
+
"attachment",
|
|
258
|
+
);
|
|
259
|
+
const resolvedName = fileName || basename(filePath);
|
|
260
|
+
const signed = await client.requestCardAttachmentUploadUrl(cardId, {
|
|
261
|
+
fileName: resolvedName,
|
|
262
|
+
fileType: contentType,
|
|
263
|
+
size: bytes.byteLength,
|
|
264
|
+
});
|
|
265
|
+
await putToSignedUrl(
|
|
266
|
+
signed.uploadUrl,
|
|
267
|
+
bytes,
|
|
268
|
+
contentType || signed.fileType || "application/octet-stream",
|
|
269
|
+
);
|
|
270
|
+
return await client.finalizeCardAttachment(cardId, {
|
|
271
|
+
storagePath: signed.storagePath,
|
|
272
|
+
fileName: resolvedName,
|
|
273
|
+
fileType: contentType || signed.fileType,
|
|
274
|
+
sha256: sha256Hex(bytes),
|
|
275
|
+
size: bytes.byteLength,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (base64Data) {
|
|
280
|
+
if (!fileName) {
|
|
281
|
+
throw new Error("fileName is required when using base64Data.");
|
|
282
|
+
}
|
|
283
|
+
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return await client.uploadCardAttachment(cardId, {
|
|
289
|
+
fileName,
|
|
290
|
+
data: base64Data,
|
|
291
|
+
fileType: contentType,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
throw new Error("Provide either filePath or base64Data.");
|
|
296
|
+
}
|
|
297
|
+
|
|
224
298
|
/**
|
|
225
299
|
* Dependencies injected into tool handlers.
|
|
226
300
|
* Allows the same handlers to be used by both stdio and remote (HTTP) transports.
|
|
@@ -524,7 +598,10 @@ function cleanupMemorySession(cardId: string): void {
|
|
|
524
598
|
export const TOOLS = {
|
|
525
599
|
// Card operations
|
|
526
600
|
harmony_create_card: {
|
|
527
|
-
description:
|
|
601
|
+
description:
|
|
602
|
+
"Create a new card in a Kanban column. Optionally attach reference files " +
|
|
603
|
+
"(e.g. a screenshot from the prompt) at creation time via `attachments` — " +
|
|
604
|
+
"the card is created first, then each file is uploaded to it.",
|
|
528
605
|
inputSchema: {
|
|
529
606
|
type: "object",
|
|
530
607
|
properties: {
|
|
@@ -550,6 +627,42 @@ export const TOOLS = {
|
|
|
550
627
|
description:
|
|
551
628
|
"Plan ID to link this card to (optional). Links the card to that plan via its plan_id.",
|
|
552
629
|
},
|
|
630
|
+
attachments: {
|
|
631
|
+
type: "array",
|
|
632
|
+
description:
|
|
633
|
+
"Optional reference files to attach to the new card (e.g. a screenshot from the prompt). " +
|
|
634
|
+
"Max 5MB each; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV. Each file's bytes " +
|
|
635
|
+
"come from `filePath` (absolute local path the server reads, preferred) or `base64Data` " +
|
|
636
|
+
"(small-file fallback; requires fileName). NOTE: a pasted image only attaches if your " +
|
|
637
|
+
"harness has written it to a local file you can pass as filePath — a model cannot re-emit " +
|
|
638
|
+
"pasted image bytes into base64Data. Per-file failures never block card creation; they are " +
|
|
639
|
+
"reported back in the result's `attachments` array so you can retry via harmony_upload.",
|
|
640
|
+
items: {
|
|
641
|
+
type: "object",
|
|
642
|
+
properties: {
|
|
643
|
+
filePath: {
|
|
644
|
+
type: "string",
|
|
645
|
+
description:
|
|
646
|
+
"Absolute path to a local file the server can read. Mutually exclusive with base64Data.",
|
|
647
|
+
},
|
|
648
|
+
base64Data: {
|
|
649
|
+
type: "string",
|
|
650
|
+
description:
|
|
651
|
+
"Base64-encoded bytes (a `data:` URL prefix is accepted and stripped). Requires fileName. Mutually exclusive with filePath.",
|
|
652
|
+
},
|
|
653
|
+
fileName: {
|
|
654
|
+
type: "string",
|
|
655
|
+
description:
|
|
656
|
+
"File name including extension (required with base64Data; else defaults to the filePath basename).",
|
|
657
|
+
},
|
|
658
|
+
contentType: {
|
|
659
|
+
type: "string",
|
|
660
|
+
description:
|
|
661
|
+
"Optional MIME type (inferred from the extension when omitted).",
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
},
|
|
553
666
|
},
|
|
554
667
|
required: ["title"],
|
|
555
668
|
},
|
|
@@ -892,9 +1005,12 @@ export const TOOLS = {
|
|
|
892
1005
|
'`target: "artifact"` hosts a self-contained HTML doc (text/html, max 2MB) linked to ' +
|
|
893
1006
|
"exactly one of cardId/planId/workspaceId, rendered in-app in a sandboxed iframe. Provide " +
|
|
894
1007
|
"the bytes as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). " +
|
|
895
|
-
"Returns the attachment/artifact
|
|
896
|
-
"
|
|
897
|
-
"
|
|
1008
|
+
"Returns the attachment/artifact. An artifact comes back with TWO URLs, and the difference " +
|
|
1009
|
+
"matters: `app_url` is the durable in-app permalink — this is the one to hand a person, it " +
|
|
1010
|
+
"never expires and requires them to log in as a workspace member — while `signed_url` only " +
|
|
1011
|
+
"renders the document in-app and dies within the hour, so never pass it on. To share with " +
|
|
1012
|
+
"someone OUTSIDE the workspace, mint a public link with harmony_share_artifact. Large files " +
|
|
1013
|
+
"on the hosted MCP server: use harmony_request_upload_url + harmony_finalize_upload instead.",
|
|
898
1014
|
inputSchema: {
|
|
899
1015
|
type: "object",
|
|
900
1016
|
properties: {
|
|
@@ -1006,7 +1122,9 @@ export const TOOLS = {
|
|
|
1006
1122
|
"enforces size + content-type (magic-byte sniff, never the declared type) + an optional " +
|
|
1007
1123
|
'sha256 integrity check, deleting and failing on any mismatch. `target: "card_attachment"` ' +
|
|
1008
1124
|
'(cardId, storagePath, fileName) or `target: "artifact"` (storagePath + the same one of ' +
|
|
1009
|
-
"cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed
|
|
1125
|
+
"cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed " +
|
|
1126
|
+
"URL; an artifact also carries `app_url`, the durable in-app permalink — hand that one to a " +
|
|
1127
|
+
"person, never the short-lived `signed_url`.",
|
|
1010
1128
|
inputSchema: {
|
|
1011
1129
|
type: "object",
|
|
1012
1130
|
properties: {
|
|
@@ -1069,7 +1187,7 @@ export const TOOLS = {
|
|
|
1069
1187
|
},
|
|
1070
1188
|
harmony_share_artifact: {
|
|
1071
1189
|
description:
|
|
1072
|
-
"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.",
|
|
1190
|
+
"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. Only needed for a recipient OUTSIDE the workspace — for a teammate, hand over the `app_url` the upload already returned instead of exposing the document publicly.",
|
|
1073
1191
|
inputSchema: {
|
|
1074
1192
|
type: "object",
|
|
1075
1193
|
properties: {
|
|
@@ -2681,6 +2799,19 @@ async function handleToolCall(
|
|
|
2681
2799
|
case "harmony_create_card": {
|
|
2682
2800
|
const title = z.string().min(1).max(500).parse(args.title);
|
|
2683
2801
|
const projectId = (args.projectId as string) || getProjectId();
|
|
2802
|
+
const attachments =
|
|
2803
|
+
args.attachments != null
|
|
2804
|
+
? z
|
|
2805
|
+
.array(
|
|
2806
|
+
z.object({
|
|
2807
|
+
filePath: z.string().optional(),
|
|
2808
|
+
base64Data: z.string().optional(),
|
|
2809
|
+
fileName: z.string().optional(),
|
|
2810
|
+
contentType: z.string().optional(),
|
|
2811
|
+
}),
|
|
2812
|
+
)
|
|
2813
|
+
.parse(args.attachments)
|
|
2814
|
+
: [];
|
|
2684
2815
|
const result = await client.createCard(projectId, {
|
|
2685
2816
|
title,
|
|
2686
2817
|
columnId: args.columnId as string | undefined,
|
|
@@ -2689,7 +2820,40 @@ async function handleToolCall(
|
|
|
2689
2820
|
assigneeId: args.assigneeId as string | undefined,
|
|
2690
2821
|
planId: args.planId as string | undefined,
|
|
2691
2822
|
});
|
|
2692
|
-
|
|
2823
|
+
|
|
2824
|
+
if (attachments.length === 0) {
|
|
2825
|
+
return { success: true, ...result };
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
// Attach reference files (e.g. a prompt screenshot) to the freshly
|
|
2829
|
+
// created card. Attachment upload requires an existing cardId, so this
|
|
2830
|
+
// runs only after createCard returns. A bad attachment must never lose
|
|
2831
|
+
// the card — per-file failures are captured and reported alongside the
|
|
2832
|
+
// successes rather than throwing out the whole create.
|
|
2833
|
+
const cardId = (result.card as { id?: string } | null)?.id;
|
|
2834
|
+
if (!cardId) {
|
|
2835
|
+
return {
|
|
2836
|
+
success: true,
|
|
2837
|
+
...result,
|
|
2838
|
+
attachmentWarning:
|
|
2839
|
+
"Card created, but attachments were skipped: no card id was returned to upload against.",
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
const attachmentResults = await Promise.all(
|
|
2843
|
+
attachments.map(async (file) => {
|
|
2844
|
+
try {
|
|
2845
|
+
const uploaded = await attachFileToCard(client, cardId, file);
|
|
2846
|
+
return { ok: true as const, attachment: uploaded.attachment };
|
|
2847
|
+
} catch (err) {
|
|
2848
|
+
return {
|
|
2849
|
+
ok: false as const,
|
|
2850
|
+
fileName: file.fileName ?? file.filePath ?? "(unnamed)",
|
|
2851
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
}),
|
|
2855
|
+
);
|
|
2856
|
+
return { success: true, ...result, attachments: attachmentResults };
|
|
2693
2857
|
}
|
|
2694
2858
|
|
|
2695
2859
|
case "harmony_update_card": {
|
|
@@ -3162,52 +3326,13 @@ async function handleToolCall(
|
|
|
3162
3326
|
args.contentType != null
|
|
3163
3327
|
? z.string().parse(args.contentType)
|
|
3164
3328
|
: undefined;
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
"attachment",
|
|
3173
|
-
);
|
|
3174
|
-
const resolvedName = fileName || basename(filePath);
|
|
3175
|
-
const signed = await client.requestCardAttachmentUploadUrl(cardId, {
|
|
3176
|
-
fileName: resolvedName,
|
|
3177
|
-
fileType: contentType,
|
|
3178
|
-
size: bytes.byteLength,
|
|
3179
|
-
});
|
|
3180
|
-
await putToSignedUrl(
|
|
3181
|
-
signed.uploadUrl,
|
|
3182
|
-
bytes,
|
|
3183
|
-
contentType || signed.fileType || "application/octet-stream",
|
|
3184
|
-
);
|
|
3185
|
-
return await client.finalizeCardAttachment(cardId, {
|
|
3186
|
-
storagePath: signed.storagePath,
|
|
3187
|
-
fileName: resolvedName,
|
|
3188
|
-
fileType: contentType || signed.fileType,
|
|
3189
|
-
sha256: sha256Hex(bytes),
|
|
3190
|
-
size: bytes.byteLength,
|
|
3191
|
-
});
|
|
3192
|
-
}
|
|
3193
|
-
|
|
3194
|
-
if (base64Data) {
|
|
3195
|
-
if (!fileName) {
|
|
3196
|
-
throw new Error("fileName is required when using base64Data.");
|
|
3197
|
-
}
|
|
3198
|
-
if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
|
|
3199
|
-
throw new Error(
|
|
3200
|
-
`File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`,
|
|
3201
|
-
);
|
|
3202
|
-
}
|
|
3203
|
-
return await client.uploadCardAttachment(cardId, {
|
|
3204
|
-
fileName,
|
|
3205
|
-
data: base64Data,
|
|
3206
|
-
fileType: contentType,
|
|
3207
|
-
});
|
|
3208
|
-
}
|
|
3209
|
-
|
|
3210
|
-
throw new Error("Provide either filePath or base64Data.");
|
|
3329
|
+
// Same handshake as the create-card `attachments` path (shared helper).
|
|
3330
|
+
return await attachFileToCard(client, cardId, {
|
|
3331
|
+
filePath,
|
|
3332
|
+
base64Data,
|
|
3333
|
+
fileName,
|
|
3334
|
+
contentType,
|
|
3335
|
+
});
|
|
3211
3336
|
}
|
|
3212
3337
|
|
|
3213
3338
|
// target === "artifact"
|
package/src/tui/setup.ts
CHANGED
|
@@ -515,14 +515,29 @@ async function getAgentFiles(
|
|
|
515
515
|
|
|
516
516
|
This project uses Harmony for task management. When working on tasks:
|
|
517
517
|
|
|
518
|
+
## Agent identity — always identify as yourself
|
|
519
|
+
|
|
520
|
+
Every \`harmony_start_agent_session\` call passes \`agentIdentifier\` + \`agentName\`. **Use your own
|
|
521
|
+
identity, never a hardcoded one from this file.** AGENTS.md is a cross-runtime convention file, so
|
|
522
|
+
more than one kind of agent will read it; the board shows agents as teammates, and a session
|
|
523
|
+
attributed to the wrong runtime misattributes the work in front of the whole team.
|
|
524
|
+
|
|
525
|
+
- \`agentIdentifier\` — a stable kebab-case id for the runtime you actually are
|
|
526
|
+
- \`agentName\` — its human-readable name
|
|
527
|
+
|
|
528
|
+
Known values: \`claude-code\` / "Claude Code", \`codex\` / "OpenAI Codex", \`cursor\` / "Cursor",
|
|
529
|
+
\`claude-desktop\` / "Claude Desktop". If you are a runtime not listed here, use your own name rather
|
|
530
|
+
than borrowing the closest entry.
|
|
531
|
+
|
|
518
532
|
## Starting Work on a Card
|
|
519
533
|
|
|
520
534
|
When given a card reference (e.g., #42 or a card name), follow this workflow:
|
|
521
535
|
|
|
522
|
-
1. Use \`
|
|
536
|
+
1. Use \`harmony_get_card\` or \`harmony_search_cards\` to find the card
|
|
523
537
|
2. Move the card to "In Progress" using \`harmony_move_card\`
|
|
524
538
|
3. Add the "agent" label using \`harmony_add_label_to_card\`
|
|
525
|
-
4. Start a session with \`harmony_start_agent_session
|
|
539
|
+
4. Start a session with \`harmony_start_agent_session\`, passing **your own** \`agentIdentifier\` +
|
|
540
|
+
\`agentName\` (see "Agent identity" above)
|
|
526
541
|
5. Show the card details to the user
|
|
527
542
|
6. Use \`harmony_generate_prompt\` to get guidance, then implement the solution
|
|
528
543
|
7. Update progress periodically with \`harmony_update_agent_progress\`
|
|
@@ -533,7 +548,8 @@ When given a card reference (e.g., #42 or a card name), follow this workflow:
|
|
|
533
548
|
Before implementing a plan or feature, check if it maps to an existing Harmony card:
|
|
534
549
|
|
|
535
550
|
1. Use \`harmony_search_cards\` with keywords from the task description
|
|
536
|
-
2. If a match is found, call \`harmony_start_agent_session\`
|
|
551
|
+
2. If a match is found, call \`harmony_start_agent_session\` with **your own** \`agentIdentifier\` +
|
|
552
|
+
\`agentName\` (see "Agent identity" above), plus \`moveToColumn: "In Progress"\`, \`addLabels: ["agent"]\`
|
|
537
553
|
3. Update progress with \`harmony_update_agent_progress\` at milestones
|
|
538
554
|
4. When done, call \`harmony_end_agent_session\` with status: "completed", moveToColumn: "Review"
|
|
539
555
|
|