@gethmy/mcp 2.4.2 → 2.4.4
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/README.md +5 -2
- package/dist/cli.js +63 -4
- package/dist/index.js +63 -4
- package/dist/lib/api-client.js +19 -2
- package/dist/remote.js +1739 -463
- package/package.json +1 -1
- package/src/__tests__/memory-audit.test.ts +60 -0
- package/src/api-client.ts +35 -2
- package/src/memory-audit.ts +10 -2
- package/src/remote.ts +87 -17
- package/src/server.ts +57 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Enables AI coding agents (Claude Code, OpenAI Codex, Cursor) to interact with yo
|
|
|
5
5
|
|
|
6
6
|
## Features
|
|
7
7
|
|
|
8
|
-
- **
|
|
8
|
+
- **67 MCP Tools** for full board control, knowledge graph, and workflow plans
|
|
9
9
|
- **Knowledge Graph Memory** - persistent memory with entity types, tiers, scopes, and typed relations
|
|
10
10
|
- **Active Learning** - auto-extracts lessons, solutions, and error patterns from completed work sessions
|
|
11
11
|
- **Context Assembly** - token-budget-aware memory injection into AI prompts
|
|
@@ -95,7 +95,7 @@ If you prefer to configure manually (e.g., in Claude.ai's UI):
|
|
|
95
95
|
1. Get an API key from [Harmony](https://gethmy.com/user/keys)
|
|
96
96
|
2. In Claude.ai, add a remote MCP server with URL `https://mcp.gethmy.com/mcp`
|
|
97
97
|
3. Set the Authorization header to `Bearer hmy_your_key_here`
|
|
98
|
-
4. All
|
|
98
|
+
4. All 67 Harmony tools become available in your conversation
|
|
99
99
|
|
|
100
100
|
**Session management** is automatic - sessions have a 1-hour TTL and are created/renewed transparently.
|
|
101
101
|
|
|
@@ -324,6 +324,9 @@ Store and retrieve persistent knowledge across sessions. Memories have types, ti
|
|
|
324
324
|
- `harmony_consolidate_memories` - Cluster similar draft/episode memories and merge into reference entities (dry-run by default)
|
|
325
325
|
- `harmony_backfill_embeddings` - Generate vector embeddings for entities missing them
|
|
326
326
|
- `harmony_backfill_relations` - Retroactively create semantic relations across existing entities
|
|
327
|
+
- `harmony_cleanup_memories` - Bulk cleanup helper: flag stale drafts, dedupe boilerplate, trim low-value entries
|
|
328
|
+
- `harmony_audit_memories` - Surface low-confidence or low-quality memories for human review
|
|
329
|
+
- `harmony_purge_memories` - Hard-delete archived or flagged memories (dry-run by default)
|
|
327
330
|
|
|
328
331
|
### Context Debugging
|
|
329
332
|
|
package/dist/cli.js
CHANGED
|
@@ -27094,9 +27094,20 @@ class HarmonyApiClient {
|
|
|
27094
27094
|
},
|
|
27095
27095
|
body: options?.rawBody ?? (body ? JSON.stringify(body) : undefined)
|
|
27096
27096
|
});
|
|
27097
|
-
const
|
|
27097
|
+
const text = await response.text();
|
|
27098
|
+
const responseContentType = response.headers.get("content-type") || "";
|
|
27099
|
+
const looksLikeJson = responseContentType.includes("application/json");
|
|
27100
|
+
let data = null;
|
|
27101
|
+
let parseError = null;
|
|
27102
|
+
if (text) {
|
|
27103
|
+
try {
|
|
27104
|
+
data = JSON.parse(text);
|
|
27105
|
+
} catch (err) {
|
|
27106
|
+
parseError = err instanceof Error ? err : new Error(String(err));
|
|
27107
|
+
}
|
|
27108
|
+
}
|
|
27098
27109
|
if (!response.ok) {
|
|
27099
|
-
const errorMsg = data
|
|
27110
|
+
const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
|
|
27100
27111
|
if (!isRetryableError(null, response.status)) {
|
|
27101
27112
|
throw new Error(errorMsg);
|
|
27102
27113
|
}
|
|
@@ -27107,6 +27118,9 @@ class HarmonyApiClient {
|
|
|
27107
27118
|
}
|
|
27108
27119
|
throw lastError;
|
|
27109
27120
|
}
|
|
27121
|
+
if (parseError) {
|
|
27122
|
+
throw new Error(`API returned ${response.status} with invalid JSON body: ${parseError.message}`);
|
|
27123
|
+
}
|
|
27110
27124
|
return data;
|
|
27111
27125
|
} catch (error48) {
|
|
27112
27126
|
lastError = error48 instanceof Error ? error48 : new Error(String(error48));
|
|
@@ -27255,6 +27269,9 @@ class HarmonyApiClient {
|
|
|
27255
27269
|
async createLabel(projectId, data) {
|
|
27256
27270
|
return this.request("POST", "/labels", { projectId, ...data });
|
|
27257
27271
|
}
|
|
27272
|
+
async deleteLabel(labelId) {
|
|
27273
|
+
return this.request("DELETE", `/labels/${labelId}`);
|
|
27274
|
+
}
|
|
27258
27275
|
async createSubtask(cardId, title) {
|
|
27259
27276
|
return this.request("POST", "/subtasks", { cardId, title });
|
|
27260
27277
|
}
|
|
@@ -28170,7 +28187,10 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraf
|
|
|
28170
28187
|
legacyReasons.push("no graph presence");
|
|
28171
28188
|
}
|
|
28172
28189
|
let bucket;
|
|
28173
|
-
if (
|
|
28190
|
+
if (boilerplate && deleteBelow > 0) {
|
|
28191
|
+
bucket = "delete";
|
|
28192
|
+
reasons.push("boilerplate override");
|
|
28193
|
+
} else if (score < deleteBelow)
|
|
28174
28194
|
bucket = "delete";
|
|
28175
28195
|
else if (score < archiveBelow)
|
|
28176
28196
|
bucket = "archive";
|
|
@@ -28994,6 +29014,30 @@ async function onboardNewUser(params) {
|
|
|
28994
29014
|
|
|
28995
29015
|
// src/server.ts
|
|
28996
29016
|
var memorySessions = new Map;
|
|
29017
|
+
function parseLabelList(raw) {
|
|
29018
|
+
if (raw === undefined || raw === null)
|
|
29019
|
+
return;
|
|
29020
|
+
if (Array.isArray(raw)) {
|
|
29021
|
+
const arr = raw.filter((v) => typeof v === "string").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
29022
|
+
return arr.length ? arr : undefined;
|
|
29023
|
+
}
|
|
29024
|
+
if (typeof raw === "string") {
|
|
29025
|
+
const trimmed = raw.trim();
|
|
29026
|
+
if (!trimmed)
|
|
29027
|
+
return;
|
|
29028
|
+
if (trimmed.startsWith("[")) {
|
|
29029
|
+
try {
|
|
29030
|
+
const parsed = JSON.parse(trimmed);
|
|
29031
|
+
if (Array.isArray(parsed) && parsed.every((x) => typeof x === "string")) {
|
|
29032
|
+
const arr = parsed.map((v) => v.trim()).filter((v) => v.length > 0);
|
|
29033
|
+
return arr.length ? arr : undefined;
|
|
29034
|
+
}
|
|
29035
|
+
} catch {}
|
|
29036
|
+
}
|
|
29037
|
+
return [trimmed];
|
|
29038
|
+
}
|
|
29039
|
+
return;
|
|
29040
|
+
}
|
|
28997
29041
|
function initMemorySession(cardId, agentIdentifier, agentName) {
|
|
28998
29042
|
memorySessions.set(cardId, {
|
|
28999
29043
|
cardId,
|
|
@@ -29271,6 +29315,16 @@ var TOOLS = {
|
|
|
29271
29315
|
required: ["name", "color"]
|
|
29272
29316
|
}
|
|
29273
29317
|
},
|
|
29318
|
+
harmony_delete_label: {
|
|
29319
|
+
description: "Delete a label from a project. Also removes it from any cards that reference it.",
|
|
29320
|
+
inputSchema: {
|
|
29321
|
+
type: "object",
|
|
29322
|
+
properties: {
|
|
29323
|
+
labelId: { type: "string", description: "Label ID to delete" }
|
|
29324
|
+
},
|
|
29325
|
+
required: ["labelId"]
|
|
29326
|
+
}
|
|
29327
|
+
},
|
|
29274
29328
|
harmony_add_label_to_card: {
|
|
29275
29329
|
description: "Add a label to a card. Provide labelId directly, or labelName to look up (or auto-create) the label by name.",
|
|
29276
29330
|
inputSchema: {
|
|
@@ -30924,6 +30978,11 @@ async function handleToolCall(name, args, deps) {
|
|
|
30924
30978
|
const result = await client3.createLabel(projectId, { name: name2, color });
|
|
30925
30979
|
return { success: true, ...result };
|
|
30926
30980
|
}
|
|
30981
|
+
case "harmony_delete_label": {
|
|
30982
|
+
const labelId = exports_external.string().uuid().parse(args.labelId);
|
|
30983
|
+
const result = await client3.deleteLabel(labelId);
|
|
30984
|
+
return { success: true, ...result };
|
|
30985
|
+
}
|
|
30927
30986
|
case "harmony_add_label_to_card": {
|
|
30928
30987
|
const cardId = exports_external.string().uuid().parse(args.cardId);
|
|
30929
30988
|
let labelId = args.labelId ? exports_external.string().uuid().parse(args.labelId) : undefined;
|
|
@@ -31059,7 +31118,7 @@ async function handleToolCall(name, args, deps) {
|
|
|
31059
31118
|
const agentIdentifier = exports_external.string().min(1).max(100).parse(args.agentIdentifier);
|
|
31060
31119
|
const agentName = exports_external.string().min(1).max(100).parse(args.agentName);
|
|
31061
31120
|
const moveToColumn = args.moveToColumn;
|
|
31062
|
-
const addLabels = args.addLabels;
|
|
31121
|
+
const addLabels = parseLabelList(args.addLabels);
|
|
31063
31122
|
let movedTo = null;
|
|
31064
31123
|
const labelsAdded = [];
|
|
31065
31124
|
if (moveToColumn || addLabels?.length) {
|
package/dist/index.js
CHANGED
|
@@ -24854,9 +24854,20 @@ class HarmonyApiClient {
|
|
|
24854
24854
|
},
|
|
24855
24855
|
body: options?.rawBody ?? (body ? JSON.stringify(body) : undefined)
|
|
24856
24856
|
});
|
|
24857
|
-
const
|
|
24857
|
+
const text = await response.text();
|
|
24858
|
+
const responseContentType = response.headers.get("content-type") || "";
|
|
24859
|
+
const looksLikeJson = responseContentType.includes("application/json");
|
|
24860
|
+
let data = null;
|
|
24861
|
+
let parseError = null;
|
|
24862
|
+
if (text) {
|
|
24863
|
+
try {
|
|
24864
|
+
data = JSON.parse(text);
|
|
24865
|
+
} catch (err) {
|
|
24866
|
+
parseError = err instanceof Error ? err : new Error(String(err));
|
|
24867
|
+
}
|
|
24868
|
+
}
|
|
24858
24869
|
if (!response.ok) {
|
|
24859
|
-
const errorMsg = data
|
|
24870
|
+
const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
|
|
24860
24871
|
if (!isRetryableError(null, response.status)) {
|
|
24861
24872
|
throw new Error(errorMsg);
|
|
24862
24873
|
}
|
|
@@ -24867,6 +24878,9 @@ class HarmonyApiClient {
|
|
|
24867
24878
|
}
|
|
24868
24879
|
throw lastError;
|
|
24869
24880
|
}
|
|
24881
|
+
if (parseError) {
|
|
24882
|
+
throw new Error(`API returned ${response.status} with invalid JSON body: ${parseError.message}`);
|
|
24883
|
+
}
|
|
24870
24884
|
return data;
|
|
24871
24885
|
} catch (error48) {
|
|
24872
24886
|
lastError = error48 instanceof Error ? error48 : new Error(String(error48));
|
|
@@ -25015,6 +25029,9 @@ class HarmonyApiClient {
|
|
|
25015
25029
|
async createLabel(projectId, data) {
|
|
25016
25030
|
return this.request("POST", "/labels", { projectId, ...data });
|
|
25017
25031
|
}
|
|
25032
|
+
async deleteLabel(labelId) {
|
|
25033
|
+
return this.request("DELETE", `/labels/${labelId}`);
|
|
25034
|
+
}
|
|
25018
25035
|
async createSubtask(cardId, title) {
|
|
25019
25036
|
return this.request("POST", "/subtasks", { cardId, title });
|
|
25020
25037
|
}
|
|
@@ -25930,7 +25947,10 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraf
|
|
|
25930
25947
|
legacyReasons.push("no graph presence");
|
|
25931
25948
|
}
|
|
25932
25949
|
let bucket;
|
|
25933
|
-
if (
|
|
25950
|
+
if (boilerplate && deleteBelow > 0) {
|
|
25951
|
+
bucket = "delete";
|
|
25952
|
+
reasons.push("boilerplate override");
|
|
25953
|
+
} else if (score < deleteBelow)
|
|
25934
25954
|
bucket = "delete";
|
|
25935
25955
|
else if (score < archiveBelow)
|
|
25936
25956
|
bucket = "archive";
|
|
@@ -26754,6 +26774,30 @@ async function onboardNewUser(params) {
|
|
|
26754
26774
|
|
|
26755
26775
|
// src/server.ts
|
|
26756
26776
|
var memorySessions = new Map;
|
|
26777
|
+
function parseLabelList(raw) {
|
|
26778
|
+
if (raw === undefined || raw === null)
|
|
26779
|
+
return;
|
|
26780
|
+
if (Array.isArray(raw)) {
|
|
26781
|
+
const arr = raw.filter((v) => typeof v === "string").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
26782
|
+
return arr.length ? arr : undefined;
|
|
26783
|
+
}
|
|
26784
|
+
if (typeof raw === "string") {
|
|
26785
|
+
const trimmed = raw.trim();
|
|
26786
|
+
if (!trimmed)
|
|
26787
|
+
return;
|
|
26788
|
+
if (trimmed.startsWith("[")) {
|
|
26789
|
+
try {
|
|
26790
|
+
const parsed = JSON.parse(trimmed);
|
|
26791
|
+
if (Array.isArray(parsed) && parsed.every((x) => typeof x === "string")) {
|
|
26792
|
+
const arr = parsed.map((v) => v.trim()).filter((v) => v.length > 0);
|
|
26793
|
+
return arr.length ? arr : undefined;
|
|
26794
|
+
}
|
|
26795
|
+
} catch {}
|
|
26796
|
+
}
|
|
26797
|
+
return [trimmed];
|
|
26798
|
+
}
|
|
26799
|
+
return;
|
|
26800
|
+
}
|
|
26757
26801
|
function initMemorySession(cardId, agentIdentifier, agentName) {
|
|
26758
26802
|
memorySessions.set(cardId, {
|
|
26759
26803
|
cardId,
|
|
@@ -27031,6 +27075,16 @@ var TOOLS = {
|
|
|
27031
27075
|
required: ["name", "color"]
|
|
27032
27076
|
}
|
|
27033
27077
|
},
|
|
27078
|
+
harmony_delete_label: {
|
|
27079
|
+
description: "Delete a label from a project. Also removes it from any cards that reference it.",
|
|
27080
|
+
inputSchema: {
|
|
27081
|
+
type: "object",
|
|
27082
|
+
properties: {
|
|
27083
|
+
labelId: { type: "string", description: "Label ID to delete" }
|
|
27084
|
+
},
|
|
27085
|
+
required: ["labelId"]
|
|
27086
|
+
}
|
|
27087
|
+
},
|
|
27034
27088
|
harmony_add_label_to_card: {
|
|
27035
27089
|
description: "Add a label to a card. Provide labelId directly, or labelName to look up (or auto-create) the label by name.",
|
|
27036
27090
|
inputSchema: {
|
|
@@ -28684,6 +28738,11 @@ async function handleToolCall(name, args, deps) {
|
|
|
28684
28738
|
const result = await client3.createLabel(projectId, { name: name2, color });
|
|
28685
28739
|
return { success: true, ...result };
|
|
28686
28740
|
}
|
|
28741
|
+
case "harmony_delete_label": {
|
|
28742
|
+
const labelId = exports_external.string().uuid().parse(args.labelId);
|
|
28743
|
+
const result = await client3.deleteLabel(labelId);
|
|
28744
|
+
return { success: true, ...result };
|
|
28745
|
+
}
|
|
28687
28746
|
case "harmony_add_label_to_card": {
|
|
28688
28747
|
const cardId = exports_external.string().uuid().parse(args.cardId);
|
|
28689
28748
|
let labelId = args.labelId ? exports_external.string().uuid().parse(args.labelId) : undefined;
|
|
@@ -28819,7 +28878,7 @@ async function handleToolCall(name, args, deps) {
|
|
|
28819
28878
|
const agentIdentifier = exports_external.string().min(1).max(100).parse(args.agentIdentifier);
|
|
28820
28879
|
const agentName = exports_external.string().min(1).max(100).parse(args.agentName);
|
|
28821
28880
|
const moveToColumn = args.moveToColumn;
|
|
28822
|
-
const addLabels = args.addLabels;
|
|
28881
|
+
const addLabels = parseLabelList(args.addLabels);
|
|
28823
28882
|
let movedTo = null;
|
|
28824
28883
|
const labelsAdded = [];
|
|
28825
28884
|
if (moveToColumn || addLabels?.length) {
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1612,9 +1612,20 @@ class HarmonyApiClient {
|
|
|
1612
1612
|
},
|
|
1613
1613
|
body: options?.rawBody ?? (body ? JSON.stringify(body) : undefined)
|
|
1614
1614
|
});
|
|
1615
|
-
const
|
|
1615
|
+
const text = await response.text();
|
|
1616
|
+
const responseContentType = response.headers.get("content-type") || "";
|
|
1617
|
+
const looksLikeJson = responseContentType.includes("application/json");
|
|
1618
|
+
let data = null;
|
|
1619
|
+
let parseError = null;
|
|
1620
|
+
if (text) {
|
|
1621
|
+
try {
|
|
1622
|
+
data = JSON.parse(text);
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
parseError = err instanceof Error ? err : new Error(String(err));
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1616
1627
|
if (!response.ok) {
|
|
1617
|
-
const errorMsg = data
|
|
1628
|
+
const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
|
|
1618
1629
|
if (!isRetryableError(null, response.status)) {
|
|
1619
1630
|
throw new Error(errorMsg);
|
|
1620
1631
|
}
|
|
@@ -1625,6 +1636,9 @@ class HarmonyApiClient {
|
|
|
1625
1636
|
}
|
|
1626
1637
|
throw lastError;
|
|
1627
1638
|
}
|
|
1639
|
+
if (parseError) {
|
|
1640
|
+
throw new Error(`API returned ${response.status} with invalid JSON body: ${parseError.message}`);
|
|
1641
|
+
}
|
|
1628
1642
|
return data;
|
|
1629
1643
|
} catch (error) {
|
|
1630
1644
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -1773,6 +1787,9 @@ class HarmonyApiClient {
|
|
|
1773
1787
|
async createLabel(projectId, data) {
|
|
1774
1788
|
return this.request("POST", "/labels", { projectId, ...data });
|
|
1775
1789
|
}
|
|
1790
|
+
async deleteLabel(labelId) {
|
|
1791
|
+
return this.request("DELETE", `/labels/${labelId}`);
|
|
1792
|
+
}
|
|
1776
1793
|
async createSubtask(cardId, title) {
|
|
1777
1794
|
return this.request("POST", "/subtasks", { cardId, title });
|
|
1778
1795
|
}
|