@gethmy/mcp 3.6.0 → 3.8.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/README.md +2 -2
- package/dist/cli.js +300 -2
- package/dist/index.js +300 -2
- package/dist/lib/api-client.js +139 -2
- package/dist/lib/config.js +1 -1
- package/dist/lib/oauth-refresh.js +1 -1
- package/dist/run-hook-cli.js +317 -8
- package/package.json +4 -3
- package/src/api-client.ts +82 -1
- package/src/config.ts +20 -3
- package/src/run-hook.ts +1 -1
- package/src/server.ts +27 -0
- package/src/run-redaction.ts +0 -483
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Claude Code, OpenAI Codex, Cursor, and any MCP client claim cards, report progre
|
|
|
5
5
|
|
|
6
6
|
## Features
|
|
7
7
|
|
|
8
|
-
- **
|
|
8
|
+
- **80 MCP Tools** for full board control, knowledge graph, and workflow plans
|
|
9
9
|
- **Global Skills** — installable in one command, served from the DB-backed [skill hub](../../docs/skills.md) with auto-update and admin-managed versioning
|
|
10
10
|
- **Knowledge Graph Memory** — Phase 1 surface: hybrid retrieval (vector + lexical + RRF), session-scoped working memory, activity feed. See [docs/memory.md](../../docs/memory.md)
|
|
11
11
|
- **GSD Workflow Plans** - plan/execute/verify/done lifecycle with auto card creation
|
|
@@ -89,7 +89,7 @@ If you prefer to configure manually (e.g., in Claude.ai's UI):
|
|
|
89
89
|
1. Get an API key from [Harmony](https://gethmy.com/user/keys)
|
|
90
90
|
2. In Claude.ai, add a remote MCP server with URL `https://mcp.gethmy.com/mcp`
|
|
91
91
|
3. Set the Authorization header to `Bearer hmy_your_key_here`
|
|
92
|
-
4. All
|
|
92
|
+
4. All 80 Harmony tools become available in your conversation
|
|
93
93
|
|
|
94
94
|
**Session management** is automatic - sessions have a 1-hour TTL and are created/renewed transparently.
|
|
95
95
|
|
package/dist/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ function noteLegacyLocalPin(path) {
|
|
|
31
31
|
if (warnedLegacyLocalPin)
|
|
32
32
|
return;
|
|
33
33
|
warnedLegacyLocalPin = true;
|
|
34
|
-
console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `
|
|
34
|
+
console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Run \`harmony-agent doctor --fix\` in this repo to write ` + `${LOCAL_CONFIG_FILENAME}, or rename the file yourself. ` + `The fallback that finds it is temporary.`);
|
|
35
35
|
}
|
|
36
36
|
function noteLocalPinRename(from, to) {
|
|
37
37
|
console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
|
|
@@ -2204,6 +2204,267 @@ var REVIEW_DISALLOWED_TOOLS = [
|
|
|
2204
2204
|
"mcp__harmony__harmony_delete_subtask",
|
|
2205
2205
|
"mcp__harmony__harmony_toggle_subtask"
|
|
2206
2206
|
];
|
|
2207
|
+
// ../harmony-shared/dist/runEventSanitize.js
|
|
2208
|
+
var REPLACEMENT = "�";
|
|
2209
|
+
function sanitizeRunEventString(value) {
|
|
2210
|
+
let out = "";
|
|
2211
|
+
for (let i = 0;i < value.length; i++) {
|
|
2212
|
+
const code = value.charCodeAt(i);
|
|
2213
|
+
if (code === 0)
|
|
2214
|
+
continue;
|
|
2215
|
+
if (code >= 55296 && code <= 56319) {
|
|
2216
|
+
const next = value.charCodeAt(i + 1);
|
|
2217
|
+
if (next >= 56320 && next <= 57343) {
|
|
2218
|
+
out += value[i] + value[i + 1];
|
|
2219
|
+
i++;
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
out += REPLACEMENT;
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
if (code >= 56320 && code <= 57343) {
|
|
2226
|
+
out += REPLACEMENT;
|
|
2227
|
+
continue;
|
|
2228
|
+
}
|
|
2229
|
+
out += value[i];
|
|
2230
|
+
}
|
|
2231
|
+
return out;
|
|
2232
|
+
}
|
|
2233
|
+
function sanitizeRunEventPayload(payload) {
|
|
2234
|
+
return walk(payload, new Map);
|
|
2235
|
+
}
|
|
2236
|
+
function sanitizeRunEventDraft(draft) {
|
|
2237
|
+
return { ...draft, payload: sanitizeRunEventPayload(draft.payload) };
|
|
2238
|
+
}
|
|
2239
|
+
function walk(value, seen) {
|
|
2240
|
+
if (typeof value === "string")
|
|
2241
|
+
return sanitizeRunEventString(value);
|
|
2242
|
+
if (value === null || typeof value !== "object")
|
|
2243
|
+
return value;
|
|
2244
|
+
const already = seen.get(value);
|
|
2245
|
+
if (already !== undefined)
|
|
2246
|
+
return already;
|
|
2247
|
+
if (Array.isArray(value)) {
|
|
2248
|
+
const out2 = [];
|
|
2249
|
+
seen.set(value, out2);
|
|
2250
|
+
for (const entry of value)
|
|
2251
|
+
out2.push(walk(entry, seen));
|
|
2252
|
+
return out2;
|
|
2253
|
+
}
|
|
2254
|
+
const out = {};
|
|
2255
|
+
seen.set(value, out);
|
|
2256
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2257
|
+
out[sanitizeRunEventString(key)] = walk(entry, seen);
|
|
2258
|
+
}
|
|
2259
|
+
return out;
|
|
2260
|
+
}
|
|
2261
|
+
// ../harmony-shared/dist/runRedaction.js
|
|
2262
|
+
var MAX_INPUT_CHARS = 2000;
|
|
2263
|
+
var MAX_OUTPUT_CHARS = 4000;
|
|
2264
|
+
var MAX_INPUT_STRING_CHARS = 600;
|
|
2265
|
+
var REDACTION_MARK = "«redacted»";
|
|
2266
|
+
var SENSITIVE_SEGMENTS = [
|
|
2267
|
+
".ssh",
|
|
2268
|
+
".gnupg",
|
|
2269
|
+
".aws",
|
|
2270
|
+
".codex",
|
|
2271
|
+
".gemini",
|
|
2272
|
+
".docker",
|
|
2273
|
+
".kube",
|
|
2274
|
+
".hmy",
|
|
2275
|
+
".harmony-mcp",
|
|
2276
|
+
".password-store",
|
|
2277
|
+
".claude",
|
|
2278
|
+
"gh",
|
|
2279
|
+
"gcloud",
|
|
2280
|
+
"op",
|
|
2281
|
+
"anthropic"
|
|
2282
|
+
];
|
|
2283
|
+
var CONFIG_SCOPED_SEGMENTS = new Set([
|
|
2284
|
+
"gh",
|
|
2285
|
+
"gcloud",
|
|
2286
|
+
"op",
|
|
2287
|
+
"anthropic"
|
|
2288
|
+
]);
|
|
2289
|
+
var SENSITIVE_BASENAMES = new Set([
|
|
2290
|
+
".netrc",
|
|
2291
|
+
"_netrc",
|
|
2292
|
+
".npmrc",
|
|
2293
|
+
".pgpass",
|
|
2294
|
+
".git-credentials",
|
|
2295
|
+
".htpasswd",
|
|
2296
|
+
".claude.json",
|
|
2297
|
+
"credentials",
|
|
2298
|
+
".credentials",
|
|
2299
|
+
"credentials.json",
|
|
2300
|
+
".credentials.json",
|
|
2301
|
+
"credentials.yml",
|
|
2302
|
+
"credentials.yaml",
|
|
2303
|
+
"auth.json",
|
|
2304
|
+
".auth.json",
|
|
2305
|
+
"secrets",
|
|
2306
|
+
"secrets.json",
|
|
2307
|
+
"secrets.yaml",
|
|
2308
|
+
"secrets.yml",
|
|
2309
|
+
"id_rsa",
|
|
2310
|
+
"id_dsa",
|
|
2311
|
+
"id_ecdsa",
|
|
2312
|
+
"id_ed25519",
|
|
2313
|
+
"known_hosts"
|
|
2314
|
+
]);
|
|
2315
|
+
var SENSITIVE_EXTENSIONS = [
|
|
2316
|
+
".pem",
|
|
2317
|
+
".key",
|
|
2318
|
+
".p12",
|
|
2319
|
+
".pfx",
|
|
2320
|
+
".keystore",
|
|
2321
|
+
".jks",
|
|
2322
|
+
".asc",
|
|
2323
|
+
".gpg"
|
|
2324
|
+
];
|
|
2325
|
+
function isSensitivePath(rawPath) {
|
|
2326
|
+
if (typeof rawPath !== "string" || rawPath.length === 0)
|
|
2327
|
+
return false;
|
|
2328
|
+
const path = rawPath.trim().toLowerCase();
|
|
2329
|
+
const segments = path.split(/[\\/]+/).filter((s) => s.length > 0);
|
|
2330
|
+
if (segments.length === 0)
|
|
2331
|
+
return false;
|
|
2332
|
+
for (let i = 0;i < segments.length; i++) {
|
|
2333
|
+
const segment = segments[i];
|
|
2334
|
+
if (!SENSITIVE_SEGMENTS.includes(segment))
|
|
2335
|
+
continue;
|
|
2336
|
+
if (CONFIG_SCOPED_SEGMENTS.has(segment)) {
|
|
2337
|
+
if (i > 0 && segments[i - 1] === ".config")
|
|
2338
|
+
return true;
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2341
|
+
return true;
|
|
2342
|
+
}
|
|
2343
|
+
const basename = segments[segments.length - 1];
|
|
2344
|
+
if (SENSITIVE_BASENAMES.has(basename))
|
|
2345
|
+
return true;
|
|
2346
|
+
if (basename === ".env" || basename.startsWith(".env."))
|
|
2347
|
+
return true;
|
|
2348
|
+
if (basename.endsWith(".env"))
|
|
2349
|
+
return true;
|
|
2350
|
+
if (SENSITIVE_EXTENSIONS.some((ext) => basename.endsWith(ext)))
|
|
2351
|
+
return true;
|
|
2352
|
+
if (/service[-_]?account.*\.json$/.test(basename))
|
|
2353
|
+
return true;
|
|
2354
|
+
return false;
|
|
2355
|
+
}
|
|
2356
|
+
function sensitivePathsIn(input, depth = 0) {
|
|
2357
|
+
if (depth > 6)
|
|
2358
|
+
return [];
|
|
2359
|
+
if (typeof input === "string") {
|
|
2360
|
+
return isSensitivePath(input) ? [input] : [];
|
|
2361
|
+
}
|
|
2362
|
+
if (Array.isArray(input)) {
|
|
2363
|
+
return input.flatMap((item) => sensitivePathsIn(item, depth + 1));
|
|
2364
|
+
}
|
|
2365
|
+
if (input !== null && typeof input === "object") {
|
|
2366
|
+
return Object.values(input).flatMap((value) => sensitivePathsIn(value, depth + 1));
|
|
2367
|
+
}
|
|
2368
|
+
return [];
|
|
2369
|
+
}
|
|
2370
|
+
var SECRET_PATTERNS = [
|
|
2371
|
+
{
|
|
2372
|
+
pattern: /-----BEGIN[^-]*PRIVATE KEY-----[\s\S]*?-----END[^-]*-----/g,
|
|
2373
|
+
replace: REDACTION_MARK
|
|
2374
|
+
},
|
|
2375
|
+
{ pattern: /\bhmy_at_[A-Za-z0-9_-]{8,}/g, replace: REDACTION_MARK },
|
|
2376
|
+
{ pattern: /\bhmy_[A-Za-z0-9_-]{8,}/g, replace: REDACTION_MARK },
|
|
2377
|
+
{ pattern: /\bsk-(?:ant-)?[A-Za-z0-9_-]{16,}/g, replace: REDACTION_MARK },
|
|
2378
|
+
{ pattern: /\bgh[pousr]_[A-Za-z0-9]{16,}/g, replace: REDACTION_MARK },
|
|
2379
|
+
{ pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}/g, replace: REDACTION_MARK },
|
|
2380
|
+
{ pattern: /\bxox[abprs]-[A-Za-z0-9-]{10,}/g, replace: REDACTION_MARK },
|
|
2381
|
+
{ pattern: /\bAKIA[0-9A-Z]{16}\b/g, replace: REDACTION_MARK },
|
|
2382
|
+
{ pattern: /\bAIza[0-9A-Za-z_-]{20,}/g, replace: REDACTION_MARK },
|
|
2383
|
+
{
|
|
2384
|
+
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
|
|
2385
|
+
replace: REDACTION_MARK
|
|
2386
|
+
},
|
|
2387
|
+
{
|
|
2388
|
+
pattern: /\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{12,}/gi,
|
|
2389
|
+
replace: `$1 ${REDACTION_MARK}`
|
|
2390
|
+
},
|
|
2391
|
+
{
|
|
2392
|
+
pattern: /(\w{1,32}:\/\/)[^/\s:@]+:[^/\s@]+@/g,
|
|
2393
|
+
replace: `$1${REDACTION_MARK}@`
|
|
2394
|
+
},
|
|
2395
|
+
{
|
|
2396
|
+
pattern: /\b([A-Za-z0-9_]{0,40}(?:TOKEN|SECRET|PASSWORD|PASSWD|APIKEY|API_KEY|ACCESS_KEY|PRIVATE_KEY|CREDENTIAL|AUTH)[A-Za-z0-9_]{0,40})\s*[=:]\s*(?:"[^"]*"|'[^']*'|`[^`]*`|[^\s,;)}\]]+)/gi,
|
|
2397
|
+
replace: `$1=${REDACTION_MARK}`
|
|
2398
|
+
},
|
|
2399
|
+
{
|
|
2400
|
+
pattern: /(--?(?:password|passwd|token|api-?key|secret|auth)(?:=|\s+))(?:"[^"]*"|'[^']*'|[^\s]+)/gi,
|
|
2401
|
+
replace: `$1${REDACTION_MARK}`
|
|
2402
|
+
}
|
|
2403
|
+
];
|
|
2404
|
+
function redactSecrets(text) {
|
|
2405
|
+
if (typeof text !== "string" || text.length === 0)
|
|
2406
|
+
return text;
|
|
2407
|
+
let out = text;
|
|
2408
|
+
for (const { pattern, replace } of SECRET_PATTERNS) {
|
|
2409
|
+
pattern.lastIndex = 0;
|
|
2410
|
+
out = out.replace(pattern, replace);
|
|
2411
|
+
}
|
|
2412
|
+
return out;
|
|
2413
|
+
}
|
|
2414
|
+
function truncate(text, max, originalLength) {
|
|
2415
|
+
const total = originalLength ?? text.length;
|
|
2416
|
+
if (total <= max)
|
|
2417
|
+
return text;
|
|
2418
|
+
return `${text.slice(0, max)}… [+${total - max} chars]`;
|
|
2419
|
+
}
|
|
2420
|
+
function redactThenTruncate(text, max) {
|
|
2421
|
+
const preCap = max * 4 + 64;
|
|
2422
|
+
const scanned = text.length > preCap ? text.slice(0, preCap) : text;
|
|
2423
|
+
return truncate(redactSecrets(scanned), max, text.length);
|
|
2424
|
+
}
|
|
2425
|
+
function redactStructure(value, depth = 0) {
|
|
2426
|
+
if (depth > 6)
|
|
2427
|
+
return REDACTION_MARK;
|
|
2428
|
+
if (typeof value === "string") {
|
|
2429
|
+
return redactThenTruncate(value, MAX_INPUT_STRING_CHARS);
|
|
2430
|
+
}
|
|
2431
|
+
if (Array.isArray(value)) {
|
|
2432
|
+
return value.slice(0, 20).map((item) => redactStructure(item, depth + 1));
|
|
2433
|
+
}
|
|
2434
|
+
if (value !== null && typeof value === "object") {
|
|
2435
|
+
const out = {};
|
|
2436
|
+
for (const [key, item] of Object.entries(value)) {
|
|
2437
|
+
out[key] = redactStructure(item, depth + 1);
|
|
2438
|
+
}
|
|
2439
|
+
return out;
|
|
2440
|
+
}
|
|
2441
|
+
return value;
|
|
2442
|
+
}
|
|
2443
|
+
function redactToolCall(args) {
|
|
2444
|
+
const sensitive = sensitivePathsIn(args.input);
|
|
2445
|
+
if (sensitive.length > 0) {
|
|
2446
|
+
return { withheld: "sensitive-path" };
|
|
2447
|
+
}
|
|
2448
|
+
const result = {};
|
|
2449
|
+
if (args.input !== undefined) {
|
|
2450
|
+
let input = redactStructure(args.input);
|
|
2451
|
+
let serialized;
|
|
2452
|
+
try {
|
|
2453
|
+
serialized = JSON.stringify(input) ?? "";
|
|
2454
|
+
} catch {
|
|
2455
|
+
serialized = "";
|
|
2456
|
+
input = REDACTION_MARK;
|
|
2457
|
+
}
|
|
2458
|
+
if (serialized.length > MAX_INPUT_CHARS) {
|
|
2459
|
+
input = truncate(serialized, MAX_INPUT_CHARS);
|
|
2460
|
+
}
|
|
2461
|
+
result.input = input;
|
|
2462
|
+
}
|
|
2463
|
+
if (typeof args.output === "string" && args.output.length > 0) {
|
|
2464
|
+
result.output = redactThenTruncate(args.output, MAX_OUTPUT_CHARS);
|
|
2465
|
+
}
|
|
2466
|
+
return result;
|
|
2467
|
+
}
|
|
2207
2468
|
// ../harmony-shared/dist/stageHandoff.js
|
|
2208
2469
|
var HANDOFF_MARKER = "harmony:stage-handoff";
|
|
2209
2470
|
var HANDOFF_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + HANDOFF_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
|
|
@@ -2517,6 +2778,15 @@ class HarmonyApiClient {
|
|
|
2517
2778
|
async registerWorkspaceAgent(workspaceId, data) {
|
|
2518
2779
|
return this.request("POST", `/workspaces/${workspaceId}/agents`, data);
|
|
2519
2780
|
}
|
|
2781
|
+
async reportAgentConfig(workspaceId, agentId, config) {
|
|
2782
|
+
return this.request("POST", `/workspaces/${workspaceId}/agents/${agentId}/reported-config`, { config });
|
|
2783
|
+
}
|
|
2784
|
+
async getWorkspaceModelConfig(workspaceId) {
|
|
2785
|
+
return this.request("GET", `/workspaces/${workspaceId}/model-config`);
|
|
2786
|
+
}
|
|
2787
|
+
async getModelCatalog() {
|
|
2788
|
+
return this.request("GET", "/model-catalog");
|
|
2789
|
+
}
|
|
2520
2790
|
async listProjects(workspaceId) {
|
|
2521
2791
|
return this.request("GET", `/workspaces/${workspaceId}/projects`);
|
|
2522
2792
|
}
|
|
@@ -2665,6 +2935,9 @@ class HarmonyApiClient {
|
|
|
2665
2935
|
title
|
|
2666
2936
|
});
|
|
2667
2937
|
}
|
|
2938
|
+
async removeExternalLink(cardId, linkId) {
|
|
2939
|
+
return this.request("DELETE", `/cards/${cardId}/external-links/${linkId}`);
|
|
2940
|
+
}
|
|
2668
2941
|
async uploadArtifact(data) {
|
|
2669
2942
|
return this.request("POST", "/artifacts", data);
|
|
2670
2943
|
}
|
|
@@ -2757,7 +3030,10 @@ class HarmonyApiClient {
|
|
|
2757
3030
|
return this.request("DELETE", `/cards/${cardId}/agent-context`, data);
|
|
2758
3031
|
}
|
|
2759
3032
|
async appendAgentRunEvents(cardId, data) {
|
|
2760
|
-
return this.request("POST", `/cards/${cardId}/agent-run-events`,
|
|
3033
|
+
return this.request("POST", `/cards/${cardId}/agent-run-events`, {
|
|
3034
|
+
...data,
|
|
3035
|
+
events: data.events.map((event) => sanitizeRunEventDraft(event))
|
|
3036
|
+
});
|
|
2761
3037
|
}
|
|
2762
3038
|
async getPendingUserMessages(cardId, sessionId, sinceSeq) {
|
|
2763
3039
|
return this.request("GET", `/cards/${cardId}/agent-messages?sessionId=${sessionId}&sinceSeq=${sinceSeq}`);
|
|
@@ -5447,6 +5723,23 @@ var TOOLS = {
|
|
|
5447
5723
|
required: ["cardId", "url"]
|
|
5448
5724
|
}
|
|
5449
5725
|
},
|
|
5726
|
+
harmony_remove_external_link: {
|
|
5727
|
+
description: "Remove an external reference URL from a card — the counterpart to harmony_add_external_link. Takes the link id from harmony_get_card_external_links, not the URL.",
|
|
5728
|
+
inputSchema: {
|
|
5729
|
+
type: "object",
|
|
5730
|
+
properties: {
|
|
5731
|
+
cardId: {
|
|
5732
|
+
type: "string",
|
|
5733
|
+
description: "Card UUID"
|
|
5734
|
+
},
|
|
5735
|
+
linkId: {
|
|
5736
|
+
type: "string",
|
|
5737
|
+
description: "External link UUID, as returned by harmony_get_card_external_links"
|
|
5738
|
+
}
|
|
5739
|
+
},
|
|
5740
|
+
required: ["cardId", "linkId"]
|
|
5741
|
+
}
|
|
5742
|
+
},
|
|
5450
5743
|
harmony_create_subtask: {
|
|
5451
5744
|
description: "Create a subtask on a card",
|
|
5452
5745
|
inputSchema: {
|
|
@@ -7341,6 +7634,11 @@ ${list}
|
|
|
7341
7634
|
const result = await client3.addExternalLink(cardId, url, title);
|
|
7342
7635
|
return { success: true, ...result };
|
|
7343
7636
|
}
|
|
7637
|
+
case "harmony_remove_external_link": {
|
|
7638
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
7639
|
+
const linkId = z.string().uuid().parse(args.linkId);
|
|
7640
|
+
return await client3.removeExternalLink(cardId, linkId);
|
|
7641
|
+
}
|
|
7344
7642
|
case "harmony_classify_card":
|
|
7345
7643
|
return deprecatedRemovedToolResult("harmony_classify_card");
|
|
7346
7644
|
case "harmony_create_subtask": {
|