@gethmy/mcp 2.8.0 → 2.8.1
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 +393 -276
- package/dist/index.js +301 -5
- package/package.json +2 -2
- package/src/api-client.ts +1 -0
- package/src/cli.ts +2 -2
- package/src/hmy-config.ts +70 -0
- package/src/server.ts +27 -3
- package/src/skills.ts +115 -19
- package/src/tui/setup.ts +8 -8
package/dist/index.js
CHANGED
|
@@ -2374,6 +2374,294 @@ async function onboardNewUser(params) {
|
|
|
2374
2374
|
};
|
|
2375
2375
|
}
|
|
2376
2376
|
|
|
2377
|
+
// src/skills.ts
|
|
2378
|
+
import {
|
|
2379
|
+
existsSync as existsSync4,
|
|
2380
|
+
mkdirSync as mkdirSync3,
|
|
2381
|
+
readFileSync as readFileSync4,
|
|
2382
|
+
renameSync,
|
|
2383
|
+
writeFileSync as writeFileSync3
|
|
2384
|
+
} from "node:fs";
|
|
2385
|
+
import { homedir as homedir3 } from "node:os";
|
|
2386
|
+
import { dirname, join as join4 } from "node:path";
|
|
2387
|
+
|
|
2388
|
+
// src/hmy-config.ts
|
|
2389
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
2390
|
+
import { homedir as homedir2 } from "node:os";
|
|
2391
|
+
import { join as join3 } from "node:path";
|
|
2392
|
+
var DEFAULTS = { updateCheck: true, pin: null };
|
|
2393
|
+
function getHmyConfigPath() {
|
|
2394
|
+
return join3(homedir2(), ".hmy", "config.yaml");
|
|
2395
|
+
}
|
|
2396
|
+
function loadHmyConfig() {
|
|
2397
|
+
const path = getHmyConfigPath();
|
|
2398
|
+
if (!existsSync3(path))
|
|
2399
|
+
return { ...DEFAULTS };
|
|
2400
|
+
try {
|
|
2401
|
+
return parseHmyConfig(readFileSync3(path, "utf-8"));
|
|
2402
|
+
} catch {
|
|
2403
|
+
return { ...DEFAULTS };
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
function parseHmyConfig(text) {
|
|
2407
|
+
const cfg = { ...DEFAULTS };
|
|
2408
|
+
for (const rawLine of text.split(`
|
|
2409
|
+
`)) {
|
|
2410
|
+
const line = rawLine.trim();
|
|
2411
|
+
if (!line || line.startsWith("#"))
|
|
2412
|
+
continue;
|
|
2413
|
+
const sep2 = line.indexOf(":");
|
|
2414
|
+
if (sep2 === -1)
|
|
2415
|
+
continue;
|
|
2416
|
+
const key = line.slice(0, sep2).trim();
|
|
2417
|
+
let value = line.slice(sep2 + 1).trim();
|
|
2418
|
+
const hash = value.indexOf(" #");
|
|
2419
|
+
if (hash !== -1)
|
|
2420
|
+
value = value.slice(0, hash).trim();
|
|
2421
|
+
value = value.replace(/^["']|["']$/g, "");
|
|
2422
|
+
switch (key) {
|
|
2423
|
+
case "update_check":
|
|
2424
|
+
cfg.updateCheck = value !== "false";
|
|
2425
|
+
break;
|
|
2426
|
+
case "pin":
|
|
2427
|
+
case "pin_version":
|
|
2428
|
+
cfg.pin = value || null;
|
|
2429
|
+
break;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
return cfg;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// src/skills.ts
|
|
2436
|
+
var HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
|
|
2437
|
+
|
|
2438
|
+
Start work on a Harmony card. Card reference: $ARGUMENTS
|
|
2439
|
+
|
|
2440
|
+
## 1. Find & Fetch Card
|
|
2441
|
+
|
|
2442
|
+
Parse the reference and fetch the card:
|
|
2443
|
+
- \`#42\` or \`42\` → \`harmony_get_card_by_short_id\` with \`shortId: 42\`
|
|
2444
|
+
- UUID → \`harmony_get_card\` with \`cardId\`
|
|
2445
|
+
- Name/text → \`harmony_search_cards\` with \`query\`
|
|
2446
|
+
|
|
2447
|
+
## 2. Get Board State
|
|
2448
|
+
|
|
2449
|
+
Call \`harmony_get_board\` to get columns and labels. From the response:
|
|
2450
|
+
- Find the "In Progress" (or "Progress") column ID
|
|
2451
|
+
- Find the "agent" label ID
|
|
2452
|
+
|
|
2453
|
+
## 3. Setup Card for Work
|
|
2454
|
+
|
|
2455
|
+
Execute these in sequence:
|
|
2456
|
+
1. \`harmony_move_card\` → Move to "In Progress" column
|
|
2457
|
+
2. \`harmony_add_label_to_card\` → Add "agent" label
|
|
2458
|
+
3. \`harmony_start_agent_session\`:
|
|
2459
|
+
- \`cardId\`: Card UUID
|
|
2460
|
+
- \`agentIdentifier\`: Your agent identifier
|
|
2461
|
+
- \`agentName\`: Your agent name
|
|
2462
|
+
- \`currentTask\`: "Analyzing card requirements"
|
|
2463
|
+
|
|
2464
|
+
## 4. Generate Work Prompt
|
|
2465
|
+
|
|
2466
|
+
Call \`harmony_generate_prompt\` with:
|
|
2467
|
+
- \`cardId\` or \`shortId\` (+ \`projectId\` if using shortId)
|
|
2468
|
+
- \`variant\`: Select based on task:
|
|
2469
|
+
- \`"execute"\` (default) → Clear tasks, bug fixes, well-defined work
|
|
2470
|
+
- \`"analysis"\` → Complex features, unclear requirements
|
|
2471
|
+
- \`"draft"\` → Medium complexity, want feedback first
|
|
2472
|
+
|
|
2473
|
+
The generated prompt provides role framing, focus areas, subtasks, linked cards, and suggested outputs.
|
|
2474
|
+
|
|
2475
|
+
## 5. Display Card Summary
|
|
2476
|
+
|
|
2477
|
+
Show the user: Card title, short ID, role, priority, labels, due date, description, and subtasks.
|
|
2478
|
+
|
|
2479
|
+
## 6. Implement Solution
|
|
2480
|
+
|
|
2481
|
+
Work on the card following the generated prompt's guidance. Update progress at milestones:
|
|
2482
|
+
- \`harmony_update_agent_progress\` with \`progressPercent\` (0-100), \`currentTask\`, \`status\`, \`blockers\`
|
|
2483
|
+
|
|
2484
|
+
**Progress checkpoints:** 20% (exploration), 50% (implementation), 80% (testing), 100% (done)
|
|
2485
|
+
|
|
2486
|
+
## 7. Complete Work
|
|
2487
|
+
|
|
2488
|
+
When finished:
|
|
2489
|
+
1. \`harmony_end_agent_session\` with \`status: "completed"\`, \`progressPercent: 100\`
|
|
2490
|
+
2. \`harmony_move_card\` to "Review" column
|
|
2491
|
+
3. Summarize accomplishments
|
|
2492
|
+
|
|
2493
|
+
If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
|
|
2494
|
+
|
|
2495
|
+
## Key Tools Reference
|
|
2496
|
+
|
|
2497
|
+
**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\`
|
|
2498
|
+
|
|
2499
|
+
**Subtasks:** \`harmony_create_subtask\`, \`harmony_toggle_subtask\`, \`harmony_delete_subtask\`
|
|
2500
|
+
|
|
2501
|
+
**Labels:** \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\`, \`harmony_create_label\`
|
|
2502
|
+
|
|
2503
|
+
**Links:** \`harmony_add_link_to_card\`, \`harmony_remove_link_from_card\`, \`harmony_get_card_links\`
|
|
2504
|
+
|
|
2505
|
+
**Board:** \`harmony_get_board\`, \`harmony_list_projects\`, \`harmony_get_context\`, \`harmony_set_project_context\`
|
|
2506
|
+
|
|
2507
|
+
**Sessions:** \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\`, \`harmony_get_agent_session\`
|
|
2508
|
+
|
|
2509
|
+
**AI:** \`harmony_generate_prompt\`, \`harmony_process_command\`
|
|
2510
|
+
`;
|
|
2511
|
+
function buildSkillFile(skill) {
|
|
2512
|
+
const content = stripSkillPreamble(skill.content);
|
|
2513
|
+
if (skill.skillVersion !== undefined && !hasMetadataVersion(content)) {
|
|
2514
|
+
return injectMetadataVersion(content, skill.skillVersion);
|
|
2515
|
+
}
|
|
2516
|
+
return content;
|
|
2517
|
+
}
|
|
2518
|
+
var PREAMBLE_START = "<!-- hmy-skills-preamble:start -->";
|
|
2519
|
+
var PREAMBLE_END = "<!-- hmy-skills-preamble:end -->";
|
|
2520
|
+
function stripSkillPreamble(content) {
|
|
2521
|
+
const start = content.indexOf(PREAMBLE_START);
|
|
2522
|
+
const end = content.indexOf(PREAMBLE_END);
|
|
2523
|
+
if (start === -1 || end === -1 || end < start)
|
|
2524
|
+
return content;
|
|
2525
|
+
const stripped = content.slice(0, start) + content.slice(end + PREAMBLE_END.length);
|
|
2526
|
+
return `${stripped.replace(/\n{3,}/g, `
|
|
2527
|
+
|
|
2528
|
+
`).replace(/\s+$/, "")}
|
|
2529
|
+
`;
|
|
2530
|
+
}
|
|
2531
|
+
function atomicWrite(filePath, content) {
|
|
2532
|
+
const dir = dirname(filePath);
|
|
2533
|
+
if (!existsSync4(dir))
|
|
2534
|
+
mkdirSync3(dir, { recursive: true });
|
|
2535
|
+
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
2536
|
+
writeFileSync3(tmp, content);
|
|
2537
|
+
renameSync(tmp, filePath);
|
|
2538
|
+
}
|
|
2539
|
+
function hasMetadataVersion(content) {
|
|
2540
|
+
return parseSkillVersion(content) !== null;
|
|
2541
|
+
}
|
|
2542
|
+
function injectMetadataVersion(content, version) {
|
|
2543
|
+
const fmMatch = content.match(/^(---\n[\s\S]*?\n)(---\n)([\s\S]*)$/);
|
|
2544
|
+
if (!fmMatch)
|
|
2545
|
+
return content;
|
|
2546
|
+
const [, head, close, body] = fmMatch;
|
|
2547
|
+
const block = `metadata:
|
|
2548
|
+
version: "${version}"
|
|
2549
|
+
`;
|
|
2550
|
+
return `${head}${block}${close}${body}`;
|
|
2551
|
+
}
|
|
2552
|
+
function parseSkillVersion(content) {
|
|
2553
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2554
|
+
if (fmMatch) {
|
|
2555
|
+
const fm = fmMatch[1];
|
|
2556
|
+
const verMatch = fm.match(/^metadata:[\s\S]*?\n[ \t]+version:[ \t]*["']?(\d+)["']?\s*$/m);
|
|
2557
|
+
if (verMatch)
|
|
2558
|
+
return verMatch[1];
|
|
2559
|
+
}
|
|
2560
|
+
const legacy = content.match(/<!-- skills-version:(\d+) -->/);
|
|
2561
|
+
return legacy ? legacy[1] : null;
|
|
2562
|
+
}
|
|
2563
|
+
function findSkillFiles(paths, knownNames) {
|
|
2564
|
+
const results = [];
|
|
2565
|
+
for (const filePath of paths) {
|
|
2566
|
+
if (!existsSync4(filePath))
|
|
2567
|
+
continue;
|
|
2568
|
+
for (const name of knownNames) {
|
|
2569
|
+
if (filePath.includes(`/${name}/`) || filePath.includes(`/${name}.md`)) {
|
|
2570
|
+
results.push({ name, filePath });
|
|
2571
|
+
break;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
return results;
|
|
2576
|
+
}
|
|
2577
|
+
var HMY_DIR = join4(homedir3(), ".hmy");
|
|
2578
|
+
var HMY_VERSION_FILE = join4(HMY_DIR, "VERSION");
|
|
2579
|
+
var LAST_CHECK_FILE = join4(HMY_DIR, "last-update-check");
|
|
2580
|
+
var CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
2581
|
+
function checkedRecently(now = Date.now()) {
|
|
2582
|
+
try {
|
|
2583
|
+
if (!existsSync4(LAST_CHECK_FILE))
|
|
2584
|
+
return false;
|
|
2585
|
+
const ts = Number.parseInt(readFileSync4(LAST_CHECK_FILE, "utf-8").trim(), 10);
|
|
2586
|
+
if (!Number.isFinite(ts))
|
|
2587
|
+
return false;
|
|
2588
|
+
return now - ts < CHECK_TTL_MS;
|
|
2589
|
+
} catch {
|
|
2590
|
+
return false;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
function recordCheck(now = Date.now()) {
|
|
2594
|
+
try {
|
|
2595
|
+
if (!existsSync4(HMY_DIR))
|
|
2596
|
+
mkdirSync3(HMY_DIR, { recursive: true });
|
|
2597
|
+
writeFileSync3(LAST_CHECK_FILE, String(now));
|
|
2598
|
+
} catch {}
|
|
2599
|
+
}
|
|
2600
|
+
async function refreshSkills(opts = {}) {
|
|
2601
|
+
try {
|
|
2602
|
+
if (!isConfigured())
|
|
2603
|
+
return { updated: false };
|
|
2604
|
+
const cfg = loadHmyConfig();
|
|
2605
|
+
if (!cfg.updateCheck || cfg.pin)
|
|
2606
|
+
return { updated: false };
|
|
2607
|
+
if (!opts.force && checkedRecently())
|
|
2608
|
+
return { updated: false };
|
|
2609
|
+
const status = areSkillsInstalled();
|
|
2610
|
+
if (!status.installed)
|
|
2611
|
+
return { updated: false };
|
|
2612
|
+
const client3 = new HarmonyApiClient;
|
|
2613
|
+
const versionInfo = await client3.fetchSkillsVersion();
|
|
2614
|
+
recordCheck();
|
|
2615
|
+
const skillFiles = findSkillFiles(status.paths, versionInfo.skills);
|
|
2616
|
+
if (skillFiles.length > 0) {
|
|
2617
|
+
const samplePath = skillFiles[0].filePath;
|
|
2618
|
+
for (const name of versionInfo.skills) {
|
|
2619
|
+
if (skillFiles.some((sf) => sf.name === name))
|
|
2620
|
+
continue;
|
|
2621
|
+
let siblingPath;
|
|
2622
|
+
if (samplePath.endsWith("SKILL.md")) {
|
|
2623
|
+
const parentDir = dirname(dirname(samplePath));
|
|
2624
|
+
siblingPath = `${parentDir}/${name}/SKILL.md`;
|
|
2625
|
+
} else {
|
|
2626
|
+
const parentDir = dirname(samplePath);
|
|
2627
|
+
siblingPath = `${parentDir}/${name}.md`;
|
|
2628
|
+
}
|
|
2629
|
+
if (existsSync4(siblingPath)) {
|
|
2630
|
+
skillFiles.push({ name, filePath: siblingPath });
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
if (skillFiles.length === 0)
|
|
2635
|
+
return { updated: false };
|
|
2636
|
+
let updated = false;
|
|
2637
|
+
for (const { name, filePath } of skillFiles) {
|
|
2638
|
+
try {
|
|
2639
|
+
const currentContent = readFileSync4(filePath, "utf-8");
|
|
2640
|
+
const localVersion = parseSkillVersion(currentContent);
|
|
2641
|
+
const fetched = await client3.fetchSkill(name);
|
|
2642
|
+
const remoteVersion = fetched.skillVersion;
|
|
2643
|
+
if (remoteVersion !== undefined && localVersion !== null && Number(localVersion) >= remoteVersion) {
|
|
2644
|
+
continue;
|
|
2645
|
+
}
|
|
2646
|
+
atomicWrite(filePath, buildSkillFile(fetched));
|
|
2647
|
+
updated = true;
|
|
2648
|
+
} catch (err) {
|
|
2649
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2650
|
+
console.error(`Harmony: skill "${name}" refresh failed: ${msg}`);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
if (updated) {
|
|
2654
|
+
try {
|
|
2655
|
+
atomicWrite(HMY_VERSION_FILE, versionInfo.version);
|
|
2656
|
+
} catch {}
|
|
2657
|
+
console.error("Harmony: Refreshed skills from server");
|
|
2658
|
+
}
|
|
2659
|
+
return { updated };
|
|
2660
|
+
} catch {
|
|
2661
|
+
return { updated: false };
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2377
2665
|
// src/server.ts
|
|
2378
2666
|
var memorySessions = new Map;
|
|
2379
2667
|
function parseLabelList(raw) {
|
|
@@ -2511,7 +2799,11 @@ var TOOLS = {
|
|
|
2511
2799
|
enum: ["low", "medium", "high", "urgent"],
|
|
2512
2800
|
description: "Priority level"
|
|
2513
2801
|
},
|
|
2514
|
-
assigneeId: { type: "string", description: "Assignee user ID" }
|
|
2802
|
+
assigneeId: { type: "string", description: "Assignee user ID" },
|
|
2803
|
+
planId: {
|
|
2804
|
+
type: "string",
|
|
2805
|
+
description: "Plan ID to link this card to (optional). Links the card to that plan via its plan_id."
|
|
2806
|
+
}
|
|
2515
2807
|
},
|
|
2516
2808
|
required: ["title"]
|
|
2517
2809
|
}
|
|
@@ -3826,7 +4118,7 @@ function registerHandlers(server, deps) {
|
|
|
3826
4118
|
{
|
|
3827
4119
|
uri,
|
|
3828
4120
|
mimeType: "text/markdown",
|
|
3829
|
-
text: fetched.content
|
|
4121
|
+
text: stripSkillPreamble(fetched.content)
|
|
3830
4122
|
}
|
|
3831
4123
|
]
|
|
3832
4124
|
};
|
|
@@ -3876,7 +4168,8 @@ async function handleToolCall(name, args, deps) {
|
|
|
3876
4168
|
columnId: args.columnId,
|
|
3877
4169
|
description: args.description,
|
|
3878
4170
|
priority: args.priority,
|
|
3879
|
-
assigneeId: args.assigneeId
|
|
4171
|
+
assigneeId: args.assigneeId,
|
|
4172
|
+
planId: args.planId
|
|
3880
4173
|
});
|
|
3881
4174
|
return { success: true, ...result };
|
|
3882
4175
|
}
|
|
@@ -4902,13 +5195,16 @@ function createConfigDeps() {
|
|
|
4902
5195
|
class HarmonyMCPServer {
|
|
4903
5196
|
server;
|
|
4904
5197
|
constructor() {
|
|
4905
|
-
this.server = new Server({ name: "@gethmy/mcp", version: "2.0.0" }, { capabilities: { tools: {}, resources: {} } });
|
|
5198
|
+
this.server = new Server({ name: "@gethmy/mcp", version: "2.0.0" }, { capabilities: { tools: {}, resources: { listChanged: true } } });
|
|
4906
5199
|
registerHandlers(this.server, createConfigDeps());
|
|
4907
5200
|
}
|
|
4908
|
-
async run() {
|
|
5201
|
+
async run(opts = {}) {
|
|
4909
5202
|
const transport = new StdioServerTransport;
|
|
4910
5203
|
await this.server.connect(transport);
|
|
4911
5204
|
console.error("Harmony MCP server running on stdio");
|
|
5205
|
+
if (opts.skillsUpdated) {
|
|
5206
|
+
this.server.sendResourceListChanged().catch(() => {});
|
|
5207
|
+
}
|
|
4912
5208
|
const configDeps = createConfigDeps();
|
|
4913
5209
|
initAutoSession(async (client3, cardId, status) => {
|
|
4914
5210
|
await runEndSessionPipeline(client3, configDeps, cardId, status);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"serve:remote": "bun src/remote.ts",
|
|
61
61
|
"dev": "bun --watch src/index.ts",
|
|
62
62
|
"test": "bun run test:unit && bun run test:integration",
|
|
63
|
-
"test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts src/__tests__/skills.test.ts src/__tests__/tool-dispatch.test.ts src/__tests__/mcp-integration.test.ts",
|
|
63
|
+
"test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts src/__tests__/skills.test.ts src/__tests__/hmy-config.test.ts src/__tests__/tool-dispatch.test.ts src/__tests__/mcp-integration.test.ts",
|
|
64
64
|
"test:integration": "bun test src/__tests__/integration-memory-system.test.ts src/__tests__/integration-memory-crud.test.ts",
|
|
65
65
|
"typecheck": "tsc --noEmit",
|
|
66
66
|
"prepublishOnly": "bun run build"
|
package/src/api-client.ts
CHANGED
package/src/cli.ts
CHANGED
|
@@ -34,9 +34,9 @@ program
|
|
|
34
34
|
console.error("Run: npx @gethmy/mcp setup");
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
|
-
await refreshSkills();
|
|
37
|
+
const { updated } = await refreshSkills();
|
|
38
38
|
const server = new HarmonyMCPServer();
|
|
39
|
-
await server.run();
|
|
39
|
+
await server.run({ skillsUpdated: updated });
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
program
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* User-facing knobs for the skill auto-update, stored at `~/.hmy/config.yaml`.
|
|
7
|
+
*
|
|
8
|
+
* This file used to be read only by the bash auto-update preamble. Now that
|
|
9
|
+
* the MCP server owns auto-update (refreshSkills at `serve` startup), the
|
|
10
|
+
* server reads it directly. We intentionally avoid a YAML dependency: the file
|
|
11
|
+
* is a flat `key: value` list, so a tiny line parser is enough and keeps the
|
|
12
|
+
* install footprint small.
|
|
13
|
+
*/
|
|
14
|
+
export interface HmyConfig {
|
|
15
|
+
/** Master switch. `update_check: false` disables auto-update entirely. */
|
|
16
|
+
updateCheck: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Freeze to a specific skills version. When set, the server skips
|
|
19
|
+
* auto-update so the user stays on whatever is currently installed.
|
|
20
|
+
*/
|
|
21
|
+
pin: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULTS: HmyConfig = { updateCheck: true, pin: null };
|
|
25
|
+
|
|
26
|
+
export function getHmyConfigPath(): string {
|
|
27
|
+
return join(homedir(), ".hmy", "config.yaml");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Read `~/.hmy/config.yaml`, falling back to defaults on any error. */
|
|
31
|
+
export function loadHmyConfig(): HmyConfig {
|
|
32
|
+
const path = getHmyConfigPath();
|
|
33
|
+
if (!existsSync(path)) return { ...DEFAULTS };
|
|
34
|
+
try {
|
|
35
|
+
return parseHmyConfig(readFileSync(path, "utf-8"));
|
|
36
|
+
} catch {
|
|
37
|
+
return { ...DEFAULTS };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse the flat `key: value` config body. Unknown keys are ignored;
|
|
43
|
+
* surrounding quotes and inline comments after a `#` are stripped. Exported
|
|
44
|
+
* for tests.
|
|
45
|
+
*/
|
|
46
|
+
export function parseHmyConfig(text: string): HmyConfig {
|
|
47
|
+
const cfg: HmyConfig = { ...DEFAULTS };
|
|
48
|
+
for (const rawLine of text.split("\n")) {
|
|
49
|
+
const line = rawLine.trim();
|
|
50
|
+
if (!line || line.startsWith("#")) continue;
|
|
51
|
+
const sep = line.indexOf(":");
|
|
52
|
+
if (sep === -1) continue;
|
|
53
|
+
const key = line.slice(0, sep).trim();
|
|
54
|
+
let value = line.slice(sep + 1).trim();
|
|
55
|
+
// Drop trailing inline comment, then surrounding quotes.
|
|
56
|
+
const hash = value.indexOf(" #");
|
|
57
|
+
if (hash !== -1) value = value.slice(0, hash).trim();
|
|
58
|
+
value = value.replace(/^["']|["']$/g, "");
|
|
59
|
+
switch (key) {
|
|
60
|
+
case "update_check":
|
|
61
|
+
cfg.updateCheck = value !== "false";
|
|
62
|
+
break;
|
|
63
|
+
case "pin":
|
|
64
|
+
case "pin_version":
|
|
65
|
+
cfg.pin = value || null;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return cfg;
|
|
70
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
sessionScopeFor,
|
|
49
49
|
} from "./memory-session.js";
|
|
50
50
|
import { onboardNewUser } from "./onboard.js";
|
|
51
|
+
import { stripSkillPreamble } from "./skills.js";
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Dependencies injected into tool handlers.
|
|
@@ -286,6 +287,11 @@ export const TOOLS = {
|
|
|
286
287
|
description: "Priority level",
|
|
287
288
|
},
|
|
288
289
|
assigneeId: { type: "string", description: "Assignee user ID" },
|
|
290
|
+
planId: {
|
|
291
|
+
type: "string",
|
|
292
|
+
description:
|
|
293
|
+
"Plan ID to link this card to (optional). Links the card to that plan via its plan_id.",
|
|
294
|
+
},
|
|
289
295
|
},
|
|
290
296
|
required: ["title"],
|
|
291
297
|
},
|
|
@@ -1781,7 +1787,9 @@ export function registerHandlers(server: Server, deps: ToolDeps): void {
|
|
|
1781
1787
|
{
|
|
1782
1788
|
uri,
|
|
1783
1789
|
mimeType: "text/markdown",
|
|
1784
|
-
|
|
1790
|
+
// Strip the legacy auto-update bash block if a stale render still
|
|
1791
|
+
// carries it — the agent shouldn't be handed curl→chmod→exec.
|
|
1792
|
+
text: stripSkillPreamble(fetched.content),
|
|
1785
1793
|
},
|
|
1786
1794
|
],
|
|
1787
1795
|
};
|
|
@@ -1863,6 +1871,7 @@ async function handleToolCall(
|
|
|
1863
1871
|
description: args.description as string | undefined,
|
|
1864
1872
|
priority: args.priority as string | undefined,
|
|
1865
1873
|
assigneeId: args.assigneeId as string | undefined,
|
|
1874
|
+
planId: args.planId as string | undefined,
|
|
1866
1875
|
});
|
|
1867
1876
|
return { success: true, ...result };
|
|
1868
1877
|
}
|
|
@@ -3383,17 +3392,32 @@ export class HarmonyMCPServer {
|
|
|
3383
3392
|
constructor() {
|
|
3384
3393
|
this.server = new Server(
|
|
3385
3394
|
{ name: "@gethmy/mcp", version: "2.0.0" },
|
|
3386
|
-
|
|
3395
|
+
// resources.listChanged lets us notify the client when a startup skill
|
|
3396
|
+
// refresh rewrites SKILL.md files, so it re-reads them this session.
|
|
3397
|
+
{ capabilities: { tools: {}, resources: { listChanged: true } } },
|
|
3387
3398
|
);
|
|
3388
3399
|
|
|
3389
3400
|
registerHandlers(this.server, createConfigDeps());
|
|
3390
3401
|
}
|
|
3391
3402
|
|
|
3392
|
-
|
|
3403
|
+
/**
|
|
3404
|
+
* @param opts.skillsUpdated Set when the pre-connect skill refresh
|
|
3405
|
+
* (cli.ts) rewrote files. Triggers a resources/list_changed notification
|
|
3406
|
+
* once the transport is connected so the client picks up the new skills.
|
|
3407
|
+
*/
|
|
3408
|
+
async run(opts: { skillsUpdated?: boolean } = {}) {
|
|
3393
3409
|
const transport = new StdioServerTransport();
|
|
3394
3410
|
await this.server.connect(transport);
|
|
3395
3411
|
console.error("Harmony MCP server running on stdio");
|
|
3396
3412
|
|
|
3413
|
+
if (opts.skillsUpdated) {
|
|
3414
|
+
// Client is connected now — tell it the skill resources changed.
|
|
3415
|
+
this.server.sendResourceListChanged().catch(() => {
|
|
3416
|
+
// Best-effort: a missed notification just means the new skills load
|
|
3417
|
+
// on the next session instead of this one.
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3397
3421
|
// Initialize auto-session tracking with MCP client identity detection
|
|
3398
3422
|
const configDeps = createConfigDeps();
|
|
3399
3423
|
initAutoSession(
|