@gethmy/mcp 2.8.0 → 2.8.2
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 +617 -276
- package/dist/index.js +525 -5
- package/dist/lib/api-client.js +121 -0
- package/package.json +2 -2
- package/src/api-client.ts +66 -1
- package/src/cli.ts +2 -2
- package/src/hmy-config.ts +70 -0
- package/src/server.ts +158 -3
- package/src/skills.ts +115 -19
- package/src/tui/setup.ts +8 -8
package/dist/index.js
CHANGED
|
@@ -932,6 +932,87 @@ function getDisplayLinkType(linkType, direction) {
|
|
|
932
932
|
return linkType;
|
|
933
933
|
return LINK_TYPE_INVERSES[linkType];
|
|
934
934
|
}
|
|
935
|
+
// ../harmony-shared/dist/commentSerializer.js
|
|
936
|
+
var CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
|
|
937
|
+
function authorLabel(c) {
|
|
938
|
+
if (c.author_type === "agent")
|
|
939
|
+
return "AI agent";
|
|
940
|
+
return c.author?.full_name || c.author?.email || "teammate";
|
|
941
|
+
}
|
|
942
|
+
function criticalIds(comments) {
|
|
943
|
+
const keep = new Set;
|
|
944
|
+
for (const c of comments) {
|
|
945
|
+
if (c.comment_type === "decision")
|
|
946
|
+
keep.add(c.id);
|
|
947
|
+
if (c.supersedes_id) {
|
|
948
|
+
keep.add(c.id);
|
|
949
|
+
keep.add(c.supersedes_id);
|
|
950
|
+
}
|
|
951
|
+
if (c.confirms_id) {
|
|
952
|
+
keep.add(c.id);
|
|
953
|
+
keep.add(c.confirms_id);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return keep;
|
|
957
|
+
}
|
|
958
|
+
function serializeCommentThread(comments, options = {}) {
|
|
959
|
+
const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
|
|
960
|
+
const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
961
|
+
if (visible.length === 0)
|
|
962
|
+
return "";
|
|
963
|
+
const indexById = new Map;
|
|
964
|
+
visible.forEach((c, i) => {
|
|
965
|
+
indexById.set(c.id, i + 1);
|
|
966
|
+
});
|
|
967
|
+
let rendered = visible;
|
|
968
|
+
let elidedCount = 0;
|
|
969
|
+
if (maxComments && visible.length > maxComments) {
|
|
970
|
+
const keep = criticalIds(visible);
|
|
971
|
+
const recentThreshold = visible.length - maxComments;
|
|
972
|
+
rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
|
|
973
|
+
elidedCount = visible.length - rendered.length;
|
|
974
|
+
}
|
|
975
|
+
const ref = (id) => {
|
|
976
|
+
const n = indexById.get(id);
|
|
977
|
+
return n ? `#${n}` : `#${id.slice(0, 8)}`;
|
|
978
|
+
};
|
|
979
|
+
const lines = [];
|
|
980
|
+
if (elidedCount > 0) {
|
|
981
|
+
lines.push({
|
|
982
|
+
at: visible[0]?.created_at ?? "",
|
|
983
|
+
text: `(${elidedCount} earlier comment(s) omitted for brevity)`
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
for (const c of rendered) {
|
|
987
|
+
const tags = [];
|
|
988
|
+
if (c.edited_at)
|
|
989
|
+
tags.push("edited");
|
|
990
|
+
if (c.supersedes_id)
|
|
991
|
+
tags.push(`supersedes ${ref(c.supersedes_id)}`);
|
|
992
|
+
if (c.confirms_id)
|
|
993
|
+
tags.push(`confirms ${ref(c.confirms_id)}`);
|
|
994
|
+
if (c.resolved_at)
|
|
995
|
+
tags.push("resolved");
|
|
996
|
+
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
997
|
+
const header = `[${ref(c.id)} | ${c.author_type} | ${authorLabel(c)} | ${c.comment_type} | ${c.created_at}${tagStr}]`;
|
|
998
|
+
lines.push({ at: c.created_at, text: `${header}
|
|
999
|
+
${c.body.trim()}` });
|
|
1000
|
+
}
|
|
1001
|
+
for (const a of activity) {
|
|
1002
|
+
const actor = a.actor ? `${a.actor} ` : "";
|
|
1003
|
+
lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
|
|
1004
|
+
}
|
|
1005
|
+
lines.sort((a, b) => a.at.localeCompare(b.at));
|
|
1006
|
+
const body = lines.map((l) => l.text).join(`
|
|
1007
|
+
|
|
1008
|
+
`);
|
|
1009
|
+
const instruction = includeInstructions ? `
|
|
1010
|
+
|
|
1011
|
+
${CONFLICT_INSTRUCTION}` : "";
|
|
1012
|
+
return `## ${heading} (oldest → newest)
|
|
1013
|
+
|
|
1014
|
+
${body}${instruction}`;
|
|
1015
|
+
}
|
|
935
1016
|
// ../harmony-shared/dist/constants.js
|
|
936
1017
|
var TIMINGS = {
|
|
937
1018
|
SEARCH_DEBOUNCE: 300,
|
|
@@ -1469,6 +1550,28 @@ class HarmonyApiClient {
|
|
|
1469
1550
|
async deleteSubtask(subtaskId) {
|
|
1470
1551
|
return this.request("DELETE", `/subtasks/${subtaskId}`);
|
|
1471
1552
|
}
|
|
1553
|
+
async addComment(cardId, body, opts) {
|
|
1554
|
+
return this.request("POST", `/cards/${cardId}/comments`, {
|
|
1555
|
+
body,
|
|
1556
|
+
authorType: "agent",
|
|
1557
|
+
commentType: opts?.commentType,
|
|
1558
|
+
supersedesId: opts?.supersedesId,
|
|
1559
|
+
confirmsId: opts?.confirmsId,
|
|
1560
|
+
agentSessionId: opts?.agentSessionId
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
async getComments(cardId, opts) {
|
|
1564
|
+
const qs = new URLSearchParams;
|
|
1565
|
+
if (opts?.limit != null)
|
|
1566
|
+
qs.set("limit", String(opts.limit));
|
|
1567
|
+
if (opts?.offset != null)
|
|
1568
|
+
qs.set("offset", String(opts.offset));
|
|
1569
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
1570
|
+
return this.request("GET", `/cards/${cardId}/comments${suffix}`);
|
|
1571
|
+
}
|
|
1572
|
+
async updateComment(commentId, updates) {
|
|
1573
|
+
return this.request("PATCH", `/comments/${commentId}`, updates);
|
|
1574
|
+
}
|
|
1472
1575
|
async startAgentSession(cardId, data) {
|
|
1473
1576
|
return this.request("POST", `/cards/${cardId}/agent-context`, data);
|
|
1474
1577
|
}
|
|
@@ -1826,6 +1929,24 @@ class HarmonyApiClient {
|
|
|
1826
1929
|
assembledContext: assembledContextStr,
|
|
1827
1930
|
assemblyId
|
|
1828
1931
|
});
|
|
1932
|
+
try {
|
|
1933
|
+
const { comments } = await this.getComments(options.cardId, {
|
|
1934
|
+
limit: 200
|
|
1935
|
+
});
|
|
1936
|
+
if (Array.isArray(comments) && comments.length > 0) {
|
|
1937
|
+
const section = serializeCommentThread(comments, {
|
|
1938
|
+
heading: "Comments",
|
|
1939
|
+
maxComments: 40
|
|
1940
|
+
});
|
|
1941
|
+
if (section)
|
|
1942
|
+
result.prompt = `${result.prompt}
|
|
1943
|
+
|
|
1944
|
+
${section}`;
|
|
1945
|
+
}
|
|
1946
|
+
} catch (err) {
|
|
1947
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1948
|
+
console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
|
|
1949
|
+
}
|
|
1829
1950
|
try {
|
|
1830
1951
|
await this.recordPromptHistory({
|
|
1831
1952
|
cardId: cardData.id,
|
|
@@ -2374,6 +2495,294 @@ async function onboardNewUser(params) {
|
|
|
2374
2495
|
};
|
|
2375
2496
|
}
|
|
2376
2497
|
|
|
2498
|
+
// src/skills.ts
|
|
2499
|
+
import {
|
|
2500
|
+
existsSync as existsSync4,
|
|
2501
|
+
mkdirSync as mkdirSync3,
|
|
2502
|
+
readFileSync as readFileSync4,
|
|
2503
|
+
renameSync,
|
|
2504
|
+
writeFileSync as writeFileSync3
|
|
2505
|
+
} from "node:fs";
|
|
2506
|
+
import { homedir as homedir3 } from "node:os";
|
|
2507
|
+
import { dirname, join as join4 } from "node:path";
|
|
2508
|
+
|
|
2509
|
+
// src/hmy-config.ts
|
|
2510
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
2511
|
+
import { homedir as homedir2 } from "node:os";
|
|
2512
|
+
import { join as join3 } from "node:path";
|
|
2513
|
+
var DEFAULTS = { updateCheck: true, pin: null };
|
|
2514
|
+
function getHmyConfigPath() {
|
|
2515
|
+
return join3(homedir2(), ".hmy", "config.yaml");
|
|
2516
|
+
}
|
|
2517
|
+
function loadHmyConfig() {
|
|
2518
|
+
const path = getHmyConfigPath();
|
|
2519
|
+
if (!existsSync3(path))
|
|
2520
|
+
return { ...DEFAULTS };
|
|
2521
|
+
try {
|
|
2522
|
+
return parseHmyConfig(readFileSync3(path, "utf-8"));
|
|
2523
|
+
} catch {
|
|
2524
|
+
return { ...DEFAULTS };
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
function parseHmyConfig(text) {
|
|
2528
|
+
const cfg = { ...DEFAULTS };
|
|
2529
|
+
for (const rawLine of text.split(`
|
|
2530
|
+
`)) {
|
|
2531
|
+
const line = rawLine.trim();
|
|
2532
|
+
if (!line || line.startsWith("#"))
|
|
2533
|
+
continue;
|
|
2534
|
+
const sep2 = line.indexOf(":");
|
|
2535
|
+
if (sep2 === -1)
|
|
2536
|
+
continue;
|
|
2537
|
+
const key = line.slice(0, sep2).trim();
|
|
2538
|
+
let value = line.slice(sep2 + 1).trim();
|
|
2539
|
+
const hash = value.indexOf(" #");
|
|
2540
|
+
if (hash !== -1)
|
|
2541
|
+
value = value.slice(0, hash).trim();
|
|
2542
|
+
value = value.replace(/^["']|["']$/g, "");
|
|
2543
|
+
switch (key) {
|
|
2544
|
+
case "update_check":
|
|
2545
|
+
cfg.updateCheck = value !== "false";
|
|
2546
|
+
break;
|
|
2547
|
+
case "pin":
|
|
2548
|
+
case "pin_version":
|
|
2549
|
+
cfg.pin = value || null;
|
|
2550
|
+
break;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
return cfg;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
// src/skills.ts
|
|
2557
|
+
var HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
|
|
2558
|
+
|
|
2559
|
+
Start work on a Harmony card. Card reference: $ARGUMENTS
|
|
2560
|
+
|
|
2561
|
+
## 1. Find & Fetch Card
|
|
2562
|
+
|
|
2563
|
+
Parse the reference and fetch the card:
|
|
2564
|
+
- \`#42\` or \`42\` → \`harmony_get_card_by_short_id\` with \`shortId: 42\`
|
|
2565
|
+
- UUID → \`harmony_get_card\` with \`cardId\`
|
|
2566
|
+
- Name/text → \`harmony_search_cards\` with \`query\`
|
|
2567
|
+
|
|
2568
|
+
## 2. Get Board State
|
|
2569
|
+
|
|
2570
|
+
Call \`harmony_get_board\` to get columns and labels. From the response:
|
|
2571
|
+
- Find the "In Progress" (or "Progress") column ID
|
|
2572
|
+
- Find the "agent" label ID
|
|
2573
|
+
|
|
2574
|
+
## 3. Setup Card for Work
|
|
2575
|
+
|
|
2576
|
+
Execute these in sequence:
|
|
2577
|
+
1. \`harmony_move_card\` → Move to "In Progress" column
|
|
2578
|
+
2. \`harmony_add_label_to_card\` → Add "agent" label
|
|
2579
|
+
3. \`harmony_start_agent_session\`:
|
|
2580
|
+
- \`cardId\`: Card UUID
|
|
2581
|
+
- \`agentIdentifier\`: Your agent identifier
|
|
2582
|
+
- \`agentName\`: Your agent name
|
|
2583
|
+
- \`currentTask\`: "Analyzing card requirements"
|
|
2584
|
+
|
|
2585
|
+
## 4. Generate Work Prompt
|
|
2586
|
+
|
|
2587
|
+
Call \`harmony_generate_prompt\` with:
|
|
2588
|
+
- \`cardId\` or \`shortId\` (+ \`projectId\` if using shortId)
|
|
2589
|
+
- \`variant\`: Select based on task:
|
|
2590
|
+
- \`"execute"\` (default) → Clear tasks, bug fixes, well-defined work
|
|
2591
|
+
- \`"analysis"\` → Complex features, unclear requirements
|
|
2592
|
+
- \`"draft"\` → Medium complexity, want feedback first
|
|
2593
|
+
|
|
2594
|
+
The generated prompt provides role framing, focus areas, subtasks, linked cards, and suggested outputs.
|
|
2595
|
+
|
|
2596
|
+
## 5. Display Card Summary
|
|
2597
|
+
|
|
2598
|
+
Show the user: Card title, short ID, role, priority, labels, due date, description, and subtasks.
|
|
2599
|
+
|
|
2600
|
+
## 6. Implement Solution
|
|
2601
|
+
|
|
2602
|
+
Work on the card following the generated prompt's guidance. Update progress at milestones:
|
|
2603
|
+
- \`harmony_update_agent_progress\` with \`progressPercent\` (0-100), \`currentTask\`, \`status\`, \`blockers\`
|
|
2604
|
+
|
|
2605
|
+
**Progress checkpoints:** 20% (exploration), 50% (implementation), 80% (testing), 100% (done)
|
|
2606
|
+
|
|
2607
|
+
## 7. Complete Work
|
|
2608
|
+
|
|
2609
|
+
When finished:
|
|
2610
|
+
1. \`harmony_end_agent_session\` with \`status: "completed"\`, \`progressPercent: 100\`
|
|
2611
|
+
2. \`harmony_move_card\` to "Review" column
|
|
2612
|
+
3. Summarize accomplishments
|
|
2613
|
+
|
|
2614
|
+
If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
|
|
2615
|
+
|
|
2616
|
+
## Key Tools Reference
|
|
2617
|
+
|
|
2618
|
+
**Cards:** \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\`, \`harmony_create_card\`, \`harmony_update_card\`, \`harmony_move_card\`, \`harmony_delete_card\`, \`harmony_assign_card\`
|
|
2619
|
+
|
|
2620
|
+
**Subtasks:** \`harmony_create_subtask\`, \`harmony_toggle_subtask\`, \`harmony_delete_subtask\`
|
|
2621
|
+
|
|
2622
|
+
**Labels:** \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\`, \`harmony_create_label\`
|
|
2623
|
+
|
|
2624
|
+
**Links:** \`harmony_add_link_to_card\`, \`harmony_remove_link_from_card\`, \`harmony_get_card_links\`
|
|
2625
|
+
|
|
2626
|
+
**Board:** \`harmony_get_board\`, \`harmony_list_projects\`, \`harmony_get_context\`, \`harmony_set_project_context\`
|
|
2627
|
+
|
|
2628
|
+
**Sessions:** \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\`, \`harmony_get_agent_session\`
|
|
2629
|
+
|
|
2630
|
+
**AI:** \`harmony_generate_prompt\`, \`harmony_process_command\`
|
|
2631
|
+
`;
|
|
2632
|
+
function buildSkillFile(skill) {
|
|
2633
|
+
const content = stripSkillPreamble(skill.content);
|
|
2634
|
+
if (skill.skillVersion !== undefined && !hasMetadataVersion(content)) {
|
|
2635
|
+
return injectMetadataVersion(content, skill.skillVersion);
|
|
2636
|
+
}
|
|
2637
|
+
return content;
|
|
2638
|
+
}
|
|
2639
|
+
var PREAMBLE_START = "<!-- hmy-skills-preamble:start -->";
|
|
2640
|
+
var PREAMBLE_END = "<!-- hmy-skills-preamble:end -->";
|
|
2641
|
+
function stripSkillPreamble(content) {
|
|
2642
|
+
const start = content.indexOf(PREAMBLE_START);
|
|
2643
|
+
const end = content.indexOf(PREAMBLE_END);
|
|
2644
|
+
if (start === -1 || end === -1 || end < start)
|
|
2645
|
+
return content;
|
|
2646
|
+
const stripped = content.slice(0, start) + content.slice(end + PREAMBLE_END.length);
|
|
2647
|
+
return `${stripped.replace(/\n{3,}/g, `
|
|
2648
|
+
|
|
2649
|
+
`).replace(/\s+$/, "")}
|
|
2650
|
+
`;
|
|
2651
|
+
}
|
|
2652
|
+
function atomicWrite(filePath, content) {
|
|
2653
|
+
const dir = dirname(filePath);
|
|
2654
|
+
if (!existsSync4(dir))
|
|
2655
|
+
mkdirSync3(dir, { recursive: true });
|
|
2656
|
+
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
2657
|
+
writeFileSync3(tmp, content);
|
|
2658
|
+
renameSync(tmp, filePath);
|
|
2659
|
+
}
|
|
2660
|
+
function hasMetadataVersion(content) {
|
|
2661
|
+
return parseSkillVersion(content) !== null;
|
|
2662
|
+
}
|
|
2663
|
+
function injectMetadataVersion(content, version) {
|
|
2664
|
+
const fmMatch = content.match(/^(---\n[\s\S]*?\n)(---\n)([\s\S]*)$/);
|
|
2665
|
+
if (!fmMatch)
|
|
2666
|
+
return content;
|
|
2667
|
+
const [, head, close, body] = fmMatch;
|
|
2668
|
+
const block = `metadata:
|
|
2669
|
+
version: "${version}"
|
|
2670
|
+
`;
|
|
2671
|
+
return `${head}${block}${close}${body}`;
|
|
2672
|
+
}
|
|
2673
|
+
function parseSkillVersion(content) {
|
|
2674
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2675
|
+
if (fmMatch) {
|
|
2676
|
+
const fm = fmMatch[1];
|
|
2677
|
+
const verMatch = fm.match(/^metadata:[\s\S]*?\n[ \t]+version:[ \t]*["']?(\d+)["']?\s*$/m);
|
|
2678
|
+
if (verMatch)
|
|
2679
|
+
return verMatch[1];
|
|
2680
|
+
}
|
|
2681
|
+
const legacy = content.match(/<!-- skills-version:(\d+) -->/);
|
|
2682
|
+
return legacy ? legacy[1] : null;
|
|
2683
|
+
}
|
|
2684
|
+
function findSkillFiles(paths, knownNames) {
|
|
2685
|
+
const results = [];
|
|
2686
|
+
for (const filePath of paths) {
|
|
2687
|
+
if (!existsSync4(filePath))
|
|
2688
|
+
continue;
|
|
2689
|
+
for (const name of knownNames) {
|
|
2690
|
+
if (filePath.includes(`/${name}/`) || filePath.includes(`/${name}.md`)) {
|
|
2691
|
+
results.push({ name, filePath });
|
|
2692
|
+
break;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
return results;
|
|
2697
|
+
}
|
|
2698
|
+
var HMY_DIR = join4(homedir3(), ".hmy");
|
|
2699
|
+
var HMY_VERSION_FILE = join4(HMY_DIR, "VERSION");
|
|
2700
|
+
var LAST_CHECK_FILE = join4(HMY_DIR, "last-update-check");
|
|
2701
|
+
var CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
2702
|
+
function checkedRecently(now = Date.now()) {
|
|
2703
|
+
try {
|
|
2704
|
+
if (!existsSync4(LAST_CHECK_FILE))
|
|
2705
|
+
return false;
|
|
2706
|
+
const ts = Number.parseInt(readFileSync4(LAST_CHECK_FILE, "utf-8").trim(), 10);
|
|
2707
|
+
if (!Number.isFinite(ts))
|
|
2708
|
+
return false;
|
|
2709
|
+
return now - ts < CHECK_TTL_MS;
|
|
2710
|
+
} catch {
|
|
2711
|
+
return false;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
function recordCheck(now = Date.now()) {
|
|
2715
|
+
try {
|
|
2716
|
+
if (!existsSync4(HMY_DIR))
|
|
2717
|
+
mkdirSync3(HMY_DIR, { recursive: true });
|
|
2718
|
+
writeFileSync3(LAST_CHECK_FILE, String(now));
|
|
2719
|
+
} catch {}
|
|
2720
|
+
}
|
|
2721
|
+
async function refreshSkills(opts = {}) {
|
|
2722
|
+
try {
|
|
2723
|
+
if (!isConfigured())
|
|
2724
|
+
return { updated: false };
|
|
2725
|
+
const cfg = loadHmyConfig();
|
|
2726
|
+
if (!cfg.updateCheck || cfg.pin)
|
|
2727
|
+
return { updated: false };
|
|
2728
|
+
if (!opts.force && checkedRecently())
|
|
2729
|
+
return { updated: false };
|
|
2730
|
+
const status = areSkillsInstalled();
|
|
2731
|
+
if (!status.installed)
|
|
2732
|
+
return { updated: false };
|
|
2733
|
+
const client3 = new HarmonyApiClient;
|
|
2734
|
+
const versionInfo = await client3.fetchSkillsVersion();
|
|
2735
|
+
recordCheck();
|
|
2736
|
+
const skillFiles = findSkillFiles(status.paths, versionInfo.skills);
|
|
2737
|
+
if (skillFiles.length > 0) {
|
|
2738
|
+
const samplePath = skillFiles[0].filePath;
|
|
2739
|
+
for (const name of versionInfo.skills) {
|
|
2740
|
+
if (skillFiles.some((sf) => sf.name === name))
|
|
2741
|
+
continue;
|
|
2742
|
+
let siblingPath;
|
|
2743
|
+
if (samplePath.endsWith("SKILL.md")) {
|
|
2744
|
+
const parentDir = dirname(dirname(samplePath));
|
|
2745
|
+
siblingPath = `${parentDir}/${name}/SKILL.md`;
|
|
2746
|
+
} else {
|
|
2747
|
+
const parentDir = dirname(samplePath);
|
|
2748
|
+
siblingPath = `${parentDir}/${name}.md`;
|
|
2749
|
+
}
|
|
2750
|
+
if (existsSync4(siblingPath)) {
|
|
2751
|
+
skillFiles.push({ name, filePath: siblingPath });
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
if (skillFiles.length === 0)
|
|
2756
|
+
return { updated: false };
|
|
2757
|
+
let updated = false;
|
|
2758
|
+
for (const { name, filePath } of skillFiles) {
|
|
2759
|
+
try {
|
|
2760
|
+
const currentContent = readFileSync4(filePath, "utf-8");
|
|
2761
|
+
const localVersion = parseSkillVersion(currentContent);
|
|
2762
|
+
const fetched = await client3.fetchSkill(name);
|
|
2763
|
+
const remoteVersion = fetched.skillVersion;
|
|
2764
|
+
if (remoteVersion !== undefined && localVersion !== null && Number(localVersion) >= remoteVersion) {
|
|
2765
|
+
continue;
|
|
2766
|
+
}
|
|
2767
|
+
atomicWrite(filePath, buildSkillFile(fetched));
|
|
2768
|
+
updated = true;
|
|
2769
|
+
} catch (err) {
|
|
2770
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2771
|
+
console.error(`Harmony: skill "${name}" refresh failed: ${msg}`);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
if (updated) {
|
|
2775
|
+
try {
|
|
2776
|
+
atomicWrite(HMY_VERSION_FILE, versionInfo.version);
|
|
2777
|
+
} catch {}
|
|
2778
|
+
console.error("Harmony: Refreshed skills from server");
|
|
2779
|
+
}
|
|
2780
|
+
return { updated };
|
|
2781
|
+
} catch {
|
|
2782
|
+
return { updated: false };
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2377
2786
|
// src/server.ts
|
|
2378
2787
|
var memorySessions = new Map;
|
|
2379
2788
|
function parseLabelList(raw) {
|
|
@@ -2511,7 +2920,11 @@ var TOOLS = {
|
|
|
2511
2920
|
enum: ["low", "medium", "high", "urgent"],
|
|
2512
2921
|
description: "Priority level"
|
|
2513
2922
|
},
|
|
2514
|
-
assigneeId: { type: "string", description: "Assignee user ID" }
|
|
2923
|
+
assigneeId: { type: "string", description: "Assignee user ID" },
|
|
2924
|
+
planId: {
|
|
2925
|
+
type: "string",
|
|
2926
|
+
description: "Plan ID to link this card to (optional). Links the card to that plan via its plan_id."
|
|
2927
|
+
}
|
|
2515
2928
|
},
|
|
2516
2929
|
required: ["title"]
|
|
2517
2930
|
}
|
|
@@ -2836,6 +3249,66 @@ var TOOLS = {
|
|
|
2836
3249
|
required: ["subtaskId"]
|
|
2837
3250
|
}
|
|
2838
3251
|
},
|
|
3252
|
+
harmony_add_comment: {
|
|
3253
|
+
description: "Post a comment on a card as the agent. Use this to converse with the human in the open: report progress, ask a question, record a decision, or note a finding — instead of editing the card description. Set supersedesId to correct an earlier comment, confirmsId to reaffirm one. When the thread conflicts, prefer the latest comment unless a later one confirms an earlier finding; cite the comment id(s) you relied on.",
|
|
3254
|
+
inputSchema: {
|
|
3255
|
+
type: "object",
|
|
3256
|
+
properties: {
|
|
3257
|
+
cardId: { type: "string", description: "Card UUID to comment on" },
|
|
3258
|
+
body: { type: "string", description: "Comment body (Markdown)" },
|
|
3259
|
+
commentType: {
|
|
3260
|
+
type: "string",
|
|
3261
|
+
enum: [
|
|
3262
|
+
"message",
|
|
3263
|
+
"progress",
|
|
3264
|
+
"question",
|
|
3265
|
+
"blocker",
|
|
3266
|
+
"decision",
|
|
3267
|
+
"summary",
|
|
3268
|
+
"finding"
|
|
3269
|
+
],
|
|
3270
|
+
description: "Type of comment. 'question'/'blocker' signal you need a human; default 'message'."
|
|
3271
|
+
},
|
|
3272
|
+
supersedesId: {
|
|
3273
|
+
type: "string",
|
|
3274
|
+
description: "Comment id this comment corrects/updates"
|
|
3275
|
+
},
|
|
3276
|
+
confirmsId: {
|
|
3277
|
+
type: "string",
|
|
3278
|
+
description: "Comment id this comment reaffirms"
|
|
3279
|
+
}
|
|
3280
|
+
},
|
|
3281
|
+
required: ["cardId", "body"]
|
|
3282
|
+
}
|
|
3283
|
+
},
|
|
3284
|
+
harmony_get_comments: {
|
|
3285
|
+
description: "Get the comment thread on a card (oldest → newest). Returns human + agent comments with author, type, edited/resolved state, and supersede/confirm links. Read this before acting so you weigh later comments over earlier ones they contradict.",
|
|
3286
|
+
inputSchema: {
|
|
3287
|
+
type: "object",
|
|
3288
|
+
properties: {
|
|
3289
|
+
cardId: { type: "string" },
|
|
3290
|
+
limit: { type: "number" },
|
|
3291
|
+
offset: { type: "number" }
|
|
3292
|
+
},
|
|
3293
|
+
required: ["cardId"]
|
|
3294
|
+
}
|
|
3295
|
+
},
|
|
3296
|
+
harmony_update_comment: {
|
|
3297
|
+
description: "Update one of your own comments: edit the body, pin it, or resolve a question/blocker once it has been answered.",
|
|
3298
|
+
inputSchema: {
|
|
3299
|
+
type: "object",
|
|
3300
|
+
properties: {
|
|
3301
|
+
commentId: { type: "string" },
|
|
3302
|
+
body: { type: "string" },
|
|
3303
|
+
pinned: { type: "boolean" },
|
|
3304
|
+
resolve: {
|
|
3305
|
+
type: "boolean",
|
|
3306
|
+
description: "Mark (true) or clear (false) the resolved state"
|
|
3307
|
+
}
|
|
3308
|
+
},
|
|
3309
|
+
required: ["commentId"]
|
|
3310
|
+
}
|
|
3311
|
+
},
|
|
2839
3312
|
harmony_list_workspaces: {
|
|
2840
3313
|
description: "List all workspaces the user has access to",
|
|
2841
3314
|
inputSchema: {
|
|
@@ -3826,7 +4299,7 @@ function registerHandlers(server, deps) {
|
|
|
3826
4299
|
{
|
|
3827
4300
|
uri,
|
|
3828
4301
|
mimeType: "text/markdown",
|
|
3829
|
-
text: fetched.content
|
|
4302
|
+
text: stripSkillPreamble(fetched.content)
|
|
3830
4303
|
}
|
|
3831
4304
|
]
|
|
3832
4305
|
};
|
|
@@ -3876,7 +4349,8 @@ async function handleToolCall(name, args, deps) {
|
|
|
3876
4349
|
columnId: args.columnId,
|
|
3877
4350
|
description: args.description,
|
|
3878
4351
|
priority: args.priority,
|
|
3879
|
-
assigneeId: args.assigneeId
|
|
4352
|
+
assigneeId: args.assigneeId,
|
|
4353
|
+
planId: args.planId
|
|
3880
4354
|
});
|
|
3881
4355
|
return { success: true, ...result };
|
|
3882
4356
|
}
|
|
@@ -4096,6 +4570,49 @@ async function handleToolCall(name, args, deps) {
|
|
|
4096
4570
|
await client3.deleteSubtask(subtaskId);
|
|
4097
4571
|
return { success: true };
|
|
4098
4572
|
}
|
|
4573
|
+
case "harmony_add_comment": {
|
|
4574
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
4575
|
+
const body = z.string().min(1).max(1e4).parse(args.body);
|
|
4576
|
+
const commentType = args.commentType !== undefined ? z.enum([
|
|
4577
|
+
"message",
|
|
4578
|
+
"progress",
|
|
4579
|
+
"question",
|
|
4580
|
+
"blocker",
|
|
4581
|
+
"decision",
|
|
4582
|
+
"summary",
|
|
4583
|
+
"finding"
|
|
4584
|
+
]).parse(args.commentType) : undefined;
|
|
4585
|
+
const supersedesId = args.supersedesId !== undefined ? z.string().uuid().parse(args.supersedesId) : undefined;
|
|
4586
|
+
const confirmsId = args.confirmsId !== undefined ? z.string().uuid().parse(args.confirmsId) : undefined;
|
|
4587
|
+
const result = await client3.addComment(cardId, body, {
|
|
4588
|
+
commentType,
|
|
4589
|
+
supersedesId,
|
|
4590
|
+
confirmsId
|
|
4591
|
+
});
|
|
4592
|
+
return { success: true, ...result };
|
|
4593
|
+
}
|
|
4594
|
+
case "harmony_get_comments": {
|
|
4595
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
4596
|
+
const limit = args.limit !== undefined ? z.number().int().min(1).max(500).parse(args.limit) : undefined;
|
|
4597
|
+
const offset = args.offset !== undefined ? z.number().int().min(0).parse(args.offset) : undefined;
|
|
4598
|
+
const result = await client3.getComments(cardId, { limit, offset });
|
|
4599
|
+
return { success: true, ...result };
|
|
4600
|
+
}
|
|
4601
|
+
case "harmony_update_comment": {
|
|
4602
|
+
const commentId = z.string().uuid().parse(args.commentId);
|
|
4603
|
+
const updates = {};
|
|
4604
|
+
if (args.body !== undefined) {
|
|
4605
|
+
updates.body = z.string().min(1).max(1e4).parse(args.body);
|
|
4606
|
+
}
|
|
4607
|
+
if (args.pinned !== undefined) {
|
|
4608
|
+
updates.pinned = z.boolean().parse(args.pinned);
|
|
4609
|
+
}
|
|
4610
|
+
if (args.resolve !== undefined) {
|
|
4611
|
+
updates.resolve = z.boolean().parse(args.resolve);
|
|
4612
|
+
}
|
|
4613
|
+
const result = await client3.updateComment(commentId, updates);
|
|
4614
|
+
return { success: true, ...result };
|
|
4615
|
+
}
|
|
4099
4616
|
case "harmony_list_workspaces": {
|
|
4100
4617
|
const result = await client3.listWorkspaces();
|
|
4101
4618
|
return { success: true, ...result };
|
|
@@ -4902,13 +5419,16 @@ function createConfigDeps() {
|
|
|
4902
5419
|
class HarmonyMCPServer {
|
|
4903
5420
|
server;
|
|
4904
5421
|
constructor() {
|
|
4905
|
-
this.server = new Server({ name: "@gethmy/mcp", version: "2.0.0" }, { capabilities: { tools: {}, resources: {} } });
|
|
5422
|
+
this.server = new Server({ name: "@gethmy/mcp", version: "2.0.0" }, { capabilities: { tools: {}, resources: { listChanged: true } } });
|
|
4906
5423
|
registerHandlers(this.server, createConfigDeps());
|
|
4907
5424
|
}
|
|
4908
|
-
async run() {
|
|
5425
|
+
async run(opts = {}) {
|
|
4909
5426
|
const transport = new StdioServerTransport;
|
|
4910
5427
|
await this.server.connect(transport);
|
|
4911
5428
|
console.error("Harmony MCP server running on stdio");
|
|
5429
|
+
if (opts.skillsUpdated) {
|
|
5430
|
+
this.server.sendResourceListChanged().catch(() => {});
|
|
5431
|
+
}
|
|
4912
5432
|
const configDeps = createConfigDeps();
|
|
4913
5433
|
initAutoSession(async (client3, cardId, status) => {
|
|
4914
5434
|
await runEndSessionPipeline(client3, configDeps, cardId, status);
|