@gethmy/mcp 2.5.7 → 2.7.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 +186 -4
- package/dist/index.js +186 -4
- package/dist/lib/api-client.js +119 -0
- package/package.json +1 -1
- package/src/api-client.ts +176 -2
- package/src/auto-session.ts +8 -4
- package/src/prompt-builder.ts +53 -0
- package/src/server.ts +74 -0
package/dist/cli.js
CHANGED
|
@@ -69,6 +69,34 @@ function formatLabels(labels) {
|
|
|
69
69
|
return `
|
|
70
70
|
**Labels:** ${labels.map((l) => l.name).join(", ")}`;
|
|
71
71
|
}
|
|
72
|
+
function formatAttachments(attachments) {
|
|
73
|
+
if (!attachments || attachments.length === 0)
|
|
74
|
+
return "";
|
|
75
|
+
const lines = attachments.map((att) => {
|
|
76
|
+
const isImage = att.file_type.startsWith("image/");
|
|
77
|
+
const sizeKb = Math.max(1, Math.round(att.file_size / 1024));
|
|
78
|
+
const url = att.signed_url ?? "(signed URL unavailable)";
|
|
79
|
+
const kind = isImage ? "image" : att.file_type;
|
|
80
|
+
return ` - ${att.file_name} (${kind}, ${sizeKb} KB) — ${url}`;
|
|
81
|
+
});
|
|
82
|
+
return `
|
|
83
|
+
## Attachments
|
|
84
|
+
*Signed URLs expire in ~1 hour. Re-fetch via \`harmony_get_card_attachments\` if expired.*
|
|
85
|
+
${lines.join(`
|
|
86
|
+
`)}`;
|
|
87
|
+
}
|
|
88
|
+
function formatExternalLinks(refs) {
|
|
89
|
+
if (!refs || refs.length === 0)
|
|
90
|
+
return "";
|
|
91
|
+
const lines = refs.map((ref) => {
|
|
92
|
+
const label = ref.title?.trim() || ref.url;
|
|
93
|
+
return ` - ${label} — ${ref.url}`;
|
|
94
|
+
});
|
|
95
|
+
return `
|
|
96
|
+
## External References
|
|
97
|
+
${lines.join(`
|
|
98
|
+
`)}`;
|
|
99
|
+
}
|
|
72
100
|
function formatLinkedCards(links) {
|
|
73
101
|
if (!links || links.length === 0)
|
|
74
102
|
return "";
|
|
@@ -102,11 +130,15 @@ function generatePrompt(options) {
|
|
|
102
130
|
includePriority: true,
|
|
103
131
|
includeLinks: true,
|
|
104
132
|
includeColumn: true,
|
|
133
|
+
includeAttachments: true,
|
|
134
|
+
includeExternalLinks: true,
|
|
105
135
|
...options.contextOptions
|
|
106
136
|
};
|
|
107
137
|
const labels = card.labels || [];
|
|
108
138
|
const subtasks = card.subtasks || [];
|
|
109
139
|
const links = card.links || [];
|
|
140
|
+
const attachments = card.attachments || [];
|
|
141
|
+
const externalLinks = card.external_links || [];
|
|
110
142
|
const category = inferCategoryFromLabels(labels);
|
|
111
143
|
const roleFraming = getRoleFraming(category);
|
|
112
144
|
const sections = [];
|
|
@@ -143,6 +175,12 @@ ${card.description}`);
|
|
|
143
175
|
if (contextOpts.includeLinks && links.length > 0) {
|
|
144
176
|
sections.push(formatLinkedCards(links));
|
|
145
177
|
}
|
|
178
|
+
if (contextOpts.includeAttachments && attachments.length > 0) {
|
|
179
|
+
sections.push(formatAttachments(attachments));
|
|
180
|
+
}
|
|
181
|
+
if (contextOpts.includeExternalLinks && externalLinks.length > 0) {
|
|
182
|
+
sections.push(formatExternalLinks(externalLinks));
|
|
183
|
+
}
|
|
146
184
|
sections.push(`
|
|
147
185
|
## Focus Areas`);
|
|
148
186
|
roleFraming.focus.forEach((f) => {
|
|
@@ -1330,6 +1368,12 @@ class HarmonyApiClient {
|
|
|
1330
1368
|
async listProjects(workspaceId) {
|
|
1331
1369
|
return this.request("GET", `/workspaces/${workspaceId}/projects`);
|
|
1332
1370
|
}
|
|
1371
|
+
async archiveProject(projectId) {
|
|
1372
|
+
return this.request("POST", `/projects/${projectId}/archive`);
|
|
1373
|
+
}
|
|
1374
|
+
async unarchiveProject(projectId) {
|
|
1375
|
+
return this.request("POST", `/projects/${projectId}/unarchive`);
|
|
1376
|
+
}
|
|
1333
1377
|
async getBoard(projectId, options) {
|
|
1334
1378
|
const params = new URLSearchParams;
|
|
1335
1379
|
if (options?.limit !== undefined)
|
|
@@ -1399,6 +1443,12 @@ class HarmonyApiClient {
|
|
|
1399
1443
|
async getCardLinks(cardId) {
|
|
1400
1444
|
return this.request("GET", `/cards/${cardId}/links`);
|
|
1401
1445
|
}
|
|
1446
|
+
async getCardAttachments(cardId) {
|
|
1447
|
+
return this.request("GET", `/cards/${cardId}/attachments`);
|
|
1448
|
+
}
|
|
1449
|
+
async getCardExternalLinks(cardId) {
|
|
1450
|
+
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
1451
|
+
}
|
|
1402
1452
|
async createColumn(projectId, name) {
|
|
1403
1453
|
return this.request("POST", "/columns", { projectId, name });
|
|
1404
1454
|
}
|
|
@@ -1479,6 +1529,51 @@ class HarmonyApiClient {
|
|
|
1479
1529
|
async updateMemoryEntity(entityId, updates) {
|
|
1480
1530
|
return this.request("PUT", `/memory/entities/${entityId}`, updates);
|
|
1481
1531
|
}
|
|
1532
|
+
async harmonyRecall(options) {
|
|
1533
|
+
const fetchLimit = Math.max(options.topK ?? 3, 50);
|
|
1534
|
+
let entities = [];
|
|
1535
|
+
if (options.query) {
|
|
1536
|
+
const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
|
|
1537
|
+
project_id: options.projectId,
|
|
1538
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
1539
|
+
limit: fetchLimit
|
|
1540
|
+
});
|
|
1541
|
+
entities = search.entities ?? [];
|
|
1542
|
+
} else {
|
|
1543
|
+
const list = await this.listMemoryEntities({
|
|
1544
|
+
workspace_id: options.workspaceId,
|
|
1545
|
+
project_id: options.projectId,
|
|
1546
|
+
scope: options.scope,
|
|
1547
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
1548
|
+
tags: options.tags,
|
|
1549
|
+
min_confidence: options.minConfidence,
|
|
1550
|
+
limit: fetchLimit
|
|
1551
|
+
});
|
|
1552
|
+
entities = list.entities ?? [];
|
|
1553
|
+
}
|
|
1554
|
+
if (options.type && options.type.length > 1) {
|
|
1555
|
+
const allowed = new Set(options.type);
|
|
1556
|
+
entities = entities.filter((e) => allowed.has(e.type));
|
|
1557
|
+
}
|
|
1558
|
+
if (options.memory_tier) {
|
|
1559
|
+
entities = entities.filter((e) => e.memory_tier === options.memory_tier);
|
|
1560
|
+
}
|
|
1561
|
+
if (options.scope) {
|
|
1562
|
+
entities = entities.filter((e) => e.scope === options.scope);
|
|
1563
|
+
}
|
|
1564
|
+
if (options.tags?.length) {
|
|
1565
|
+
const wanted = new Set(options.tags);
|
|
1566
|
+
entities = entities.filter((e) => (e.tags ?? []).some((t) => wanted.has(t)));
|
|
1567
|
+
}
|
|
1568
|
+
if (typeof options.minConfidence === "number") {
|
|
1569
|
+
const threshold = options.minConfidence;
|
|
1570
|
+
entities = entities.filter((e) => typeof e.confidence === "number" && e.confidence >= threshold);
|
|
1571
|
+
}
|
|
1572
|
+
if (options.topK !== undefined) {
|
|
1573
|
+
entities = entities.slice(0, options.topK);
|
|
1574
|
+
}
|
|
1575
|
+
return { entities };
|
|
1576
|
+
}
|
|
1482
1577
|
async deleteMemoryEntity(entityId) {
|
|
1483
1578
|
return this.request("DELETE", `/memory/entities/${entityId}`);
|
|
1484
1579
|
}
|
|
@@ -1665,6 +1760,30 @@ class HarmonyApiClient {
|
|
|
1665
1760
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1666
1761
|
console.debug(`[generateCardPrompt] getCardLinks failed: ${msg}`);
|
|
1667
1762
|
}
|
|
1763
|
+
try {
|
|
1764
|
+
const attachmentsResult = await this.getCardAttachments(options.cardId);
|
|
1765
|
+
cardData.attachments = (attachmentsResult.attachments ?? []).map((att) => ({
|
|
1766
|
+
id: att.id,
|
|
1767
|
+
file_name: att.file_name,
|
|
1768
|
+
file_type: att.file_type,
|
|
1769
|
+
file_size: att.file_size,
|
|
1770
|
+
signed_url: att.signed_url
|
|
1771
|
+
}));
|
|
1772
|
+
} catch (err) {
|
|
1773
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1774
|
+
console.debug(`[generateCardPrompt] getCardAttachments failed: ${msg}`);
|
|
1775
|
+
}
|
|
1776
|
+
try {
|
|
1777
|
+
const externalLinksResult = await this.getCardExternalLinks(options.cardId);
|
|
1778
|
+
cardData.external_links = (externalLinksResult.external_links ?? []).map((link) => ({
|
|
1779
|
+
id: link.id,
|
|
1780
|
+
url: link.url,
|
|
1781
|
+
title: link.title
|
|
1782
|
+
}));
|
|
1783
|
+
} catch (err) {
|
|
1784
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1785
|
+
console.debug(`[generateCardPrompt] getCardExternalLinks failed: ${msg}`);
|
|
1786
|
+
}
|
|
1668
1787
|
let columnData = null;
|
|
1669
1788
|
const projectIdForBoard = options.projectId || cardData.project_id;
|
|
1670
1789
|
if (projectIdForBoard) {
|
|
@@ -1785,11 +1904,8 @@ function resolveAgentIdentity(info) {
|
|
|
1785
1904
|
var AUTO_START_TRIGGERS = new Set([
|
|
1786
1905
|
"harmony_generate_prompt",
|
|
1787
1906
|
"harmony_update_card",
|
|
1788
|
-
"harmony_move_card",
|
|
1789
1907
|
"harmony_create_subtask",
|
|
1790
|
-
"harmony_toggle_subtask"
|
|
1791
|
-
"harmony_add_label_to_card",
|
|
1792
|
-
"harmony_remove_label_from_card"
|
|
1908
|
+
"harmony_toggle_subtask"
|
|
1793
1909
|
]);
|
|
1794
1910
|
var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1795
1911
|
var CHECK_INTERVAL_MS = 60 * 1000;
|
|
@@ -2459,6 +2575,26 @@ var TOOLS = {
|
|
|
2459
2575
|
required: ["cardId"]
|
|
2460
2576
|
}
|
|
2461
2577
|
},
|
|
2578
|
+
harmony_archive_project: {
|
|
2579
|
+
description: "Archive a project. Hides the project (and all its columns and cards) from the active workspace. Can be restored later with harmony_unarchive_project.",
|
|
2580
|
+
inputSchema: {
|
|
2581
|
+
type: "object",
|
|
2582
|
+
properties: {
|
|
2583
|
+
projectId: { type: "string", description: "Project ID to archive" }
|
|
2584
|
+
},
|
|
2585
|
+
required: ["projectId"]
|
|
2586
|
+
}
|
|
2587
|
+
},
|
|
2588
|
+
harmony_unarchive_project: {
|
|
2589
|
+
description: "Restore an archived project so it (and its content) becomes visible again.",
|
|
2590
|
+
inputSchema: {
|
|
2591
|
+
type: "object",
|
|
2592
|
+
properties: {
|
|
2593
|
+
projectId: { type: "string", description: "Project ID to unarchive" }
|
|
2594
|
+
},
|
|
2595
|
+
required: ["projectId"]
|
|
2596
|
+
}
|
|
2597
|
+
},
|
|
2462
2598
|
harmony_delete_card: {
|
|
2463
2599
|
description: "Delete a card",
|
|
2464
2600
|
inputSchema: {
|
|
@@ -2647,6 +2783,32 @@ var TOOLS = {
|
|
|
2647
2783
|
required: ["cardId"]
|
|
2648
2784
|
}
|
|
2649
2785
|
},
|
|
2786
|
+
harmony_get_card_attachments: {
|
|
2787
|
+
description: "Get all file attachments for a card with short-lived signed URLs. URLs expire after `signed_url_expires_in` seconds (default 3600). Re-fetch to refresh.",
|
|
2788
|
+
inputSchema: {
|
|
2789
|
+
type: "object",
|
|
2790
|
+
properties: {
|
|
2791
|
+
cardId: {
|
|
2792
|
+
type: "string",
|
|
2793
|
+
description: "Card UUID"
|
|
2794
|
+
}
|
|
2795
|
+
},
|
|
2796
|
+
required: ["cardId"]
|
|
2797
|
+
}
|
|
2798
|
+
},
|
|
2799
|
+
harmony_get_card_external_links: {
|
|
2800
|
+
description: "Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
|
|
2801
|
+
inputSchema: {
|
|
2802
|
+
type: "object",
|
|
2803
|
+
properties: {
|
|
2804
|
+
cardId: {
|
|
2805
|
+
type: "string",
|
|
2806
|
+
description: "Card UUID"
|
|
2807
|
+
}
|
|
2808
|
+
},
|
|
2809
|
+
required: ["cardId"]
|
|
2810
|
+
}
|
|
2811
|
+
},
|
|
2650
2812
|
harmony_create_subtask: {
|
|
2651
2813
|
description: "Create a subtask on a card",
|
|
2652
2814
|
inputSchema: {
|
|
@@ -3785,6 +3947,16 @@ async function handleToolCall(name, args, deps) {
|
|
|
3785
3947
|
const result = await client3.unarchiveCard(cardId);
|
|
3786
3948
|
return { success: true, ...result };
|
|
3787
3949
|
}
|
|
3950
|
+
case "harmony_archive_project": {
|
|
3951
|
+
const projectId = z.string().uuid().parse(args.projectId);
|
|
3952
|
+
const result = await client3.archiveProject(projectId);
|
|
3953
|
+
return { success: true, ...result };
|
|
3954
|
+
}
|
|
3955
|
+
case "harmony_unarchive_project": {
|
|
3956
|
+
const projectId = z.string().uuid().parse(args.projectId);
|
|
3957
|
+
const result = await client3.unarchiveProject(projectId);
|
|
3958
|
+
return { success: true, ...result };
|
|
3959
|
+
}
|
|
3788
3960
|
case "harmony_delete_card": {
|
|
3789
3961
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
3790
3962
|
await client3.deleteCard(cardId);
|
|
@@ -3902,6 +4074,16 @@ async function handleToolCall(name, args, deps) {
|
|
|
3902
4074
|
const result = await client3.getCardLinks(cardId);
|
|
3903
4075
|
return result;
|
|
3904
4076
|
}
|
|
4077
|
+
case "harmony_get_card_attachments": {
|
|
4078
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
4079
|
+
const result = await client3.getCardAttachments(cardId);
|
|
4080
|
+
return result;
|
|
4081
|
+
}
|
|
4082
|
+
case "harmony_get_card_external_links": {
|
|
4083
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
4084
|
+
const result = await client3.getCardExternalLinks(cardId);
|
|
4085
|
+
return result;
|
|
4086
|
+
}
|
|
3905
4087
|
case "harmony_create_subtask": {
|
|
3906
4088
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
3907
4089
|
const title = z.string().min(1).max(500).parse(args.title);
|
package/dist/index.js
CHANGED
|
@@ -69,6 +69,34 @@ function formatLabels(labels) {
|
|
|
69
69
|
return `
|
|
70
70
|
**Labels:** ${labels.map((l) => l.name).join(", ")}`;
|
|
71
71
|
}
|
|
72
|
+
function formatAttachments(attachments) {
|
|
73
|
+
if (!attachments || attachments.length === 0)
|
|
74
|
+
return "";
|
|
75
|
+
const lines = attachments.map((att) => {
|
|
76
|
+
const isImage = att.file_type.startsWith("image/");
|
|
77
|
+
const sizeKb = Math.max(1, Math.round(att.file_size / 1024));
|
|
78
|
+
const url = att.signed_url ?? "(signed URL unavailable)";
|
|
79
|
+
const kind = isImage ? "image" : att.file_type;
|
|
80
|
+
return ` - ${att.file_name} (${kind}, ${sizeKb} KB) — ${url}`;
|
|
81
|
+
});
|
|
82
|
+
return `
|
|
83
|
+
## Attachments
|
|
84
|
+
*Signed URLs expire in ~1 hour. Re-fetch via \`harmony_get_card_attachments\` if expired.*
|
|
85
|
+
${lines.join(`
|
|
86
|
+
`)}`;
|
|
87
|
+
}
|
|
88
|
+
function formatExternalLinks(refs) {
|
|
89
|
+
if (!refs || refs.length === 0)
|
|
90
|
+
return "";
|
|
91
|
+
const lines = refs.map((ref) => {
|
|
92
|
+
const label = ref.title?.trim() || ref.url;
|
|
93
|
+
return ` - ${label} — ${ref.url}`;
|
|
94
|
+
});
|
|
95
|
+
return `
|
|
96
|
+
## External References
|
|
97
|
+
${lines.join(`
|
|
98
|
+
`)}`;
|
|
99
|
+
}
|
|
72
100
|
function formatLinkedCards(links) {
|
|
73
101
|
if (!links || links.length === 0)
|
|
74
102
|
return "";
|
|
@@ -102,11 +130,15 @@ function generatePrompt(options) {
|
|
|
102
130
|
includePriority: true,
|
|
103
131
|
includeLinks: true,
|
|
104
132
|
includeColumn: true,
|
|
133
|
+
includeAttachments: true,
|
|
134
|
+
includeExternalLinks: true,
|
|
105
135
|
...options.contextOptions
|
|
106
136
|
};
|
|
107
137
|
const labels = card.labels || [];
|
|
108
138
|
const subtasks = card.subtasks || [];
|
|
109
139
|
const links = card.links || [];
|
|
140
|
+
const attachments = card.attachments || [];
|
|
141
|
+
const externalLinks = card.external_links || [];
|
|
110
142
|
const category = inferCategoryFromLabels(labels);
|
|
111
143
|
const roleFraming = getRoleFraming(category);
|
|
112
144
|
const sections = [];
|
|
@@ -143,6 +175,12 @@ ${card.description}`);
|
|
|
143
175
|
if (contextOpts.includeLinks && links.length > 0) {
|
|
144
176
|
sections.push(formatLinkedCards(links));
|
|
145
177
|
}
|
|
178
|
+
if (contextOpts.includeAttachments && attachments.length > 0) {
|
|
179
|
+
sections.push(formatAttachments(attachments));
|
|
180
|
+
}
|
|
181
|
+
if (contextOpts.includeExternalLinks && externalLinks.length > 0) {
|
|
182
|
+
sections.push(formatExternalLinks(externalLinks));
|
|
183
|
+
}
|
|
146
184
|
sections.push(`
|
|
147
185
|
## Focus Areas`);
|
|
148
186
|
roleFraming.focus.forEach((f) => {
|
|
@@ -1326,6 +1364,12 @@ class HarmonyApiClient {
|
|
|
1326
1364
|
async listProjects(workspaceId) {
|
|
1327
1365
|
return this.request("GET", `/workspaces/${workspaceId}/projects`);
|
|
1328
1366
|
}
|
|
1367
|
+
async archiveProject(projectId) {
|
|
1368
|
+
return this.request("POST", `/projects/${projectId}/archive`);
|
|
1369
|
+
}
|
|
1370
|
+
async unarchiveProject(projectId) {
|
|
1371
|
+
return this.request("POST", `/projects/${projectId}/unarchive`);
|
|
1372
|
+
}
|
|
1329
1373
|
async getBoard(projectId, options) {
|
|
1330
1374
|
const params = new URLSearchParams;
|
|
1331
1375
|
if (options?.limit !== undefined)
|
|
@@ -1395,6 +1439,12 @@ class HarmonyApiClient {
|
|
|
1395
1439
|
async getCardLinks(cardId) {
|
|
1396
1440
|
return this.request("GET", `/cards/${cardId}/links`);
|
|
1397
1441
|
}
|
|
1442
|
+
async getCardAttachments(cardId) {
|
|
1443
|
+
return this.request("GET", `/cards/${cardId}/attachments`);
|
|
1444
|
+
}
|
|
1445
|
+
async getCardExternalLinks(cardId) {
|
|
1446
|
+
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
1447
|
+
}
|
|
1398
1448
|
async createColumn(projectId, name) {
|
|
1399
1449
|
return this.request("POST", "/columns", { projectId, name });
|
|
1400
1450
|
}
|
|
@@ -1475,6 +1525,51 @@ class HarmonyApiClient {
|
|
|
1475
1525
|
async updateMemoryEntity(entityId, updates) {
|
|
1476
1526
|
return this.request("PUT", `/memory/entities/${entityId}`, updates);
|
|
1477
1527
|
}
|
|
1528
|
+
async harmonyRecall(options) {
|
|
1529
|
+
const fetchLimit = Math.max(options.topK ?? 3, 50);
|
|
1530
|
+
let entities = [];
|
|
1531
|
+
if (options.query) {
|
|
1532
|
+
const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
|
|
1533
|
+
project_id: options.projectId,
|
|
1534
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
1535
|
+
limit: fetchLimit
|
|
1536
|
+
});
|
|
1537
|
+
entities = search.entities ?? [];
|
|
1538
|
+
} else {
|
|
1539
|
+
const list = await this.listMemoryEntities({
|
|
1540
|
+
workspace_id: options.workspaceId,
|
|
1541
|
+
project_id: options.projectId,
|
|
1542
|
+
scope: options.scope,
|
|
1543
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
1544
|
+
tags: options.tags,
|
|
1545
|
+
min_confidence: options.minConfidence,
|
|
1546
|
+
limit: fetchLimit
|
|
1547
|
+
});
|
|
1548
|
+
entities = list.entities ?? [];
|
|
1549
|
+
}
|
|
1550
|
+
if (options.type && options.type.length > 1) {
|
|
1551
|
+
const allowed = new Set(options.type);
|
|
1552
|
+
entities = entities.filter((e) => allowed.has(e.type));
|
|
1553
|
+
}
|
|
1554
|
+
if (options.memory_tier) {
|
|
1555
|
+
entities = entities.filter((e) => e.memory_tier === options.memory_tier);
|
|
1556
|
+
}
|
|
1557
|
+
if (options.scope) {
|
|
1558
|
+
entities = entities.filter((e) => e.scope === options.scope);
|
|
1559
|
+
}
|
|
1560
|
+
if (options.tags?.length) {
|
|
1561
|
+
const wanted = new Set(options.tags);
|
|
1562
|
+
entities = entities.filter((e) => (e.tags ?? []).some((t) => wanted.has(t)));
|
|
1563
|
+
}
|
|
1564
|
+
if (typeof options.minConfidence === "number") {
|
|
1565
|
+
const threshold = options.minConfidence;
|
|
1566
|
+
entities = entities.filter((e) => typeof e.confidence === "number" && e.confidence >= threshold);
|
|
1567
|
+
}
|
|
1568
|
+
if (options.topK !== undefined) {
|
|
1569
|
+
entities = entities.slice(0, options.topK);
|
|
1570
|
+
}
|
|
1571
|
+
return { entities };
|
|
1572
|
+
}
|
|
1478
1573
|
async deleteMemoryEntity(entityId) {
|
|
1479
1574
|
return this.request("DELETE", `/memory/entities/${entityId}`);
|
|
1480
1575
|
}
|
|
@@ -1661,6 +1756,30 @@ class HarmonyApiClient {
|
|
|
1661
1756
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1662
1757
|
console.debug(`[generateCardPrompt] getCardLinks failed: ${msg}`);
|
|
1663
1758
|
}
|
|
1759
|
+
try {
|
|
1760
|
+
const attachmentsResult = await this.getCardAttachments(options.cardId);
|
|
1761
|
+
cardData.attachments = (attachmentsResult.attachments ?? []).map((att) => ({
|
|
1762
|
+
id: att.id,
|
|
1763
|
+
file_name: att.file_name,
|
|
1764
|
+
file_type: att.file_type,
|
|
1765
|
+
file_size: att.file_size,
|
|
1766
|
+
signed_url: att.signed_url
|
|
1767
|
+
}));
|
|
1768
|
+
} catch (err) {
|
|
1769
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1770
|
+
console.debug(`[generateCardPrompt] getCardAttachments failed: ${msg}`);
|
|
1771
|
+
}
|
|
1772
|
+
try {
|
|
1773
|
+
const externalLinksResult = await this.getCardExternalLinks(options.cardId);
|
|
1774
|
+
cardData.external_links = (externalLinksResult.external_links ?? []).map((link) => ({
|
|
1775
|
+
id: link.id,
|
|
1776
|
+
url: link.url,
|
|
1777
|
+
title: link.title
|
|
1778
|
+
}));
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1781
|
+
console.debug(`[generateCardPrompt] getCardExternalLinks failed: ${msg}`);
|
|
1782
|
+
}
|
|
1664
1783
|
let columnData = null;
|
|
1665
1784
|
const projectIdForBoard = options.projectId || cardData.project_id;
|
|
1666
1785
|
if (projectIdForBoard) {
|
|
@@ -1781,11 +1900,8 @@ function resolveAgentIdentity(info) {
|
|
|
1781
1900
|
var AUTO_START_TRIGGERS = new Set([
|
|
1782
1901
|
"harmony_generate_prompt",
|
|
1783
1902
|
"harmony_update_card",
|
|
1784
|
-
"harmony_move_card",
|
|
1785
1903
|
"harmony_create_subtask",
|
|
1786
|
-
"harmony_toggle_subtask"
|
|
1787
|
-
"harmony_add_label_to_card",
|
|
1788
|
-
"harmony_remove_label_from_card"
|
|
1904
|
+
"harmony_toggle_subtask"
|
|
1789
1905
|
]);
|
|
1790
1906
|
var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1791
1907
|
var CHECK_INTERVAL_MS = 60 * 1000;
|
|
@@ -2455,6 +2571,26 @@ var TOOLS = {
|
|
|
2455
2571
|
required: ["cardId"]
|
|
2456
2572
|
}
|
|
2457
2573
|
},
|
|
2574
|
+
harmony_archive_project: {
|
|
2575
|
+
description: "Archive a project. Hides the project (and all its columns and cards) from the active workspace. Can be restored later with harmony_unarchive_project.",
|
|
2576
|
+
inputSchema: {
|
|
2577
|
+
type: "object",
|
|
2578
|
+
properties: {
|
|
2579
|
+
projectId: { type: "string", description: "Project ID to archive" }
|
|
2580
|
+
},
|
|
2581
|
+
required: ["projectId"]
|
|
2582
|
+
}
|
|
2583
|
+
},
|
|
2584
|
+
harmony_unarchive_project: {
|
|
2585
|
+
description: "Restore an archived project so it (and its content) becomes visible again.",
|
|
2586
|
+
inputSchema: {
|
|
2587
|
+
type: "object",
|
|
2588
|
+
properties: {
|
|
2589
|
+
projectId: { type: "string", description: "Project ID to unarchive" }
|
|
2590
|
+
},
|
|
2591
|
+
required: ["projectId"]
|
|
2592
|
+
}
|
|
2593
|
+
},
|
|
2458
2594
|
harmony_delete_card: {
|
|
2459
2595
|
description: "Delete a card",
|
|
2460
2596
|
inputSchema: {
|
|
@@ -2643,6 +2779,32 @@ var TOOLS = {
|
|
|
2643
2779
|
required: ["cardId"]
|
|
2644
2780
|
}
|
|
2645
2781
|
},
|
|
2782
|
+
harmony_get_card_attachments: {
|
|
2783
|
+
description: "Get all file attachments for a card with short-lived signed URLs. URLs expire after `signed_url_expires_in` seconds (default 3600). Re-fetch to refresh.",
|
|
2784
|
+
inputSchema: {
|
|
2785
|
+
type: "object",
|
|
2786
|
+
properties: {
|
|
2787
|
+
cardId: {
|
|
2788
|
+
type: "string",
|
|
2789
|
+
description: "Card UUID"
|
|
2790
|
+
}
|
|
2791
|
+
},
|
|
2792
|
+
required: ["cardId"]
|
|
2793
|
+
}
|
|
2794
|
+
},
|
|
2795
|
+
harmony_get_card_external_links: {
|
|
2796
|
+
description: "Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
|
|
2797
|
+
inputSchema: {
|
|
2798
|
+
type: "object",
|
|
2799
|
+
properties: {
|
|
2800
|
+
cardId: {
|
|
2801
|
+
type: "string",
|
|
2802
|
+
description: "Card UUID"
|
|
2803
|
+
}
|
|
2804
|
+
},
|
|
2805
|
+
required: ["cardId"]
|
|
2806
|
+
}
|
|
2807
|
+
},
|
|
2646
2808
|
harmony_create_subtask: {
|
|
2647
2809
|
description: "Create a subtask on a card",
|
|
2648
2810
|
inputSchema: {
|
|
@@ -3781,6 +3943,16 @@ async function handleToolCall(name, args, deps) {
|
|
|
3781
3943
|
const result = await client3.unarchiveCard(cardId);
|
|
3782
3944
|
return { success: true, ...result };
|
|
3783
3945
|
}
|
|
3946
|
+
case "harmony_archive_project": {
|
|
3947
|
+
const projectId = z.string().uuid().parse(args.projectId);
|
|
3948
|
+
const result = await client3.archiveProject(projectId);
|
|
3949
|
+
return { success: true, ...result };
|
|
3950
|
+
}
|
|
3951
|
+
case "harmony_unarchive_project": {
|
|
3952
|
+
const projectId = z.string().uuid().parse(args.projectId);
|
|
3953
|
+
const result = await client3.unarchiveProject(projectId);
|
|
3954
|
+
return { success: true, ...result };
|
|
3955
|
+
}
|
|
3784
3956
|
case "harmony_delete_card": {
|
|
3785
3957
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
3786
3958
|
await client3.deleteCard(cardId);
|
|
@@ -3898,6 +4070,16 @@ async function handleToolCall(name, args, deps) {
|
|
|
3898
4070
|
const result = await client3.getCardLinks(cardId);
|
|
3899
4071
|
return result;
|
|
3900
4072
|
}
|
|
4073
|
+
case "harmony_get_card_attachments": {
|
|
4074
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
4075
|
+
const result = await client3.getCardAttachments(cardId);
|
|
4076
|
+
return result;
|
|
4077
|
+
}
|
|
4078
|
+
case "harmony_get_card_external_links": {
|
|
4079
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
4080
|
+
const result = await client3.getCardExternalLinks(cardId);
|
|
4081
|
+
return result;
|
|
4082
|
+
}
|
|
3901
4083
|
case "harmony_create_subtask": {
|
|
3902
4084
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
3903
4085
|
const title = z.string().min(1).max(500).parse(args.title);
|
package/dist/lib/api-client.js
CHANGED
|
@@ -66,6 +66,34 @@ function formatLabels(labels) {
|
|
|
66
66
|
return `
|
|
67
67
|
**Labels:** ${labels.map((l) => l.name).join(", ")}`;
|
|
68
68
|
}
|
|
69
|
+
function formatAttachments(attachments) {
|
|
70
|
+
if (!attachments || attachments.length === 0)
|
|
71
|
+
return "";
|
|
72
|
+
const lines = attachments.map((att) => {
|
|
73
|
+
const isImage = att.file_type.startsWith("image/");
|
|
74
|
+
const sizeKb = Math.max(1, Math.round(att.file_size / 1024));
|
|
75
|
+
const url = att.signed_url ?? "(signed URL unavailable)";
|
|
76
|
+
const kind = isImage ? "image" : att.file_type;
|
|
77
|
+
return ` - ${att.file_name} (${kind}, ${sizeKb} KB) — ${url}`;
|
|
78
|
+
});
|
|
79
|
+
return `
|
|
80
|
+
## Attachments
|
|
81
|
+
*Signed URLs expire in ~1 hour. Re-fetch via \`harmony_get_card_attachments\` if expired.*
|
|
82
|
+
${lines.join(`
|
|
83
|
+
`)}`;
|
|
84
|
+
}
|
|
85
|
+
function formatExternalLinks(refs) {
|
|
86
|
+
if (!refs || refs.length === 0)
|
|
87
|
+
return "";
|
|
88
|
+
const lines = refs.map((ref) => {
|
|
89
|
+
const label = ref.title?.trim() || ref.url;
|
|
90
|
+
return ` - ${label} — ${ref.url}`;
|
|
91
|
+
});
|
|
92
|
+
return `
|
|
93
|
+
## External References
|
|
94
|
+
${lines.join(`
|
|
95
|
+
`)}`;
|
|
96
|
+
}
|
|
69
97
|
function formatLinkedCards(links) {
|
|
70
98
|
if (!links || links.length === 0)
|
|
71
99
|
return "";
|
|
@@ -99,11 +127,15 @@ function generatePrompt(options) {
|
|
|
99
127
|
includePriority: true,
|
|
100
128
|
includeLinks: true,
|
|
101
129
|
includeColumn: true,
|
|
130
|
+
includeAttachments: true,
|
|
131
|
+
includeExternalLinks: true,
|
|
102
132
|
...options.contextOptions
|
|
103
133
|
};
|
|
104
134
|
const labels = card.labels || [];
|
|
105
135
|
const subtasks = card.subtasks || [];
|
|
106
136
|
const links = card.links || [];
|
|
137
|
+
const attachments = card.attachments || [];
|
|
138
|
+
const externalLinks = card.external_links || [];
|
|
107
139
|
const category = inferCategoryFromLabels(labels);
|
|
108
140
|
const roleFraming = getRoleFraming(category);
|
|
109
141
|
const sections = [];
|
|
@@ -140,6 +172,12 @@ ${card.description}`);
|
|
|
140
172
|
if (contextOpts.includeLinks && links.length > 0) {
|
|
141
173
|
sections.push(formatLinkedCards(links));
|
|
142
174
|
}
|
|
175
|
+
if (contextOpts.includeAttachments && attachments.length > 0) {
|
|
176
|
+
sections.push(formatAttachments(attachments));
|
|
177
|
+
}
|
|
178
|
+
if (contextOpts.includeExternalLinks && externalLinks.length > 0) {
|
|
179
|
+
sections.push(formatExternalLinks(externalLinks));
|
|
180
|
+
}
|
|
143
181
|
sections.push(`
|
|
144
182
|
## Focus Areas`);
|
|
145
183
|
roleFraming.focus.forEach((f) => {
|
|
@@ -933,6 +971,12 @@ class HarmonyApiClient {
|
|
|
933
971
|
async listProjects(workspaceId) {
|
|
934
972
|
return this.request("GET", `/workspaces/${workspaceId}/projects`);
|
|
935
973
|
}
|
|
974
|
+
async archiveProject(projectId) {
|
|
975
|
+
return this.request("POST", `/projects/${projectId}/archive`);
|
|
976
|
+
}
|
|
977
|
+
async unarchiveProject(projectId) {
|
|
978
|
+
return this.request("POST", `/projects/${projectId}/unarchive`);
|
|
979
|
+
}
|
|
936
980
|
async getBoard(projectId, options) {
|
|
937
981
|
const params = new URLSearchParams;
|
|
938
982
|
if (options?.limit !== undefined)
|
|
@@ -1002,6 +1046,12 @@ class HarmonyApiClient {
|
|
|
1002
1046
|
async getCardLinks(cardId) {
|
|
1003
1047
|
return this.request("GET", `/cards/${cardId}/links`);
|
|
1004
1048
|
}
|
|
1049
|
+
async getCardAttachments(cardId) {
|
|
1050
|
+
return this.request("GET", `/cards/${cardId}/attachments`);
|
|
1051
|
+
}
|
|
1052
|
+
async getCardExternalLinks(cardId) {
|
|
1053
|
+
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
1054
|
+
}
|
|
1005
1055
|
async createColumn(projectId, name) {
|
|
1006
1056
|
return this.request("POST", "/columns", { projectId, name });
|
|
1007
1057
|
}
|
|
@@ -1082,6 +1132,51 @@ class HarmonyApiClient {
|
|
|
1082
1132
|
async updateMemoryEntity(entityId, updates) {
|
|
1083
1133
|
return this.request("PUT", `/memory/entities/${entityId}`, updates);
|
|
1084
1134
|
}
|
|
1135
|
+
async harmonyRecall(options) {
|
|
1136
|
+
const fetchLimit = Math.max(options.topK ?? 3, 50);
|
|
1137
|
+
let entities = [];
|
|
1138
|
+
if (options.query) {
|
|
1139
|
+
const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
|
|
1140
|
+
project_id: options.projectId,
|
|
1141
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
1142
|
+
limit: fetchLimit
|
|
1143
|
+
});
|
|
1144
|
+
entities = search.entities ?? [];
|
|
1145
|
+
} else {
|
|
1146
|
+
const list = await this.listMemoryEntities({
|
|
1147
|
+
workspace_id: options.workspaceId,
|
|
1148
|
+
project_id: options.projectId,
|
|
1149
|
+
scope: options.scope,
|
|
1150
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
1151
|
+
tags: options.tags,
|
|
1152
|
+
min_confidence: options.minConfidence,
|
|
1153
|
+
limit: fetchLimit
|
|
1154
|
+
});
|
|
1155
|
+
entities = list.entities ?? [];
|
|
1156
|
+
}
|
|
1157
|
+
if (options.type && options.type.length > 1) {
|
|
1158
|
+
const allowed = new Set(options.type);
|
|
1159
|
+
entities = entities.filter((e) => allowed.has(e.type));
|
|
1160
|
+
}
|
|
1161
|
+
if (options.memory_tier) {
|
|
1162
|
+
entities = entities.filter((e) => e.memory_tier === options.memory_tier);
|
|
1163
|
+
}
|
|
1164
|
+
if (options.scope) {
|
|
1165
|
+
entities = entities.filter((e) => e.scope === options.scope);
|
|
1166
|
+
}
|
|
1167
|
+
if (options.tags?.length) {
|
|
1168
|
+
const wanted = new Set(options.tags);
|
|
1169
|
+
entities = entities.filter((e) => (e.tags ?? []).some((t) => wanted.has(t)));
|
|
1170
|
+
}
|
|
1171
|
+
if (typeof options.minConfidence === "number") {
|
|
1172
|
+
const threshold = options.minConfidence;
|
|
1173
|
+
entities = entities.filter((e) => typeof e.confidence === "number" && e.confidence >= threshold);
|
|
1174
|
+
}
|
|
1175
|
+
if (options.topK !== undefined) {
|
|
1176
|
+
entities = entities.slice(0, options.topK);
|
|
1177
|
+
}
|
|
1178
|
+
return { entities };
|
|
1179
|
+
}
|
|
1085
1180
|
async deleteMemoryEntity(entityId) {
|
|
1086
1181
|
return this.request("DELETE", `/memory/entities/${entityId}`);
|
|
1087
1182
|
}
|
|
@@ -1268,6 +1363,30 @@ class HarmonyApiClient {
|
|
|
1268
1363
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1269
1364
|
console.debug(`[generateCardPrompt] getCardLinks failed: ${msg}`);
|
|
1270
1365
|
}
|
|
1366
|
+
try {
|
|
1367
|
+
const attachmentsResult = await this.getCardAttachments(options.cardId);
|
|
1368
|
+
cardData.attachments = (attachmentsResult.attachments ?? []).map((att) => ({
|
|
1369
|
+
id: att.id,
|
|
1370
|
+
file_name: att.file_name,
|
|
1371
|
+
file_type: att.file_type,
|
|
1372
|
+
file_size: att.file_size,
|
|
1373
|
+
signed_url: att.signed_url
|
|
1374
|
+
}));
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1377
|
+
console.debug(`[generateCardPrompt] getCardAttachments failed: ${msg}`);
|
|
1378
|
+
}
|
|
1379
|
+
try {
|
|
1380
|
+
const externalLinksResult = await this.getCardExternalLinks(options.cardId);
|
|
1381
|
+
cardData.external_links = (externalLinksResult.external_links ?? []).map((link) => ({
|
|
1382
|
+
id: link.id,
|
|
1383
|
+
url: link.url,
|
|
1384
|
+
title: link.title
|
|
1385
|
+
}));
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1388
|
+
console.debug(`[generateCardPrompt] getCardExternalLinks failed: ${msg}`);
|
|
1389
|
+
}
|
|
1271
1390
|
let columnData = null;
|
|
1272
1391
|
const projectIdForBoard = options.projectId || cardData.project_id;
|
|
1273
1392
|
if (projectIdForBoard) {
|
package/package.json
CHANGED
package/src/api-client.ts
CHANGED
|
@@ -124,6 +124,28 @@ export class HarmonyUnauthorizedError extends Error {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
export interface CardAttachment {
|
|
128
|
+
id: string;
|
|
129
|
+
card_id: string;
|
|
130
|
+
file_name: string;
|
|
131
|
+
file_type: string;
|
|
132
|
+
file_size: number;
|
|
133
|
+
storage_path: string;
|
|
134
|
+
uploaded_by: string | null;
|
|
135
|
+
created_at: string;
|
|
136
|
+
signed_url: string | null;
|
|
137
|
+
signed_url_expires_in: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface CardExternalLinkRow {
|
|
141
|
+
id: string;
|
|
142
|
+
card_id: string;
|
|
143
|
+
url: string;
|
|
144
|
+
title: string | null;
|
|
145
|
+
created_by: string | null;
|
|
146
|
+
created_at: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
127
149
|
export class HarmonyApiClient {
|
|
128
150
|
private apiKey: string;
|
|
129
151
|
private apiUrl: string;
|
|
@@ -376,6 +398,14 @@ export class HarmonyApiClient {
|
|
|
376
398
|
return this.request("GET", `/workspaces/${workspaceId}/projects`);
|
|
377
399
|
}
|
|
378
400
|
|
|
401
|
+
async archiveProject(projectId: string): Promise<{ project: unknown }> {
|
|
402
|
+
return this.request("POST", `/projects/${projectId}/archive`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async unarchiveProject(projectId: string): Promise<{ project: unknown }> {
|
|
406
|
+
return this.request("POST", `/projects/${projectId}/unarchive`);
|
|
407
|
+
}
|
|
408
|
+
|
|
379
409
|
async getBoard(
|
|
380
410
|
projectId: string,
|
|
381
411
|
options?: {
|
|
@@ -524,6 +554,18 @@ export class HarmonyApiClient {
|
|
|
524
554
|
return this.request("GET", `/cards/${cardId}/links`);
|
|
525
555
|
}
|
|
526
556
|
|
|
557
|
+
async getCardAttachments(
|
|
558
|
+
cardId: string,
|
|
559
|
+
): Promise<{ attachments: CardAttachment[] }> {
|
|
560
|
+
return this.request("GET", `/cards/${cardId}/attachments`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async getCardExternalLinks(
|
|
564
|
+
cardId: string,
|
|
565
|
+
): Promise<{ external_links: CardExternalLinkRow[] }> {
|
|
566
|
+
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
567
|
+
}
|
|
568
|
+
|
|
527
569
|
// ============ COLUMN OPERATIONS ============
|
|
528
570
|
|
|
529
571
|
async createColumn(
|
|
@@ -743,15 +785,101 @@ export class HarmonyApiClient {
|
|
|
743
785
|
scope?: string;
|
|
744
786
|
type?: string;
|
|
745
787
|
memory_tier?: string;
|
|
746
|
-
//
|
|
747
|
-
//
|
|
788
|
+
// Supersede semantics — used by Phase 1.5 review-reject back-fill to
|
|
789
|
+
// tombstone the original implement episode without hard-deleting it.
|
|
790
|
+
// The backend sets superseded_at automatically when superseded_by lands.
|
|
748
791
|
superseded_by?: string | null;
|
|
792
|
+
superseded_at?: string | null;
|
|
749
793
|
version?: number;
|
|
750
794
|
},
|
|
751
795
|
): Promise<{ entity: unknown; warnings?: string[] }> {
|
|
752
796
|
return this.request("PUT", `/memory/entities/${entityId}`, updates);
|
|
753
797
|
}
|
|
754
798
|
|
|
799
|
+
/**
|
|
800
|
+
* Retrieve memories filtered by type/tier/scope, optionally ranked by a
|
|
801
|
+
* free-text query. Wraps `searchMemoryEntities` (when a query is given)
|
|
802
|
+
* or `listMemoryEntities` and applies client-side filters that the REST
|
|
803
|
+
* surface doesn't natively expose (multi-type, memory_tier).
|
|
804
|
+
*
|
|
805
|
+
* Used by the agent daemon's read hook to surface similar past episodes
|
|
806
|
+
* before building a new task prompt (Phase 1.5).
|
|
807
|
+
*/
|
|
808
|
+
async harmonyRecall(options: {
|
|
809
|
+
workspaceId: string;
|
|
810
|
+
projectId?: string;
|
|
811
|
+
query?: string;
|
|
812
|
+
type?: string[];
|
|
813
|
+
memory_tier?: string;
|
|
814
|
+
scope?: string;
|
|
815
|
+
tags?: string[];
|
|
816
|
+
minConfidence?: number;
|
|
817
|
+
topK?: number;
|
|
818
|
+
}): Promise<{ entities: unknown[] }> {
|
|
819
|
+
// Over-fetch beyond topK so client-side filters (multi-type, memory_tier,
|
|
820
|
+
// tags) have headroom — matches the MCP server's recall path (server.ts).
|
|
821
|
+
const fetchLimit = Math.max(options.topK ?? 3, 50);
|
|
822
|
+
let entities: Array<Record<string, unknown>> = [];
|
|
823
|
+
|
|
824
|
+
if (options.query) {
|
|
825
|
+
// searchMemoryEntities accepts a single type — refine client-side
|
|
826
|
+
// when the caller passed multiple.
|
|
827
|
+
const search = await this.searchMemoryEntities(
|
|
828
|
+
options.workspaceId,
|
|
829
|
+
options.query,
|
|
830
|
+
{
|
|
831
|
+
project_id: options.projectId,
|
|
832
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
833
|
+
limit: fetchLimit,
|
|
834
|
+
},
|
|
835
|
+
);
|
|
836
|
+
entities = (search.entities ?? []) as Array<Record<string, unknown>>;
|
|
837
|
+
} else {
|
|
838
|
+
const list = await this.listMemoryEntities({
|
|
839
|
+
workspace_id: options.workspaceId,
|
|
840
|
+
project_id: options.projectId,
|
|
841
|
+
scope: options.scope,
|
|
842
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
843
|
+
tags: options.tags,
|
|
844
|
+
min_confidence: options.minConfidence,
|
|
845
|
+
limit: fetchLimit,
|
|
846
|
+
});
|
|
847
|
+
entities = (list.entities ?? []) as Array<Record<string, unknown>>;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Client-side filters: REST surface lacks multi-type and memory_tier.
|
|
851
|
+
if (options.type && options.type.length > 1) {
|
|
852
|
+
const allowed = new Set(options.type);
|
|
853
|
+
entities = entities.filter((e) => allowed.has(e.type as string));
|
|
854
|
+
}
|
|
855
|
+
if (options.memory_tier) {
|
|
856
|
+
entities = entities.filter((e) => e.memory_tier === options.memory_tier);
|
|
857
|
+
}
|
|
858
|
+
if (options.scope) {
|
|
859
|
+
entities = entities.filter((e) => e.scope === options.scope);
|
|
860
|
+
}
|
|
861
|
+
if (options.tags?.length) {
|
|
862
|
+
const wanted = new Set(options.tags);
|
|
863
|
+
entities = entities.filter((e) =>
|
|
864
|
+
((e.tags as string[]) ?? []).some((t) => wanted.has(t)),
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
if (typeof options.minConfidence === "number") {
|
|
868
|
+
const threshold = options.minConfidence;
|
|
869
|
+
entities = entities.filter(
|
|
870
|
+
(e) =>
|
|
871
|
+
typeof e.confidence === "number" &&
|
|
872
|
+
(e.confidence as number) >= threshold,
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (options.topK !== undefined) {
|
|
877
|
+
entities = entities.slice(0, options.topK);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return { entities };
|
|
881
|
+
}
|
|
882
|
+
|
|
755
883
|
async deleteMemoryEntity(entityId: string): Promise<{ success: boolean }> {
|
|
756
884
|
return this.request("DELETE", `/memory/entities/${entityId}`);
|
|
757
885
|
}
|
|
@@ -1196,6 +1324,40 @@ export class HarmonyApiClient {
|
|
|
1196
1324
|
console.debug(`[generateCardPrompt] getCardLinks failed: ${msg}`);
|
|
1197
1325
|
}
|
|
1198
1326
|
|
|
1327
|
+
// Fetch attachments + external links. Best-effort — never break prompt
|
|
1328
|
+
// generation if storage signing or the underlying tables fail.
|
|
1329
|
+
try {
|
|
1330
|
+
const attachmentsResult = await this.getCardAttachments(options.cardId);
|
|
1331
|
+
cardData.attachments = (attachmentsResult.attachments ?? []).map(
|
|
1332
|
+
(att) => ({
|
|
1333
|
+
id: att.id,
|
|
1334
|
+
file_name: att.file_name,
|
|
1335
|
+
file_type: att.file_type,
|
|
1336
|
+
file_size: att.file_size,
|
|
1337
|
+
signed_url: att.signed_url,
|
|
1338
|
+
}),
|
|
1339
|
+
);
|
|
1340
|
+
} catch (err) {
|
|
1341
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1342
|
+
console.debug(`[generateCardPrompt] getCardAttachments failed: ${msg}`);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
try {
|
|
1346
|
+
const externalLinksResult = await this.getCardExternalLinks(
|
|
1347
|
+
options.cardId,
|
|
1348
|
+
);
|
|
1349
|
+
cardData.external_links = (externalLinksResult.external_links ?? []).map(
|
|
1350
|
+
(link) => ({
|
|
1351
|
+
id: link.id,
|
|
1352
|
+
url: link.url,
|
|
1353
|
+
title: link.title,
|
|
1354
|
+
}),
|
|
1355
|
+
);
|
|
1356
|
+
} catch (err) {
|
|
1357
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1358
|
+
console.debug(`[generateCardPrompt] getCardExternalLinks failed: ${msg}`);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1199
1361
|
// Try to get column info
|
|
1200
1362
|
let columnData: { name: string } | null = null;
|
|
1201
1363
|
const projectIdForBoard = options.projectId || cardData.project_id;
|
|
@@ -1309,6 +1471,18 @@ interface CardPromptData {
|
|
|
1309
1471
|
direction: "outgoing" | "incoming";
|
|
1310
1472
|
}>;
|
|
1311
1473
|
assignee?: { full_name?: string; email: string } | null;
|
|
1474
|
+
attachments?: Array<{
|
|
1475
|
+
id: string;
|
|
1476
|
+
file_name: string;
|
|
1477
|
+
file_type: string;
|
|
1478
|
+
file_size: number;
|
|
1479
|
+
signed_url: string | null;
|
|
1480
|
+
}>;
|
|
1481
|
+
external_links?: Array<{
|
|
1482
|
+
id: string;
|
|
1483
|
+
url: string;
|
|
1484
|
+
title?: string | null;
|
|
1485
|
+
}>;
|
|
1312
1486
|
column_id?: string;
|
|
1313
1487
|
project_id?: string;
|
|
1314
1488
|
}
|
package/src/auto-session.ts
CHANGED
|
@@ -65,15 +65,19 @@ export function resolveAgentIdentity(info: ClientInfo | null): {
|
|
|
65
65
|
return { agentIdentifier: key, agentName: displayName };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Tools that trigger auto-start of a session.
|
|
70
|
+
*
|
|
71
|
+
* Restricted to tools that signal real work on a card. Board-management ops
|
|
72
|
+
* (move, label add/remove) are excluded — they're routinely used for triage
|
|
73
|
+
* and would create false-positive sessions whose side effect (the auto-added
|
|
74
|
+
* `agent` label on the card) confuses both UI and humans.
|
|
75
|
+
*/
|
|
69
76
|
export const AUTO_START_TRIGGERS = new Set([
|
|
70
77
|
"harmony_generate_prompt",
|
|
71
78
|
"harmony_update_card",
|
|
72
|
-
"harmony_move_card",
|
|
73
79
|
"harmony_create_subtask",
|
|
74
80
|
"harmony_toggle_subtask",
|
|
75
|
-
"harmony_add_label_to_card",
|
|
76
|
-
"harmony_remove_label_from_card",
|
|
77
81
|
]);
|
|
78
82
|
|
|
79
83
|
export const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
package/src/prompt-builder.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface PromptContextOptions {
|
|
|
44
44
|
includePriority: boolean;
|
|
45
45
|
includeLinks: boolean;
|
|
46
46
|
includeColumn: boolean;
|
|
47
|
+
includeAttachments: boolean;
|
|
48
|
+
includeExternalLinks: boolean;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export interface RoleFraming {
|
|
@@ -307,6 +309,27 @@ function formatLabels(labels: Array<{ name: string }>): string {
|
|
|
307
309
|
return `\n**Labels:** ${labels.map((l) => l.name).join(", ")}`;
|
|
308
310
|
}
|
|
309
311
|
|
|
312
|
+
function formatAttachments(attachments: CardAttachmentRef[]): string {
|
|
313
|
+
if (!attachments || attachments.length === 0) return "";
|
|
314
|
+
const lines = attachments.map((att) => {
|
|
315
|
+
const isImage = att.file_type.startsWith("image/");
|
|
316
|
+
const sizeKb = Math.max(1, Math.round(att.file_size / 1024));
|
|
317
|
+
const url = att.signed_url ?? "(signed URL unavailable)";
|
|
318
|
+
const kind = isImage ? "image" : att.file_type;
|
|
319
|
+
return ` - ${att.file_name} (${kind}, ${sizeKb} KB) — ${url}`;
|
|
320
|
+
});
|
|
321
|
+
return `\n## Attachments\n*Signed URLs expire in ~1 hour. Re-fetch via \`harmony_get_card_attachments\` if expired.*\n${lines.join("\n")}`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function formatExternalLinks(refs: CardExternalLinkRef[]): string {
|
|
325
|
+
if (!refs || refs.length === 0) return "";
|
|
326
|
+
const lines = refs.map((ref) => {
|
|
327
|
+
const label = ref.title?.trim() || ref.url;
|
|
328
|
+
return ` - ${label} — ${ref.url}`;
|
|
329
|
+
});
|
|
330
|
+
return `\n## External References\n${lines.join("\n")}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
310
333
|
/**
|
|
311
334
|
* Format linked cards for prompt
|
|
312
335
|
*/
|
|
@@ -325,6 +348,20 @@ function formatLinkedCards(
|
|
|
325
348
|
return `\n## Related Cards\n${lines.join("\n")}`;
|
|
326
349
|
}
|
|
327
350
|
|
|
351
|
+
export interface CardAttachmentRef {
|
|
352
|
+
id: string;
|
|
353
|
+
file_name: string;
|
|
354
|
+
file_type: string;
|
|
355
|
+
file_size: number;
|
|
356
|
+
signed_url: string | null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export interface CardExternalLinkRef {
|
|
360
|
+
id: string;
|
|
361
|
+
url: string;
|
|
362
|
+
title?: string | null;
|
|
363
|
+
}
|
|
364
|
+
|
|
328
365
|
export interface CardData {
|
|
329
366
|
id: string;
|
|
330
367
|
short_id: number;
|
|
@@ -341,6 +378,8 @@ export interface CardData {
|
|
|
341
378
|
direction: "outgoing" | "incoming";
|
|
342
379
|
}>;
|
|
343
380
|
assignee?: { full_name?: string; email: string } | null;
|
|
381
|
+
attachments?: CardAttachmentRef[];
|
|
382
|
+
external_links?: CardExternalLinkRef[];
|
|
344
383
|
}
|
|
345
384
|
|
|
346
385
|
export interface ColumnData {
|
|
@@ -397,12 +436,16 @@ export function generatePrompt(
|
|
|
397
436
|
includePriority: true,
|
|
398
437
|
includeLinks: true,
|
|
399
438
|
includeColumn: true,
|
|
439
|
+
includeAttachments: true,
|
|
440
|
+
includeExternalLinks: true,
|
|
400
441
|
...options.contextOptions,
|
|
401
442
|
};
|
|
402
443
|
|
|
403
444
|
const labels = card.labels || [];
|
|
404
445
|
const subtasks = card.subtasks || [];
|
|
405
446
|
const links = card.links || [];
|
|
447
|
+
const attachments = card.attachments || [];
|
|
448
|
+
const externalLinks = card.external_links || [];
|
|
406
449
|
|
|
407
450
|
const category = inferCategoryFromLabels(labels);
|
|
408
451
|
const roleFraming = getRoleFraming(category);
|
|
@@ -456,6 +499,16 @@ export function generatePrompt(
|
|
|
456
499
|
sections.push(formatLinkedCards(links));
|
|
457
500
|
}
|
|
458
501
|
|
|
502
|
+
// Attachments (signed URLs) — fetchable image/file references
|
|
503
|
+
if (contextOpts.includeAttachments && attachments.length > 0) {
|
|
504
|
+
sections.push(formatAttachments(attachments));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// External URL references (docs, gists, dashboards)
|
|
508
|
+
if (contextOpts.includeExternalLinks && externalLinks.length > 0) {
|
|
509
|
+
sections.push(formatExternalLinks(externalLinks));
|
|
510
|
+
}
|
|
511
|
+
|
|
459
512
|
// Focus areas
|
|
460
513
|
sections.push(`\n## Focus Areas`);
|
|
461
514
|
roleFraming.focus.forEach((f) => {
|
package/src/server.ts
CHANGED
|
@@ -348,6 +348,28 @@ export const TOOLS = {
|
|
|
348
348
|
required: ["cardId"],
|
|
349
349
|
},
|
|
350
350
|
},
|
|
351
|
+
harmony_archive_project: {
|
|
352
|
+
description:
|
|
353
|
+
"Archive a project. Hides the project (and all its columns and cards) from the active workspace. Can be restored later with harmony_unarchive_project.",
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties: {
|
|
357
|
+
projectId: { type: "string", description: "Project ID to archive" },
|
|
358
|
+
},
|
|
359
|
+
required: ["projectId"],
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
harmony_unarchive_project: {
|
|
363
|
+
description:
|
|
364
|
+
"Restore an archived project so it (and its content) becomes visible again.",
|
|
365
|
+
inputSchema: {
|
|
366
|
+
type: "object",
|
|
367
|
+
properties: {
|
|
368
|
+
projectId: { type: "string", description: "Project ID to unarchive" },
|
|
369
|
+
},
|
|
370
|
+
required: ["projectId"],
|
|
371
|
+
},
|
|
372
|
+
},
|
|
351
373
|
harmony_delete_card: {
|
|
352
374
|
description: "Delete a card",
|
|
353
375
|
inputSchema: {
|
|
@@ -546,6 +568,34 @@ export const TOOLS = {
|
|
|
546
568
|
required: ["cardId"],
|
|
547
569
|
},
|
|
548
570
|
},
|
|
571
|
+
harmony_get_card_attachments: {
|
|
572
|
+
description:
|
|
573
|
+
"Get all file attachments for a card with short-lived signed URLs. URLs expire after `signed_url_expires_in` seconds (default 3600). Re-fetch to refresh.",
|
|
574
|
+
inputSchema: {
|
|
575
|
+
type: "object",
|
|
576
|
+
properties: {
|
|
577
|
+
cardId: {
|
|
578
|
+
type: "string",
|
|
579
|
+
description: "Card UUID",
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
required: ["cardId"],
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
harmony_get_card_external_links: {
|
|
586
|
+
description:
|
|
587
|
+
"Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
|
|
588
|
+
inputSchema: {
|
|
589
|
+
type: "object",
|
|
590
|
+
properties: {
|
|
591
|
+
cardId: {
|
|
592
|
+
type: "string",
|
|
593
|
+
description: "Card UUID",
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
required: ["cardId"],
|
|
597
|
+
},
|
|
598
|
+
},
|
|
549
599
|
|
|
550
600
|
// Subtask operations
|
|
551
601
|
harmony_create_subtask: {
|
|
@@ -1908,6 +1958,18 @@ async function handleToolCall(
|
|
|
1908
1958
|
return { success: true, ...result };
|
|
1909
1959
|
}
|
|
1910
1960
|
|
|
1961
|
+
case "harmony_archive_project": {
|
|
1962
|
+
const projectId = z.string().uuid().parse(args.projectId);
|
|
1963
|
+
const result = await client.archiveProject(projectId);
|
|
1964
|
+
return { success: true, ...result };
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
case "harmony_unarchive_project": {
|
|
1968
|
+
const projectId = z.string().uuid().parse(args.projectId);
|
|
1969
|
+
const result = await client.unarchiveProject(projectId);
|
|
1970
|
+
return { success: true, ...result };
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1911
1973
|
case "harmony_delete_card": {
|
|
1912
1974
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
1913
1975
|
await client.deleteCard(cardId);
|
|
@@ -2074,6 +2136,18 @@ async function handleToolCall(
|
|
|
2074
2136
|
return result;
|
|
2075
2137
|
}
|
|
2076
2138
|
|
|
2139
|
+
case "harmony_get_card_attachments": {
|
|
2140
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
2141
|
+
const result = await client.getCardAttachments(cardId);
|
|
2142
|
+
return result;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
case "harmony_get_card_external_links": {
|
|
2146
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
2147
|
+
const result = await client.getCardExternalLinks(cardId);
|
|
2148
|
+
return result;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2077
2151
|
// Subtask operations
|
|
2078
2152
|
case "harmony_create_subtask": {
|
|
2079
2153
|
const cardId = z.string().uuid().parse(args.cardId);
|