@gethmy/mcp 2.4.1 → 2.4.3
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 +76 -14
- package/dist/index.js +76 -14
- package/dist/lib/api-client.js +19 -2
- package/package.json +1 -1
- package/src/api-client.ts +29 -2
- package/src/memory-audit.ts +25 -9
- 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
|
}
|
|
@@ -28074,7 +28091,8 @@ var BOILERPLATE_PATTERNS = [
|
|
|
28074
28091
|
/^placeholder/i,
|
|
28075
28092
|
/^\.\.\.$/,
|
|
28076
28093
|
/^untitled/i,
|
|
28077
|
-
/^(note|memo|draft)\s*\d*$/i
|
|
28094
|
+
/^(note|memo|draft)\s*\d*$/i,
|
|
28095
|
+
/^task transition:/i
|
|
28078
28096
|
];
|
|
28079
28097
|
function isBoilerplate(title, content) {
|
|
28080
28098
|
const t = title.trim();
|
|
@@ -28122,21 +28140,26 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraf
|
|
|
28122
28140
|
reasons.push("no relations");
|
|
28123
28141
|
let content = 0;
|
|
28124
28142
|
const contentLen = entity.content?.length || 0;
|
|
28125
|
-
|
|
28126
|
-
|
|
28127
|
-
const titleOk = entity.title.trim().length >= 4 && !/^(untitled|draft|note)\b/i.test(entity.title.trim());
|
|
28128
|
-
if (titleOk)
|
|
28129
|
-
content += 4;
|
|
28130
|
-
if (!isBoilerplate(entity.title, entity.content))
|
|
28131
|
-
content += 3;
|
|
28132
|
-
if (contentLen < 80)
|
|
28133
|
-
reasons.push(`thin content (${contentLen} chars)`);
|
|
28134
|
-
if (isBoilerplate(entity.title, entity.content))
|
|
28143
|
+
const boilerplate = isBoilerplate(entity.title, entity.content);
|
|
28144
|
+
if (boilerplate) {
|
|
28135
28145
|
reasons.push("boilerplate title/content");
|
|
28146
|
+
} else {
|
|
28147
|
+
if (contentLen >= 80)
|
|
28148
|
+
content += 8;
|
|
28149
|
+
const titleOk = entity.title.trim().length >= 4 && !/^(untitled|draft|note)\b/i.test(entity.title.trim());
|
|
28150
|
+
if (titleOk)
|
|
28151
|
+
content += 4;
|
|
28152
|
+
content += 3;
|
|
28153
|
+
if (contentLen < 80)
|
|
28154
|
+
reasons.push(`thin content (${contentLen} chars)`);
|
|
28155
|
+
}
|
|
28136
28156
|
let tierAgeFit = 15;
|
|
28137
28157
|
if (entity.memory_tier === "draft" && ageDays > 60 && !entity.promoted_from_id) {
|
|
28138
28158
|
tierAgeFit = 0;
|
|
28139
28159
|
reasons.push("stuck draft >60d never promoted");
|
|
28160
|
+
} else if (entity.memory_tier === "draft" && (entity.access_count || 0) === 0 && ageDays > 2) {
|
|
28161
|
+
tierAgeFit = 5;
|
|
28162
|
+
reasons.push("draft >2d with zero access");
|
|
28140
28163
|
}
|
|
28141
28164
|
if (entity.promoted_from_id) {
|
|
28142
28165
|
tierAgeFit = Math.min(15, tierAgeFit + 5);
|
|
@@ -28988,6 +29011,30 @@ async function onboardNewUser(params) {
|
|
|
28988
29011
|
|
|
28989
29012
|
// src/server.ts
|
|
28990
29013
|
var memorySessions = new Map;
|
|
29014
|
+
function parseLabelList(raw) {
|
|
29015
|
+
if (raw === undefined || raw === null)
|
|
29016
|
+
return;
|
|
29017
|
+
if (Array.isArray(raw)) {
|
|
29018
|
+
const arr = raw.filter((v) => typeof v === "string").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
29019
|
+
return arr.length ? arr : undefined;
|
|
29020
|
+
}
|
|
29021
|
+
if (typeof raw === "string") {
|
|
29022
|
+
const trimmed = raw.trim();
|
|
29023
|
+
if (!trimmed)
|
|
29024
|
+
return;
|
|
29025
|
+
if (trimmed.startsWith("[")) {
|
|
29026
|
+
try {
|
|
29027
|
+
const parsed = JSON.parse(trimmed);
|
|
29028
|
+
if (Array.isArray(parsed) && parsed.every((x) => typeof x === "string")) {
|
|
29029
|
+
const arr = parsed.map((v) => v.trim()).filter((v) => v.length > 0);
|
|
29030
|
+
return arr.length ? arr : undefined;
|
|
29031
|
+
}
|
|
29032
|
+
} catch {}
|
|
29033
|
+
}
|
|
29034
|
+
return [trimmed];
|
|
29035
|
+
}
|
|
29036
|
+
return;
|
|
29037
|
+
}
|
|
28991
29038
|
function initMemorySession(cardId, agentIdentifier, agentName) {
|
|
28992
29039
|
memorySessions.set(cardId, {
|
|
28993
29040
|
cardId,
|
|
@@ -29265,6 +29312,16 @@ var TOOLS = {
|
|
|
29265
29312
|
required: ["name", "color"]
|
|
29266
29313
|
}
|
|
29267
29314
|
},
|
|
29315
|
+
harmony_delete_label: {
|
|
29316
|
+
description: "Delete a label from a project. Also removes it from any cards that reference it.",
|
|
29317
|
+
inputSchema: {
|
|
29318
|
+
type: "object",
|
|
29319
|
+
properties: {
|
|
29320
|
+
labelId: { type: "string", description: "Label ID to delete" }
|
|
29321
|
+
},
|
|
29322
|
+
required: ["labelId"]
|
|
29323
|
+
}
|
|
29324
|
+
},
|
|
29268
29325
|
harmony_add_label_to_card: {
|
|
29269
29326
|
description: "Add a label to a card. Provide labelId directly, or labelName to look up (or auto-create) the label by name.",
|
|
29270
29327
|
inputSchema: {
|
|
@@ -30918,6 +30975,11 @@ async function handleToolCall(name, args, deps) {
|
|
|
30918
30975
|
const result = await client3.createLabel(projectId, { name: name2, color });
|
|
30919
30976
|
return { success: true, ...result };
|
|
30920
30977
|
}
|
|
30978
|
+
case "harmony_delete_label": {
|
|
30979
|
+
const labelId = exports_external.string().uuid().parse(args.labelId);
|
|
30980
|
+
const result = await client3.deleteLabel(labelId);
|
|
30981
|
+
return { success: true, ...result };
|
|
30982
|
+
}
|
|
30921
30983
|
case "harmony_add_label_to_card": {
|
|
30922
30984
|
const cardId = exports_external.string().uuid().parse(args.cardId);
|
|
30923
30985
|
let labelId = args.labelId ? exports_external.string().uuid().parse(args.labelId) : undefined;
|
|
@@ -31053,7 +31115,7 @@ async function handleToolCall(name, args, deps) {
|
|
|
31053
31115
|
const agentIdentifier = exports_external.string().min(1).max(100).parse(args.agentIdentifier);
|
|
31054
31116
|
const agentName = exports_external.string().min(1).max(100).parse(args.agentName);
|
|
31055
31117
|
const moveToColumn = args.moveToColumn;
|
|
31056
|
-
const addLabels = args.addLabels;
|
|
31118
|
+
const addLabels = parseLabelList(args.addLabels);
|
|
31057
31119
|
let movedTo = null;
|
|
31058
31120
|
const labelsAdded = [];
|
|
31059
31121
|
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
|
}
|
|
@@ -25834,7 +25851,8 @@ var BOILERPLATE_PATTERNS = [
|
|
|
25834
25851
|
/^placeholder/i,
|
|
25835
25852
|
/^\.\.\.$/,
|
|
25836
25853
|
/^untitled/i,
|
|
25837
|
-
/^(note|memo|draft)\s*\d*$/i
|
|
25854
|
+
/^(note|memo|draft)\s*\d*$/i,
|
|
25855
|
+
/^task transition:/i
|
|
25838
25856
|
];
|
|
25839
25857
|
function isBoilerplate(title, content) {
|
|
25840
25858
|
const t = title.trim();
|
|
@@ -25882,21 +25900,26 @@ function scoreEntity(entity, relationCount, archiveBelow, deleteBelow, staleDraf
|
|
|
25882
25900
|
reasons.push("no relations");
|
|
25883
25901
|
let content = 0;
|
|
25884
25902
|
const contentLen = entity.content?.length || 0;
|
|
25885
|
-
|
|
25886
|
-
|
|
25887
|
-
const titleOk = entity.title.trim().length >= 4 && !/^(untitled|draft|note)\b/i.test(entity.title.trim());
|
|
25888
|
-
if (titleOk)
|
|
25889
|
-
content += 4;
|
|
25890
|
-
if (!isBoilerplate(entity.title, entity.content))
|
|
25891
|
-
content += 3;
|
|
25892
|
-
if (contentLen < 80)
|
|
25893
|
-
reasons.push(`thin content (${contentLen} chars)`);
|
|
25894
|
-
if (isBoilerplate(entity.title, entity.content))
|
|
25903
|
+
const boilerplate = isBoilerplate(entity.title, entity.content);
|
|
25904
|
+
if (boilerplate) {
|
|
25895
25905
|
reasons.push("boilerplate title/content");
|
|
25906
|
+
} else {
|
|
25907
|
+
if (contentLen >= 80)
|
|
25908
|
+
content += 8;
|
|
25909
|
+
const titleOk = entity.title.trim().length >= 4 && !/^(untitled|draft|note)\b/i.test(entity.title.trim());
|
|
25910
|
+
if (titleOk)
|
|
25911
|
+
content += 4;
|
|
25912
|
+
content += 3;
|
|
25913
|
+
if (contentLen < 80)
|
|
25914
|
+
reasons.push(`thin content (${contentLen} chars)`);
|
|
25915
|
+
}
|
|
25896
25916
|
let tierAgeFit = 15;
|
|
25897
25917
|
if (entity.memory_tier === "draft" && ageDays > 60 && !entity.promoted_from_id) {
|
|
25898
25918
|
tierAgeFit = 0;
|
|
25899
25919
|
reasons.push("stuck draft >60d never promoted");
|
|
25920
|
+
} else if (entity.memory_tier === "draft" && (entity.access_count || 0) === 0 && ageDays > 2) {
|
|
25921
|
+
tierAgeFit = 5;
|
|
25922
|
+
reasons.push("draft >2d with zero access");
|
|
25900
25923
|
}
|
|
25901
25924
|
if (entity.promoted_from_id) {
|
|
25902
25925
|
tierAgeFit = Math.min(15, tierAgeFit + 5);
|
|
@@ -26748,6 +26771,30 @@ async function onboardNewUser(params) {
|
|
|
26748
26771
|
|
|
26749
26772
|
// src/server.ts
|
|
26750
26773
|
var memorySessions = new Map;
|
|
26774
|
+
function parseLabelList(raw) {
|
|
26775
|
+
if (raw === undefined || raw === null)
|
|
26776
|
+
return;
|
|
26777
|
+
if (Array.isArray(raw)) {
|
|
26778
|
+
const arr = raw.filter((v) => typeof v === "string").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
26779
|
+
return arr.length ? arr : undefined;
|
|
26780
|
+
}
|
|
26781
|
+
if (typeof raw === "string") {
|
|
26782
|
+
const trimmed = raw.trim();
|
|
26783
|
+
if (!trimmed)
|
|
26784
|
+
return;
|
|
26785
|
+
if (trimmed.startsWith("[")) {
|
|
26786
|
+
try {
|
|
26787
|
+
const parsed = JSON.parse(trimmed);
|
|
26788
|
+
if (Array.isArray(parsed) && parsed.every((x) => typeof x === "string")) {
|
|
26789
|
+
const arr = parsed.map((v) => v.trim()).filter((v) => v.length > 0);
|
|
26790
|
+
return arr.length ? arr : undefined;
|
|
26791
|
+
}
|
|
26792
|
+
} catch {}
|
|
26793
|
+
}
|
|
26794
|
+
return [trimmed];
|
|
26795
|
+
}
|
|
26796
|
+
return;
|
|
26797
|
+
}
|
|
26751
26798
|
function initMemorySession(cardId, agentIdentifier, agentName) {
|
|
26752
26799
|
memorySessions.set(cardId, {
|
|
26753
26800
|
cardId,
|
|
@@ -27025,6 +27072,16 @@ var TOOLS = {
|
|
|
27025
27072
|
required: ["name", "color"]
|
|
27026
27073
|
}
|
|
27027
27074
|
},
|
|
27075
|
+
harmony_delete_label: {
|
|
27076
|
+
description: "Delete a label from a project. Also removes it from any cards that reference it.",
|
|
27077
|
+
inputSchema: {
|
|
27078
|
+
type: "object",
|
|
27079
|
+
properties: {
|
|
27080
|
+
labelId: { type: "string", description: "Label ID to delete" }
|
|
27081
|
+
},
|
|
27082
|
+
required: ["labelId"]
|
|
27083
|
+
}
|
|
27084
|
+
},
|
|
27028
27085
|
harmony_add_label_to_card: {
|
|
27029
27086
|
description: "Add a label to a card. Provide labelId directly, or labelName to look up (or auto-create) the label by name.",
|
|
27030
27087
|
inputSchema: {
|
|
@@ -28678,6 +28735,11 @@ async function handleToolCall(name, args, deps) {
|
|
|
28678
28735
|
const result = await client3.createLabel(projectId, { name: name2, color });
|
|
28679
28736
|
return { success: true, ...result };
|
|
28680
28737
|
}
|
|
28738
|
+
case "harmony_delete_label": {
|
|
28739
|
+
const labelId = exports_external.string().uuid().parse(args.labelId);
|
|
28740
|
+
const result = await client3.deleteLabel(labelId);
|
|
28741
|
+
return { success: true, ...result };
|
|
28742
|
+
}
|
|
28681
28743
|
case "harmony_add_label_to_card": {
|
|
28682
28744
|
const cardId = exports_external.string().uuid().parse(args.cardId);
|
|
28683
28745
|
let labelId = args.labelId ? exports_external.string().uuid().parse(args.labelId) : undefined;
|
|
@@ -28813,7 +28875,7 @@ async function handleToolCall(name, args, deps) {
|
|
|
28813
28875
|
const agentIdentifier = exports_external.string().min(1).max(100).parse(args.agentIdentifier);
|
|
28814
28876
|
const agentName = exports_external.string().min(1).max(100).parse(args.agentName);
|
|
28815
28877
|
const moveToColumn = args.moveToColumn;
|
|
28816
|
-
const addLabels = args.addLabels;
|
|
28878
|
+
const addLabels = parseLabelList(args.addLabels);
|
|
28817
28879
|
let movedTo = null;
|
|
28818
28880
|
const labelsAdded = [];
|
|
28819
28881
|
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
|
}
|
package/package.json
CHANGED
package/src/api-client.ts
CHANGED
|
@@ -176,10 +176,27 @@ export class HarmonyApiClient {
|
|
|
176
176
|
body: options?.rawBody ?? (body ? JSON.stringify(body) : undefined),
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
-
const
|
|
179
|
+
const text = await response.text();
|
|
180
|
+
const responseContentType = response.headers.get("content-type") || "";
|
|
181
|
+
const looksLikeJson = responseContentType.includes("application/json");
|
|
182
|
+
|
|
183
|
+
let data: ApiResponse | null = null;
|
|
184
|
+
let parseError: Error | null = null;
|
|
185
|
+
if (text) {
|
|
186
|
+
try {
|
|
187
|
+
data = JSON.parse(text);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
parseError = err instanceof Error ? err : new Error(String(err));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
180
192
|
|
|
181
193
|
if (!response.ok) {
|
|
182
|
-
const errorMsg =
|
|
194
|
+
const errorMsg =
|
|
195
|
+
data?.error ||
|
|
196
|
+
(looksLikeJson
|
|
197
|
+
? null
|
|
198
|
+
: `API error: ${response.status} (non-JSON response)`) ||
|
|
199
|
+
`API error: ${response.status}`;
|
|
183
200
|
if (!isRetryableError(null, response.status)) {
|
|
184
201
|
throw new Error(errorMsg);
|
|
185
202
|
}
|
|
@@ -191,6 +208,12 @@ export class HarmonyApiClient {
|
|
|
191
208
|
throw lastError;
|
|
192
209
|
}
|
|
193
210
|
|
|
211
|
+
if (parseError) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`API returned ${response.status} with invalid JSON body: ${parseError.message}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
194
217
|
return data as T;
|
|
195
218
|
} catch (error) {
|
|
196
219
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -455,6 +478,10 @@ export class HarmonyApiClient {
|
|
|
455
478
|
return this.request("POST", "/labels", { projectId, ...data });
|
|
456
479
|
}
|
|
457
480
|
|
|
481
|
+
async deleteLabel(labelId: string): Promise<{ success: boolean }> {
|
|
482
|
+
return this.request("DELETE", `/labels/${labelId}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
458
485
|
// ============ SUBTASK OPERATIONS ============
|
|
459
486
|
|
|
460
487
|
async createSubtask(
|
package/src/memory-audit.ts
CHANGED
|
@@ -122,6 +122,9 @@ const BOILERPLATE_PATTERNS = [
|
|
|
122
122
|
/^\.\.\.$/,
|
|
123
123
|
/^untitled/i,
|
|
124
124
|
/^(note|memo|draft)\s*\d*$/i,
|
|
125
|
+
// Auto-captured task-transition snapshots from a retired active-learning rule.
|
|
126
|
+
// No user intent, no access pattern — treat as boilerplate so scoring archives them.
|
|
127
|
+
/^task transition:/i,
|
|
125
128
|
];
|
|
126
129
|
|
|
127
130
|
function isBoilerplate(title: string, content: string): boolean {
|
|
@@ -177,18 +180,22 @@ function scoreEntity(
|
|
|
177
180
|
if (!hasTags) reasons.push("no tags");
|
|
178
181
|
if (!hasRelations) reasons.push("no relations");
|
|
179
182
|
|
|
180
|
-
// Content quality (15)
|
|
183
|
+
// Content quality (15) — boilerplate hard-zeroes the whole band. Auto-captured
|
|
184
|
+
// noise should never inherit the length/title bonuses it structurally earns.
|
|
181
185
|
let content = 0;
|
|
182
186
|
const contentLen = entity.content?.length || 0;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
entity.title.trim().length >= 4 &&
|
|
186
|
-
!/^(untitled|draft|note)\b/i.test(entity.title.trim());
|
|
187
|
-
if (titleOk) content += 4;
|
|
188
|
-
if (!isBoilerplate(entity.title, entity.content)) content += 3;
|
|
189
|
-
if (contentLen < 80) reasons.push(`thin content (${contentLen} chars)`);
|
|
190
|
-
if (isBoilerplate(entity.title, entity.content))
|
|
187
|
+
const boilerplate = isBoilerplate(entity.title, entity.content);
|
|
188
|
+
if (boilerplate) {
|
|
191
189
|
reasons.push("boilerplate title/content");
|
|
190
|
+
} else {
|
|
191
|
+
if (contentLen >= 80) content += 8;
|
|
192
|
+
const titleOk =
|
|
193
|
+
entity.title.trim().length >= 4 &&
|
|
194
|
+
!/^(untitled|draft|note)\b/i.test(entity.title.trim());
|
|
195
|
+
if (titleOk) content += 4;
|
|
196
|
+
content += 3;
|
|
197
|
+
if (contentLen < 80) reasons.push(`thin content (${contentLen} chars)`);
|
|
198
|
+
}
|
|
192
199
|
|
|
193
200
|
// Tier-age fit (15)
|
|
194
201
|
let tierAgeFit = 15;
|
|
@@ -199,6 +206,15 @@ function scoreEntity(
|
|
|
199
206
|
) {
|
|
200
207
|
tierAgeFit = 0;
|
|
201
208
|
reasons.push("stuck draft >60d never promoted");
|
|
209
|
+
} else if (
|
|
210
|
+
entity.memory_tier === "draft" &&
|
|
211
|
+
(entity.access_count || 0) === 0 &&
|
|
212
|
+
ageDays > 2
|
|
213
|
+
) {
|
|
214
|
+
// Young drafts get a 2-day grace window. After that, zero access means
|
|
215
|
+
// zero signal — strip the tier-age bonus so useless auto-captures fall to archive.
|
|
216
|
+
tierAgeFit = 5;
|
|
217
|
+
reasons.push("draft >2d with zero access");
|
|
202
218
|
}
|
|
203
219
|
if (entity.promoted_from_id) {
|
|
204
220
|
tierAgeFit = Math.min(15, tierAgeFit + 5);
|
package/src/server.ts
CHANGED
|
@@ -100,6 +100,45 @@ interface MemorySessionState {
|
|
|
100
100
|
|
|
101
101
|
const memorySessions = new Map<string, MemorySessionState>();
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Normalize a label-list argument into `string[]`.
|
|
105
|
+
*
|
|
106
|
+
* Some MCP callers pass `addLabels` as a JSON-encoded string (e.g. `'["agent"]'`)
|
|
107
|
+
* instead of a real array. Iterating such a string with `for..of` would yield
|
|
108
|
+
* individual characters and create one bogus single-char label per character.
|
|
109
|
+
* This helper coerces the value into a well-formed array and drops empty entries.
|
|
110
|
+
*/
|
|
111
|
+
function parseLabelList(raw: unknown): string[] | undefined {
|
|
112
|
+
if (raw === undefined || raw === null) return undefined;
|
|
113
|
+
if (Array.isArray(raw)) {
|
|
114
|
+
const arr = raw
|
|
115
|
+
.filter((v): v is string => typeof v === "string")
|
|
116
|
+
.map((v) => v.trim())
|
|
117
|
+
.filter((v) => v.length > 0);
|
|
118
|
+
return arr.length ? arr : undefined;
|
|
119
|
+
}
|
|
120
|
+
if (typeof raw === "string") {
|
|
121
|
+
const trimmed = raw.trim();
|
|
122
|
+
if (!trimmed) return undefined;
|
|
123
|
+
if (trimmed.startsWith("[")) {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(trimmed);
|
|
126
|
+
if (
|
|
127
|
+
Array.isArray(parsed) &&
|
|
128
|
+
parsed.every((x) => typeof x === "string")
|
|
129
|
+
) {
|
|
130
|
+
const arr = parsed.map((v) => v.trim()).filter((v) => v.length > 0);
|
|
131
|
+
return arr.length ? arr : undefined;
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// fall through to single-label fallback
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [trimmed];
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
103
142
|
function initMemorySession(
|
|
104
143
|
cardId: string,
|
|
105
144
|
agentIdentifier: string,
|
|
@@ -431,6 +470,17 @@ const TOOLS = {
|
|
|
431
470
|
required: ["name", "color"],
|
|
432
471
|
},
|
|
433
472
|
},
|
|
473
|
+
harmony_delete_label: {
|
|
474
|
+
description:
|
|
475
|
+
"Delete a label from a project. Also removes it from any cards that reference it.",
|
|
476
|
+
inputSchema: {
|
|
477
|
+
type: "object",
|
|
478
|
+
properties: {
|
|
479
|
+
labelId: { type: "string", description: "Label ID to delete" },
|
|
480
|
+
},
|
|
481
|
+
required: ["labelId"],
|
|
482
|
+
},
|
|
483
|
+
},
|
|
434
484
|
harmony_add_label_to_card: {
|
|
435
485
|
description:
|
|
436
486
|
"Add a label to a card. Provide labelId directly, or labelName to look up (or auto-create) the label by name.",
|
|
@@ -2412,6 +2462,12 @@ async function handleToolCall(
|
|
|
2412
2462
|
return { success: true, ...result };
|
|
2413
2463
|
}
|
|
2414
2464
|
|
|
2465
|
+
case "harmony_delete_label": {
|
|
2466
|
+
const labelId = z.string().uuid().parse(args.labelId);
|
|
2467
|
+
const result = await client.deleteLabel(labelId);
|
|
2468
|
+
return { success: true, ...result };
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2415
2471
|
case "harmony_add_label_to_card": {
|
|
2416
2472
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
2417
2473
|
let labelId = args.labelId
|
|
@@ -2603,7 +2659,7 @@ async function handleToolCall(
|
|
|
2603
2659
|
.parse(args.agentIdentifier);
|
|
2604
2660
|
const agentName = z.string().min(1).max(100).parse(args.agentName);
|
|
2605
2661
|
const moveToColumn = args.moveToColumn as string | undefined;
|
|
2606
|
-
const addLabels = args.addLabels
|
|
2662
|
+
const addLabels = parseLabelList(args.addLabels);
|
|
2607
2663
|
|
|
2608
2664
|
let movedTo: string | null = null;
|
|
2609
2665
|
const labelsAdded: string[] = [];
|