@gethmy/mcp 2.7.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 +29 -2
- 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/cli.js
CHANGED
|
@@ -2378,6 +2378,294 @@ async function onboardNewUser(params) {
|
|
|
2378
2378
|
};
|
|
2379
2379
|
}
|
|
2380
2380
|
|
|
2381
|
+
// src/skills.ts
|
|
2382
|
+
import {
|
|
2383
|
+
existsSync as existsSync4,
|
|
2384
|
+
mkdirSync as mkdirSync3,
|
|
2385
|
+
readFileSync as readFileSync4,
|
|
2386
|
+
renameSync,
|
|
2387
|
+
writeFileSync as writeFileSync3
|
|
2388
|
+
} from "node:fs";
|
|
2389
|
+
import { homedir as homedir3 } from "node:os";
|
|
2390
|
+
import { dirname, join as join4 } from "node:path";
|
|
2391
|
+
|
|
2392
|
+
// src/hmy-config.ts
|
|
2393
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
2394
|
+
import { homedir as homedir2 } from "node:os";
|
|
2395
|
+
import { join as join3 } from "node:path";
|
|
2396
|
+
var DEFAULTS = { updateCheck: true, pin: null };
|
|
2397
|
+
function getHmyConfigPath() {
|
|
2398
|
+
return join3(homedir2(), ".hmy", "config.yaml");
|
|
2399
|
+
}
|
|
2400
|
+
function loadHmyConfig() {
|
|
2401
|
+
const path = getHmyConfigPath();
|
|
2402
|
+
if (!existsSync3(path))
|
|
2403
|
+
return { ...DEFAULTS };
|
|
2404
|
+
try {
|
|
2405
|
+
return parseHmyConfig(readFileSync3(path, "utf-8"));
|
|
2406
|
+
} catch {
|
|
2407
|
+
return { ...DEFAULTS };
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
function parseHmyConfig(text) {
|
|
2411
|
+
const cfg = { ...DEFAULTS };
|
|
2412
|
+
for (const rawLine of text.split(`
|
|
2413
|
+
`)) {
|
|
2414
|
+
const line = rawLine.trim();
|
|
2415
|
+
if (!line || line.startsWith("#"))
|
|
2416
|
+
continue;
|
|
2417
|
+
const sep2 = line.indexOf(":");
|
|
2418
|
+
if (sep2 === -1)
|
|
2419
|
+
continue;
|
|
2420
|
+
const key = line.slice(0, sep2).trim();
|
|
2421
|
+
let value = line.slice(sep2 + 1).trim();
|
|
2422
|
+
const hash = value.indexOf(" #");
|
|
2423
|
+
if (hash !== -1)
|
|
2424
|
+
value = value.slice(0, hash).trim();
|
|
2425
|
+
value = value.replace(/^["']|["']$/g, "");
|
|
2426
|
+
switch (key) {
|
|
2427
|
+
case "update_check":
|
|
2428
|
+
cfg.updateCheck = value !== "false";
|
|
2429
|
+
break;
|
|
2430
|
+
case "pin":
|
|
2431
|
+
case "pin_version":
|
|
2432
|
+
cfg.pin = value || null;
|
|
2433
|
+
break;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
return cfg;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
// src/skills.ts
|
|
2440
|
+
var HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
|
|
2441
|
+
|
|
2442
|
+
Start work on a Harmony card. Card reference: $ARGUMENTS
|
|
2443
|
+
|
|
2444
|
+
## 1. Find & Fetch Card
|
|
2445
|
+
|
|
2446
|
+
Parse the reference and fetch the card:
|
|
2447
|
+
- \`#42\` or \`42\` → \`harmony_get_card_by_short_id\` with \`shortId: 42\`
|
|
2448
|
+
- UUID → \`harmony_get_card\` with \`cardId\`
|
|
2449
|
+
- Name/text → \`harmony_search_cards\` with \`query\`
|
|
2450
|
+
|
|
2451
|
+
## 2. Get Board State
|
|
2452
|
+
|
|
2453
|
+
Call \`harmony_get_board\` to get columns and labels. From the response:
|
|
2454
|
+
- Find the "In Progress" (or "Progress") column ID
|
|
2455
|
+
- Find the "agent" label ID
|
|
2456
|
+
|
|
2457
|
+
## 3. Setup Card for Work
|
|
2458
|
+
|
|
2459
|
+
Execute these in sequence:
|
|
2460
|
+
1. \`harmony_move_card\` → Move to "In Progress" column
|
|
2461
|
+
2. \`harmony_add_label_to_card\` → Add "agent" label
|
|
2462
|
+
3. \`harmony_start_agent_session\`:
|
|
2463
|
+
- \`cardId\`: Card UUID
|
|
2464
|
+
- \`agentIdentifier\`: Your agent identifier
|
|
2465
|
+
- \`agentName\`: Your agent name
|
|
2466
|
+
- \`currentTask\`: "Analyzing card requirements"
|
|
2467
|
+
|
|
2468
|
+
## 4. Generate Work Prompt
|
|
2469
|
+
|
|
2470
|
+
Call \`harmony_generate_prompt\` with:
|
|
2471
|
+
- \`cardId\` or \`shortId\` (+ \`projectId\` if using shortId)
|
|
2472
|
+
- \`variant\`: Select based on task:
|
|
2473
|
+
- \`"execute"\` (default) → Clear tasks, bug fixes, well-defined work
|
|
2474
|
+
- \`"analysis"\` → Complex features, unclear requirements
|
|
2475
|
+
- \`"draft"\` → Medium complexity, want feedback first
|
|
2476
|
+
|
|
2477
|
+
The generated prompt provides role framing, focus areas, subtasks, linked cards, and suggested outputs.
|
|
2478
|
+
|
|
2479
|
+
## 5. Display Card Summary
|
|
2480
|
+
|
|
2481
|
+
Show the user: Card title, short ID, role, priority, labels, due date, description, and subtasks.
|
|
2482
|
+
|
|
2483
|
+
## 6. Implement Solution
|
|
2484
|
+
|
|
2485
|
+
Work on the card following the generated prompt's guidance. Update progress at milestones:
|
|
2486
|
+
- \`harmony_update_agent_progress\` with \`progressPercent\` (0-100), \`currentTask\`, \`status\`, \`blockers\`
|
|
2487
|
+
|
|
2488
|
+
**Progress checkpoints:** 20% (exploration), 50% (implementation), 80% (testing), 100% (done)
|
|
2489
|
+
|
|
2490
|
+
## 7. Complete Work
|
|
2491
|
+
|
|
2492
|
+
When finished:
|
|
2493
|
+
1. \`harmony_end_agent_session\` with \`status: "completed"\`, \`progressPercent: 100\`
|
|
2494
|
+
2. \`harmony_move_card\` to "Review" column
|
|
2495
|
+
3. Summarize accomplishments
|
|
2496
|
+
|
|
2497
|
+
If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
|
|
2498
|
+
|
|
2499
|
+
## Key Tools Reference
|
|
2500
|
+
|
|
2501
|
+
**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\`
|
|
2502
|
+
|
|
2503
|
+
**Subtasks:** \`harmony_create_subtask\`, \`harmony_toggle_subtask\`, \`harmony_delete_subtask\`
|
|
2504
|
+
|
|
2505
|
+
**Labels:** \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\`, \`harmony_create_label\`
|
|
2506
|
+
|
|
2507
|
+
**Links:** \`harmony_add_link_to_card\`, \`harmony_remove_link_from_card\`, \`harmony_get_card_links\`
|
|
2508
|
+
|
|
2509
|
+
**Board:** \`harmony_get_board\`, \`harmony_list_projects\`, \`harmony_get_context\`, \`harmony_set_project_context\`
|
|
2510
|
+
|
|
2511
|
+
**Sessions:** \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\`, \`harmony_get_agent_session\`
|
|
2512
|
+
|
|
2513
|
+
**AI:** \`harmony_generate_prompt\`, \`harmony_process_command\`
|
|
2514
|
+
`;
|
|
2515
|
+
function buildSkillFile(skill) {
|
|
2516
|
+
const content = stripSkillPreamble(skill.content);
|
|
2517
|
+
if (skill.skillVersion !== undefined && !hasMetadataVersion(content)) {
|
|
2518
|
+
return injectMetadataVersion(content, skill.skillVersion);
|
|
2519
|
+
}
|
|
2520
|
+
return content;
|
|
2521
|
+
}
|
|
2522
|
+
var PREAMBLE_START = "<!-- hmy-skills-preamble:start -->";
|
|
2523
|
+
var PREAMBLE_END = "<!-- hmy-skills-preamble:end -->";
|
|
2524
|
+
function stripSkillPreamble(content) {
|
|
2525
|
+
const start = content.indexOf(PREAMBLE_START);
|
|
2526
|
+
const end = content.indexOf(PREAMBLE_END);
|
|
2527
|
+
if (start === -1 || end === -1 || end < start)
|
|
2528
|
+
return content;
|
|
2529
|
+
const stripped = content.slice(0, start) + content.slice(end + PREAMBLE_END.length);
|
|
2530
|
+
return `${stripped.replace(/\n{3,}/g, `
|
|
2531
|
+
|
|
2532
|
+
`).replace(/\s+$/, "")}
|
|
2533
|
+
`;
|
|
2534
|
+
}
|
|
2535
|
+
function atomicWrite(filePath, content) {
|
|
2536
|
+
const dir = dirname(filePath);
|
|
2537
|
+
if (!existsSync4(dir))
|
|
2538
|
+
mkdirSync3(dir, { recursive: true });
|
|
2539
|
+
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
2540
|
+
writeFileSync3(tmp, content);
|
|
2541
|
+
renameSync(tmp, filePath);
|
|
2542
|
+
}
|
|
2543
|
+
function hasMetadataVersion(content) {
|
|
2544
|
+
return parseSkillVersion(content) !== null;
|
|
2545
|
+
}
|
|
2546
|
+
function injectMetadataVersion(content, version) {
|
|
2547
|
+
const fmMatch = content.match(/^(---\n[\s\S]*?\n)(---\n)([\s\S]*)$/);
|
|
2548
|
+
if (!fmMatch)
|
|
2549
|
+
return content;
|
|
2550
|
+
const [, head, close, body] = fmMatch;
|
|
2551
|
+
const block = `metadata:
|
|
2552
|
+
version: "${version}"
|
|
2553
|
+
`;
|
|
2554
|
+
return `${head}${block}${close}${body}`;
|
|
2555
|
+
}
|
|
2556
|
+
function parseSkillVersion(content) {
|
|
2557
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2558
|
+
if (fmMatch) {
|
|
2559
|
+
const fm = fmMatch[1];
|
|
2560
|
+
const verMatch = fm.match(/^metadata:[\s\S]*?\n[ \t]+version:[ \t]*["']?(\d+)["']?\s*$/m);
|
|
2561
|
+
if (verMatch)
|
|
2562
|
+
return verMatch[1];
|
|
2563
|
+
}
|
|
2564
|
+
const legacy = content.match(/<!-- skills-version:(\d+) -->/);
|
|
2565
|
+
return legacy ? legacy[1] : null;
|
|
2566
|
+
}
|
|
2567
|
+
function findSkillFiles(paths, knownNames) {
|
|
2568
|
+
const results = [];
|
|
2569
|
+
for (const filePath of paths) {
|
|
2570
|
+
if (!existsSync4(filePath))
|
|
2571
|
+
continue;
|
|
2572
|
+
for (const name of knownNames) {
|
|
2573
|
+
if (filePath.includes(`/${name}/`) || filePath.includes(`/${name}.md`)) {
|
|
2574
|
+
results.push({ name, filePath });
|
|
2575
|
+
break;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
return results;
|
|
2580
|
+
}
|
|
2581
|
+
var HMY_DIR = join4(homedir3(), ".hmy");
|
|
2582
|
+
var HMY_VERSION_FILE = join4(HMY_DIR, "VERSION");
|
|
2583
|
+
var LAST_CHECK_FILE = join4(HMY_DIR, "last-update-check");
|
|
2584
|
+
var CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
2585
|
+
function checkedRecently(now = Date.now()) {
|
|
2586
|
+
try {
|
|
2587
|
+
if (!existsSync4(LAST_CHECK_FILE))
|
|
2588
|
+
return false;
|
|
2589
|
+
const ts = Number.parseInt(readFileSync4(LAST_CHECK_FILE, "utf-8").trim(), 10);
|
|
2590
|
+
if (!Number.isFinite(ts))
|
|
2591
|
+
return false;
|
|
2592
|
+
return now - ts < CHECK_TTL_MS;
|
|
2593
|
+
} catch {
|
|
2594
|
+
return false;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
function recordCheck(now = Date.now()) {
|
|
2598
|
+
try {
|
|
2599
|
+
if (!existsSync4(HMY_DIR))
|
|
2600
|
+
mkdirSync3(HMY_DIR, { recursive: true });
|
|
2601
|
+
writeFileSync3(LAST_CHECK_FILE, String(now));
|
|
2602
|
+
} catch {}
|
|
2603
|
+
}
|
|
2604
|
+
async function refreshSkills(opts = {}) {
|
|
2605
|
+
try {
|
|
2606
|
+
if (!isConfigured())
|
|
2607
|
+
return { updated: false };
|
|
2608
|
+
const cfg = loadHmyConfig();
|
|
2609
|
+
if (!cfg.updateCheck || cfg.pin)
|
|
2610
|
+
return { updated: false };
|
|
2611
|
+
if (!opts.force && checkedRecently())
|
|
2612
|
+
return { updated: false };
|
|
2613
|
+
const status = areSkillsInstalled();
|
|
2614
|
+
if (!status.installed)
|
|
2615
|
+
return { updated: false };
|
|
2616
|
+
const client3 = new HarmonyApiClient;
|
|
2617
|
+
const versionInfo = await client3.fetchSkillsVersion();
|
|
2618
|
+
recordCheck();
|
|
2619
|
+
const skillFiles = findSkillFiles(status.paths, versionInfo.skills);
|
|
2620
|
+
if (skillFiles.length > 0) {
|
|
2621
|
+
const samplePath = skillFiles[0].filePath;
|
|
2622
|
+
for (const name of versionInfo.skills) {
|
|
2623
|
+
if (skillFiles.some((sf) => sf.name === name))
|
|
2624
|
+
continue;
|
|
2625
|
+
let siblingPath;
|
|
2626
|
+
if (samplePath.endsWith("SKILL.md")) {
|
|
2627
|
+
const parentDir = dirname(dirname(samplePath));
|
|
2628
|
+
siblingPath = `${parentDir}/${name}/SKILL.md`;
|
|
2629
|
+
} else {
|
|
2630
|
+
const parentDir = dirname(samplePath);
|
|
2631
|
+
siblingPath = `${parentDir}/${name}.md`;
|
|
2632
|
+
}
|
|
2633
|
+
if (existsSync4(siblingPath)) {
|
|
2634
|
+
skillFiles.push({ name, filePath: siblingPath });
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
if (skillFiles.length === 0)
|
|
2639
|
+
return { updated: false };
|
|
2640
|
+
let updated = false;
|
|
2641
|
+
for (const { name, filePath } of skillFiles) {
|
|
2642
|
+
try {
|
|
2643
|
+
const currentContent = readFileSync4(filePath, "utf-8");
|
|
2644
|
+
const localVersion = parseSkillVersion(currentContent);
|
|
2645
|
+
const fetched = await client3.fetchSkill(name);
|
|
2646
|
+
const remoteVersion = fetched.skillVersion;
|
|
2647
|
+
if (remoteVersion !== undefined && localVersion !== null && Number(localVersion) >= remoteVersion) {
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
atomicWrite(filePath, buildSkillFile(fetched));
|
|
2651
|
+
updated = true;
|
|
2652
|
+
} catch (err) {
|
|
2653
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2654
|
+
console.error(`Harmony: skill "${name}" refresh failed: ${msg}`);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
if (updated) {
|
|
2658
|
+
try {
|
|
2659
|
+
atomicWrite(HMY_VERSION_FILE, versionInfo.version);
|
|
2660
|
+
} catch {}
|
|
2661
|
+
console.error("Harmony: Refreshed skills from server");
|
|
2662
|
+
}
|
|
2663
|
+
return { updated };
|
|
2664
|
+
} catch {
|
|
2665
|
+
return { updated: false };
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2381
2669
|
// src/server.ts
|
|
2382
2670
|
var memorySessions = new Map;
|
|
2383
2671
|
function parseLabelList(raw) {
|
|
@@ -2515,7 +2803,11 @@ var TOOLS = {
|
|
|
2515
2803
|
enum: ["low", "medium", "high", "urgent"],
|
|
2516
2804
|
description: "Priority level"
|
|
2517
2805
|
},
|
|
2518
|
-
assigneeId: { type: "string", description: "Assignee user ID" }
|
|
2806
|
+
assigneeId: { type: "string", description: "Assignee user ID" },
|
|
2807
|
+
planId: {
|
|
2808
|
+
type: "string",
|
|
2809
|
+
description: "Plan ID to link this card to (optional). Links the card to that plan via its plan_id."
|
|
2810
|
+
}
|
|
2519
2811
|
},
|
|
2520
2812
|
required: ["title"]
|
|
2521
2813
|
}
|
|
@@ -3830,7 +4122,7 @@ function registerHandlers(server, deps) {
|
|
|
3830
4122
|
{
|
|
3831
4123
|
uri,
|
|
3832
4124
|
mimeType: "text/markdown",
|
|
3833
|
-
text: fetched.content
|
|
4125
|
+
text: stripSkillPreamble(fetched.content)
|
|
3834
4126
|
}
|
|
3835
4127
|
]
|
|
3836
4128
|
};
|
|
@@ -3880,7 +4172,8 @@ async function handleToolCall(name, args, deps) {
|
|
|
3880
4172
|
columnId: args.columnId,
|
|
3881
4173
|
description: args.description,
|
|
3882
4174
|
priority: args.priority,
|
|
3883
|
-
assigneeId: args.assigneeId
|
|
4175
|
+
assigneeId: args.assigneeId,
|
|
4176
|
+
planId: args.planId
|
|
3884
4177
|
});
|
|
3885
4178
|
return { success: true, ...result };
|
|
3886
4179
|
}
|
|
@@ -4906,13 +5199,16 @@ function createConfigDeps() {
|
|
|
4906
5199
|
class HarmonyMCPServer {
|
|
4907
5200
|
server;
|
|
4908
5201
|
constructor() {
|
|
4909
|
-
this.server = new Server({ name: "@gethmy/mcp", version: "2.0.0" }, { capabilities: { tools: {}, resources: {} } });
|
|
5202
|
+
this.server = new Server({ name: "@gethmy/mcp", version: "2.0.0" }, { capabilities: { tools: {}, resources: { listChanged: true } } });
|
|
4910
5203
|
registerHandlers(this.server, createConfigDeps());
|
|
4911
5204
|
}
|
|
4912
|
-
async run() {
|
|
5205
|
+
async run(opts = {}) {
|
|
4913
5206
|
const transport = new StdioServerTransport;
|
|
4914
5207
|
await this.server.connect(transport);
|
|
4915
5208
|
console.error("Harmony MCP server running on stdio");
|
|
5209
|
+
if (opts.skillsUpdated) {
|
|
5210
|
+
this.server.sendResourceListChanged().catch(() => {});
|
|
5211
|
+
}
|
|
4916
5212
|
const configDeps = createConfigDeps();
|
|
4917
5213
|
initAutoSession(async (client3, cardId, status) => {
|
|
4918
5214
|
await runEndSessionPipeline(client3, configDeps, cardId, status);
|
|
@@ -4959,209 +5255,30 @@ class HarmonyMCPServer {
|
|
|
4959
5255
|
}
|
|
4960
5256
|
}
|
|
4961
5257
|
|
|
4962
|
-
// src/skills.ts
|
|
4963
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4964
|
-
import { dirname } from "node:path";
|
|
4965
|
-
var HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
|
|
4966
|
-
|
|
4967
|
-
Start work on a Harmony card. Card reference: $ARGUMENTS
|
|
4968
|
-
|
|
4969
|
-
## 1. Find & Fetch Card
|
|
4970
|
-
|
|
4971
|
-
Parse the reference and fetch the card:
|
|
4972
|
-
- \`#42\` or \`42\` → \`harmony_get_card_by_short_id\` with \`shortId: 42\`
|
|
4973
|
-
- UUID → \`harmony_get_card\` with \`cardId\`
|
|
4974
|
-
- Name/text → \`harmony_search_cards\` with \`query\`
|
|
4975
|
-
|
|
4976
|
-
## 2. Get Board State
|
|
4977
|
-
|
|
4978
|
-
Call \`harmony_get_board\` to get columns and labels. From the response:
|
|
4979
|
-
- Find the "In Progress" (or "Progress") column ID
|
|
4980
|
-
- Find the "agent" label ID
|
|
4981
|
-
|
|
4982
|
-
## 3. Setup Card for Work
|
|
4983
|
-
|
|
4984
|
-
Execute these in sequence:
|
|
4985
|
-
1. \`harmony_move_card\` → Move to "In Progress" column
|
|
4986
|
-
2. \`harmony_add_label_to_card\` → Add "agent" label
|
|
4987
|
-
3. \`harmony_start_agent_session\`:
|
|
4988
|
-
- \`cardId\`: Card UUID
|
|
4989
|
-
- \`agentIdentifier\`: Your agent identifier
|
|
4990
|
-
- \`agentName\`: Your agent name
|
|
4991
|
-
- \`currentTask\`: "Analyzing card requirements"
|
|
4992
|
-
|
|
4993
|
-
## 4. Generate Work Prompt
|
|
4994
|
-
|
|
4995
|
-
Call \`harmony_generate_prompt\` with:
|
|
4996
|
-
- \`cardId\` or \`shortId\` (+ \`projectId\` if using shortId)
|
|
4997
|
-
- \`variant\`: Select based on task:
|
|
4998
|
-
- \`"execute"\` (default) → Clear tasks, bug fixes, well-defined work
|
|
4999
|
-
- \`"analysis"\` → Complex features, unclear requirements
|
|
5000
|
-
- \`"draft"\` → Medium complexity, want feedback first
|
|
5001
|
-
|
|
5002
|
-
The generated prompt provides role framing, focus areas, subtasks, linked cards, and suggested outputs.
|
|
5003
|
-
|
|
5004
|
-
## 5. Display Card Summary
|
|
5005
|
-
|
|
5006
|
-
Show the user: Card title, short ID, role, priority, labels, due date, description, and subtasks.
|
|
5007
|
-
|
|
5008
|
-
## 6. Implement Solution
|
|
5009
|
-
|
|
5010
|
-
Work on the card following the generated prompt's guidance. Update progress at milestones:
|
|
5011
|
-
- \`harmony_update_agent_progress\` with \`progressPercent\` (0-100), \`currentTask\`, \`status\`, \`blockers\`
|
|
5012
|
-
|
|
5013
|
-
**Progress checkpoints:** 20% (exploration), 50% (implementation), 80% (testing), 100% (done)
|
|
5014
|
-
|
|
5015
|
-
## 7. Complete Work
|
|
5016
|
-
|
|
5017
|
-
When finished:
|
|
5018
|
-
1. \`harmony_end_agent_session\` with \`status: "completed"\`, \`progressPercent: 100\`
|
|
5019
|
-
2. \`harmony_move_card\` to "Review" column
|
|
5020
|
-
3. Summarize accomplishments
|
|
5021
|
-
|
|
5022
|
-
If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
|
|
5023
|
-
|
|
5024
|
-
## Key Tools Reference
|
|
5025
|
-
|
|
5026
|
-
**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\`
|
|
5027
|
-
|
|
5028
|
-
**Subtasks:** \`harmony_create_subtask\`, \`harmony_toggle_subtask\`, \`harmony_delete_subtask\`
|
|
5029
|
-
|
|
5030
|
-
**Labels:** \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\`, \`harmony_create_label\`
|
|
5031
|
-
|
|
5032
|
-
**Links:** \`harmony_add_link_to_card\`, \`harmony_remove_link_from_card\`, \`harmony_get_card_links\`
|
|
5033
|
-
|
|
5034
|
-
**Board:** \`harmony_get_board\`, \`harmony_list_projects\`, \`harmony_get_context\`, \`harmony_set_project_context\`
|
|
5035
|
-
|
|
5036
|
-
**Sessions:** \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\`, \`harmony_get_agent_session\`
|
|
5037
|
-
|
|
5038
|
-
**AI:** \`harmony_generate_prompt\`, \`harmony_process_command\`
|
|
5039
|
-
`;
|
|
5040
|
-
function buildSkillFile(skill) {
|
|
5041
|
-
if (skill.skillVersion !== undefined && !hasMetadataVersion(skill.content)) {
|
|
5042
|
-
return injectMetadataVersion(skill.content, skill.skillVersion);
|
|
5043
|
-
}
|
|
5044
|
-
return skill.content;
|
|
5045
|
-
}
|
|
5046
|
-
function hasMetadataVersion(content) {
|
|
5047
|
-
return parseSkillVersion(content) !== null;
|
|
5048
|
-
}
|
|
5049
|
-
function injectMetadataVersion(content, version) {
|
|
5050
|
-
const fmMatch = content.match(/^(---\n[\s\S]*?\n)(---\n)([\s\S]*)$/);
|
|
5051
|
-
if (!fmMatch)
|
|
5052
|
-
return content;
|
|
5053
|
-
const [, head, close, body] = fmMatch;
|
|
5054
|
-
const block = `metadata:
|
|
5055
|
-
version: "${version}"
|
|
5056
|
-
`;
|
|
5057
|
-
return `${head}${block}${close}${body}`;
|
|
5058
|
-
}
|
|
5059
|
-
function parseSkillVersion(content) {
|
|
5060
|
-
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
5061
|
-
if (fmMatch) {
|
|
5062
|
-
const fm = fmMatch[1];
|
|
5063
|
-
const verMatch = fm.match(/^metadata:[\s\S]*?\n[ \t]+version:[ \t]*["']?(\d+)["']?\s*$/m);
|
|
5064
|
-
if (verMatch)
|
|
5065
|
-
return verMatch[1];
|
|
5066
|
-
}
|
|
5067
|
-
const legacy = content.match(/<!-- skills-version:(\d+) -->/);
|
|
5068
|
-
return legacy ? legacy[1] : null;
|
|
5069
|
-
}
|
|
5070
|
-
function findSkillFiles(paths, knownNames) {
|
|
5071
|
-
const results = [];
|
|
5072
|
-
for (const filePath of paths) {
|
|
5073
|
-
if (!existsSync3(filePath))
|
|
5074
|
-
continue;
|
|
5075
|
-
for (const name of knownNames) {
|
|
5076
|
-
if (filePath.includes(`/${name}/`) || filePath.includes(`/${name}.md`)) {
|
|
5077
|
-
results.push({ name, filePath });
|
|
5078
|
-
break;
|
|
5079
|
-
}
|
|
5080
|
-
}
|
|
5081
|
-
}
|
|
5082
|
-
return results;
|
|
5083
|
-
}
|
|
5084
|
-
async function refreshSkills() {
|
|
5085
|
-
try {
|
|
5086
|
-
if (!isConfigured())
|
|
5087
|
-
return;
|
|
5088
|
-
const status = areSkillsInstalled();
|
|
5089
|
-
if (!status.installed)
|
|
5090
|
-
return;
|
|
5091
|
-
const client3 = new HarmonyApiClient;
|
|
5092
|
-
const versionInfo = await client3.fetchSkillsVersion();
|
|
5093
|
-
const skillFiles = findSkillFiles(status.paths, versionInfo.skills);
|
|
5094
|
-
if (skillFiles.length > 0) {
|
|
5095
|
-
const samplePath = skillFiles[0].filePath;
|
|
5096
|
-
for (const name of versionInfo.skills) {
|
|
5097
|
-
if (skillFiles.some((sf) => sf.name === name))
|
|
5098
|
-
continue;
|
|
5099
|
-
let siblingPath;
|
|
5100
|
-
if (samplePath.endsWith("SKILL.md")) {
|
|
5101
|
-
const parentDir = dirname(dirname(samplePath));
|
|
5102
|
-
siblingPath = `${parentDir}/${name}/SKILL.md`;
|
|
5103
|
-
} else {
|
|
5104
|
-
const parentDir = dirname(samplePath);
|
|
5105
|
-
siblingPath = `${parentDir}/${name}.md`;
|
|
5106
|
-
}
|
|
5107
|
-
if (existsSync3(siblingPath)) {
|
|
5108
|
-
skillFiles.push({ name, filePath: siblingPath });
|
|
5109
|
-
}
|
|
5110
|
-
}
|
|
5111
|
-
}
|
|
5112
|
-
if (skillFiles.length === 0)
|
|
5113
|
-
return;
|
|
5114
|
-
let updated = false;
|
|
5115
|
-
for (const { name, filePath } of skillFiles) {
|
|
5116
|
-
try {
|
|
5117
|
-
const currentContent = readFileSync3(filePath, "utf-8");
|
|
5118
|
-
const localVersion = parseSkillVersion(currentContent);
|
|
5119
|
-
const fetched = await client3.fetchSkill(name);
|
|
5120
|
-
const remoteVersion = fetched.skillVersion;
|
|
5121
|
-
if (remoteVersion !== undefined && localVersion !== null && Number(localVersion) >= remoteVersion) {
|
|
5122
|
-
continue;
|
|
5123
|
-
}
|
|
5124
|
-
const newContent = buildSkillFile(fetched);
|
|
5125
|
-
const dir = dirname(filePath);
|
|
5126
|
-
if (!existsSync3(dir))
|
|
5127
|
-
mkdirSync3(dir, { recursive: true });
|
|
5128
|
-
writeFileSync3(filePath, newContent);
|
|
5129
|
-
updated = true;
|
|
5130
|
-
} catch (err) {
|
|
5131
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
5132
|
-
console.error(`Harmony: skill "${name}" refresh failed: ${msg}`);
|
|
5133
|
-
}
|
|
5134
|
-
}
|
|
5135
|
-
if (updated) {
|
|
5136
|
-
console.error("Harmony: Refreshed skills from server");
|
|
5137
|
-
}
|
|
5138
|
-
} catch {}
|
|
5139
|
-
}
|
|
5140
|
-
|
|
5141
5258
|
// src/tui/setup.ts
|
|
5142
5259
|
import { createHash as createHash3 } from "node:crypto";
|
|
5143
5260
|
import {
|
|
5144
|
-
existsSync as
|
|
5261
|
+
existsSync as existsSync8,
|
|
5145
5262
|
lstatSync,
|
|
5146
5263
|
mkdirSync as mkdirSync5,
|
|
5147
5264
|
symlinkSync,
|
|
5148
5265
|
unlinkSync
|
|
5149
5266
|
} from "node:fs";
|
|
5150
|
-
import { homedir as
|
|
5151
|
-
import { dirname as dirname3, join as
|
|
5267
|
+
import { homedir as homedir6 } from "node:os";
|
|
5268
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
5152
5269
|
import * as p3 from "@clack/prompts";
|
|
5153
5270
|
|
|
5154
5271
|
// src/tui/agents.ts
|
|
5155
|
-
import { existsSync as
|
|
5156
|
-
import { homedir as
|
|
5157
|
-
import { join as
|
|
5272
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
5273
|
+
import { homedir as homedir4 } from "node:os";
|
|
5274
|
+
import { join as join5 } from "node:path";
|
|
5158
5275
|
var AGENT_DEFINITIONS = [
|
|
5159
5276
|
{
|
|
5160
5277
|
id: "claude",
|
|
5161
5278
|
name: "Claude Code",
|
|
5162
5279
|
description: "Anthropic CLI agent",
|
|
5163
5280
|
hint: "/hmy <card>",
|
|
5164
|
-
globalPaths: [
|
|
5281
|
+
globalPaths: [join5(homedir4(), ".claude")],
|
|
5165
5282
|
localPaths: [".claude"]
|
|
5166
5283
|
},
|
|
5167
5284
|
{
|
|
@@ -5169,7 +5286,7 @@ var AGENT_DEFINITIONS = [
|
|
|
5169
5286
|
name: "Codex",
|
|
5170
5287
|
description: "OpenAI coding agent",
|
|
5171
5288
|
hint: "/prompts:hmy <card>",
|
|
5172
|
-
globalPaths: [
|
|
5289
|
+
globalPaths: [join5(homedir4(), ".codex")],
|
|
5173
5290
|
localPaths: ["AGENTS.md"]
|
|
5174
5291
|
},
|
|
5175
5292
|
{
|
|
@@ -5185,20 +5302,20 @@ var AGENT_DEFINITIONS = [
|
|
|
5185
5302
|
name: "Windsurf",
|
|
5186
5303
|
description: "Codeium AI IDE",
|
|
5187
5304
|
hint: "MCP tools available automatically",
|
|
5188
|
-
globalPaths: [
|
|
5305
|
+
globalPaths: [join5(homedir4(), ".codeium", "windsurf")],
|
|
5189
5306
|
localPaths: [".windsurf", ".windsurfrules"]
|
|
5190
5307
|
}
|
|
5191
5308
|
];
|
|
5192
5309
|
function detectAgents(cwd = process.cwd()) {
|
|
5193
5310
|
return AGENT_DEFINITIONS.map((def) => {
|
|
5194
|
-
const globalPath = def.globalPaths.find((p) =>
|
|
5195
|
-
const localPath = def.localPaths.find((p) =>
|
|
5311
|
+
const globalPath = def.globalPaths.find((p) => existsSync5(p)) || null;
|
|
5312
|
+
const localPath = def.localPaths.find((p) => existsSync5(join5(cwd, p))) || null;
|
|
5196
5313
|
return {
|
|
5197
5314
|
id: def.id,
|
|
5198
5315
|
name: def.name,
|
|
5199
5316
|
detected: !!(globalPath || localPath),
|
|
5200
5317
|
globalPath,
|
|
5201
|
-
localPath: localPath ?
|
|
5318
|
+
localPath: localPath ? join5(cwd, localPath) : null,
|
|
5202
5319
|
description: def.description,
|
|
5203
5320
|
hint: def.hint
|
|
5204
5321
|
};
|
|
@@ -5206,8 +5323,8 @@ function detectAgents(cwd = process.cwd()) {
|
|
|
5206
5323
|
}
|
|
5207
5324
|
|
|
5208
5325
|
// src/tui/docs.ts
|
|
5209
|
-
import { existsSync as
|
|
5210
|
-
import { isAbsolute, join as
|
|
5326
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync } from "node:fs";
|
|
5327
|
+
import { isAbsolute, join as join6, resolve, sep as sep2 } from "node:path";
|
|
5211
5328
|
import * as p from "@clack/prompts";
|
|
5212
5329
|
|
|
5213
5330
|
// src/tui/theme.ts
|
|
@@ -5294,14 +5411,14 @@ var IGNORED_DIRS = new Set([
|
|
|
5294
5411
|
]);
|
|
5295
5412
|
function readJson(filePath) {
|
|
5296
5413
|
try {
|
|
5297
|
-
return JSON.parse(
|
|
5414
|
+
return JSON.parse(readFileSync5(filePath, "utf-8"));
|
|
5298
5415
|
} catch {
|
|
5299
5416
|
return null;
|
|
5300
5417
|
}
|
|
5301
5418
|
}
|
|
5302
5419
|
function readText(filePath) {
|
|
5303
5420
|
try {
|
|
5304
|
-
return
|
|
5421
|
+
return readFileSync5(filePath, "utf-8");
|
|
5305
5422
|
} catch {
|
|
5306
5423
|
return null;
|
|
5307
5424
|
}
|
|
@@ -5312,7 +5429,7 @@ function listDirs(dirPath) {
|
|
|
5312
5429
|
if (IGNORED_DIRS.has(entry) || entry.startsWith("."))
|
|
5313
5430
|
return false;
|
|
5314
5431
|
try {
|
|
5315
|
-
return statSync(
|
|
5432
|
+
return statSync(join6(dirPath, entry)).isDirectory();
|
|
5316
5433
|
} catch {
|
|
5317
5434
|
return false;
|
|
5318
5435
|
}
|
|
@@ -5360,25 +5477,25 @@ function describeDir(name) {
|
|
|
5360
5477
|
}
|
|
5361
5478
|
function scanProject(cwd) {
|
|
5362
5479
|
let packageManager = null;
|
|
5363
|
-
if (
|
|
5480
|
+
if (existsSync6(join6(cwd, "bun.lock")) || existsSync6(join6(cwd, "bun.lockb"))) {
|
|
5364
5481
|
packageManager = "bun";
|
|
5365
|
-
} else if (
|
|
5482
|
+
} else if (existsSync6(join6(cwd, "pnpm-lock.yaml"))) {
|
|
5366
5483
|
packageManager = "pnpm";
|
|
5367
|
-
} else if (
|
|
5484
|
+
} else if (existsSync6(join6(cwd, "yarn.lock"))) {
|
|
5368
5485
|
packageManager = "yarn";
|
|
5369
|
-
} else if (
|
|
5486
|
+
} else if (existsSync6(join6(cwd, "package.json"))) {
|
|
5370
5487
|
packageManager = "npm";
|
|
5371
5488
|
}
|
|
5372
|
-
const pkg = readJson(
|
|
5489
|
+
const pkg = readJson(join6(cwd, "package.json"));
|
|
5373
5490
|
const scripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
|
|
5374
5491
|
let language = "unknown";
|
|
5375
|
-
if (
|
|
5492
|
+
if (existsSync6(join6(cwd, "tsconfig.json"))) {
|
|
5376
5493
|
language = "typescript";
|
|
5377
|
-
} else if (
|
|
5494
|
+
} else if (existsSync6(join6(cwd, "go.mod"))) {
|
|
5378
5495
|
language = "go";
|
|
5379
|
-
} else if (
|
|
5496
|
+
} else if (existsSync6(join6(cwd, "Cargo.toml"))) {
|
|
5380
5497
|
language = "rust";
|
|
5381
|
-
} else if (
|
|
5498
|
+
} else if (existsSync6(join6(cwd, "setup.py")) || existsSync6(join6(cwd, "pyproject.toml"))) {
|
|
5382
5499
|
language = "python";
|
|
5383
5500
|
} else if (pkg) {
|
|
5384
5501
|
language = "javascript";
|
|
@@ -5414,7 +5531,7 @@ function scanProject(cwd) {
|
|
|
5414
5531
|
}
|
|
5415
5532
|
}
|
|
5416
5533
|
let linter = null;
|
|
5417
|
-
if (
|
|
5534
|
+
if (existsSync6(join6(cwd, "biome.json")) || existsSync6(join6(cwd, "biome.jsonc"))) {
|
|
5418
5535
|
linter = "biome";
|
|
5419
5536
|
} else {
|
|
5420
5537
|
const eslintFiles = [
|
|
@@ -5429,7 +5546,7 @@ function scanProject(cwd) {
|
|
|
5429
5546
|
"eslint.config.cjs",
|
|
5430
5547
|
"eslint.config.ts"
|
|
5431
5548
|
];
|
|
5432
|
-
if (eslintFiles.some((f) =>
|
|
5549
|
+
if (eslintFiles.some((f) => existsSync6(join6(cwd, f)))) {
|
|
5433
5550
|
linter = "eslint";
|
|
5434
5551
|
} else {
|
|
5435
5552
|
const prettierFiles = [
|
|
@@ -5441,13 +5558,13 @@ function scanProject(cwd) {
|
|
|
5441
5558
|
"prettier.config.js",
|
|
5442
5559
|
"prettier.config.mjs"
|
|
5443
5560
|
];
|
|
5444
|
-
if (prettierFiles.some((f) =>
|
|
5561
|
+
if (prettierFiles.some((f) => existsSync6(join6(cwd, f)))) {
|
|
5445
5562
|
linter = "prettier";
|
|
5446
5563
|
}
|
|
5447
5564
|
}
|
|
5448
5565
|
}
|
|
5449
5566
|
let indentStyle = null;
|
|
5450
|
-
const biome = readJson(
|
|
5567
|
+
const biome = readJson(join6(cwd, "biome.json")) ?? readJson(join6(cwd, "biome.jsonc"));
|
|
5451
5568
|
if (biome) {
|
|
5452
5569
|
const formatter = biome.formatter;
|
|
5453
5570
|
if (formatter) {
|
|
@@ -5457,7 +5574,7 @@ function scanProject(cwd) {
|
|
|
5457
5574
|
}
|
|
5458
5575
|
}
|
|
5459
5576
|
if (!indentStyle) {
|
|
5460
|
-
const editorConfig = readText(
|
|
5577
|
+
const editorConfig = readText(join6(cwd, ".editorconfig"));
|
|
5461
5578
|
if (editorConfig) {
|
|
5462
5579
|
const styleMatch = editorConfig.match(/indent_style\s*=\s*(space|tab)/);
|
|
5463
5580
|
const sizeMatch = editorConfig.match(/indent_size\s*=\s*(\d+)/);
|
|
@@ -5470,13 +5587,13 @@ function scanProject(cwd) {
|
|
|
5470
5587
|
}
|
|
5471
5588
|
}
|
|
5472
5589
|
const dirs = listDirs(cwd);
|
|
5473
|
-
const srcDirs =
|
|
5474
|
-
const monorepo =
|
|
5590
|
+
const srcDirs = existsSync6(join6(cwd, "src")) ? listDirs(join6(cwd, "src")) : [];
|
|
5591
|
+
const monorepo = existsSync6(join6(cwd, "packages")) || existsSync6(join6(cwd, "apps"));
|
|
5475
5592
|
const existingDocs = {
|
|
5476
|
-
agentsMd:
|
|
5477
|
-
claudeMd:
|
|
5478
|
-
docsDir:
|
|
5479
|
-
architectureMd:
|
|
5593
|
+
agentsMd: existsSync6(join6(cwd, "AGENTS.md")),
|
|
5594
|
+
claudeMd: existsSync6(join6(cwd, "CLAUDE.md")),
|
|
5595
|
+
docsDir: existsSync6(join6(cwd, "docs")),
|
|
5596
|
+
architectureMd: existsSync6(join6(cwd, "docs", "architecture.md"))
|
|
5480
5597
|
};
|
|
5481
5598
|
return {
|
|
5482
5599
|
packageManager,
|
|
@@ -5627,9 +5744,9 @@ var VAGUE_STANDARDS = [
|
|
|
5627
5744
|
];
|
|
5628
5745
|
function verifyDocs(cwd) {
|
|
5629
5746
|
const issues = [];
|
|
5630
|
-
const claudeMd = readText(
|
|
5631
|
-
const agentsMd = readText(
|
|
5632
|
-
const pkg = readJson(
|
|
5747
|
+
const claudeMd = readText(join6(cwd, "CLAUDE.md"));
|
|
5748
|
+
const agentsMd = readText(join6(cwd, "AGENTS.md"));
|
|
5749
|
+
const pkg = readJson(join6(cwd, "package.json"));
|
|
5633
5750
|
const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
|
|
5634
5751
|
const projectRoot = resolve(cwd);
|
|
5635
5752
|
if (claudeMd) {
|
|
@@ -5659,7 +5776,7 @@ function verifyDocs(cwd) {
|
|
|
5659
5776
|
continue;
|
|
5660
5777
|
}
|
|
5661
5778
|
importedFiles.push({ ref: refPath, resolved: resolvedPath });
|
|
5662
|
-
if (!
|
|
5779
|
+
if (!existsSync6(resolvedPath)) {
|
|
5663
5780
|
issues.push({
|
|
5664
5781
|
severity: "error",
|
|
5665
5782
|
file: "CLAUDE.md",
|
|
@@ -5795,7 +5912,7 @@ function verifyDocs(cwd) {
|
|
|
5795
5912
|
}
|
|
5796
5913
|
checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
|
|
5797
5914
|
}
|
|
5798
|
-
const archMd = readText(
|
|
5915
|
+
const archMd = readText(join6(cwd, "docs", "architecture.md"));
|
|
5799
5916
|
if (archMd) {
|
|
5800
5917
|
checkBacktickPaths(archMd, "docs/architecture.md", cwd, issues);
|
|
5801
5918
|
}
|
|
@@ -5842,7 +5959,7 @@ function checkBacktickPaths(content, file, cwd, issues) {
|
|
|
5842
5959
|
const resolvedRef = resolve(root, refPath);
|
|
5843
5960
|
if (resolvedRef !== root && !resolvedRef.startsWith(root + sep2))
|
|
5844
5961
|
continue;
|
|
5845
|
-
if (!
|
|
5962
|
+
if (!existsSync6(resolvedRef)) {
|
|
5846
5963
|
issues.push({
|
|
5847
5964
|
severity: "warning",
|
|
5848
5965
|
file,
|
|
@@ -5865,18 +5982,18 @@ async function runDocsStep(cwd) {
|
|
|
5865
5982
|
}
|
|
5866
5983
|
const files = [];
|
|
5867
5984
|
files.push({
|
|
5868
|
-
path:
|
|
5985
|
+
path: join6(cwd, "AGENTS.md"),
|
|
5869
5986
|
content: generateAgentsMd(info, cwd),
|
|
5870
5987
|
type: "text"
|
|
5871
5988
|
});
|
|
5872
5989
|
files.push({
|
|
5873
|
-
path:
|
|
5990
|
+
path: join6(cwd, "CLAUDE.md"),
|
|
5874
5991
|
content: generateClaudeMd(info),
|
|
5875
5992
|
type: "text"
|
|
5876
5993
|
});
|
|
5877
5994
|
if (info.dirs.includes("docs") || info.srcDirs.length > 0) {
|
|
5878
5995
|
files.push({
|
|
5879
|
-
path:
|
|
5996
|
+
path: join6(cwd, "docs", "architecture.md"),
|
|
5880
5997
|
content: generateArchitectureMd(info, cwd),
|
|
5881
5998
|
type: "text"
|
|
5882
5999
|
});
|
|
@@ -5914,21 +6031,21 @@ async function runDocsStep(cwd) {
|
|
|
5914
6031
|
// src/tui/writer.ts
|
|
5915
6032
|
import {
|
|
5916
6033
|
chmodSync,
|
|
5917
|
-
existsSync as
|
|
6034
|
+
existsSync as existsSync7,
|
|
5918
6035
|
mkdirSync as mkdirSync4,
|
|
5919
|
-
readFileSync as
|
|
6036
|
+
readFileSync as readFileSync6,
|
|
5920
6037
|
writeFileSync as writeFileSync4
|
|
5921
6038
|
} from "node:fs";
|
|
5922
|
-
import { homedir as
|
|
6039
|
+
import { homedir as homedir5 } from "node:os";
|
|
5923
6040
|
import { dirname as dirname2 } from "node:path";
|
|
5924
6041
|
import * as p2 from "@clack/prompts";
|
|
5925
6042
|
function ensureDir(dirPath) {
|
|
5926
|
-
if (!
|
|
6043
|
+
if (!existsSync7(dirPath)) {
|
|
5927
6044
|
mkdirSync4(dirPath, { recursive: true, mode: 493 });
|
|
5928
6045
|
}
|
|
5929
6046
|
}
|
|
5930
6047
|
function writeFile(filePath, content, options = {}) {
|
|
5931
|
-
const exists =
|
|
6048
|
+
const exists = existsSync7(filePath);
|
|
5932
6049
|
if (exists && !options.force) {
|
|
5933
6050
|
return { path: filePath, action: "skip" };
|
|
5934
6051
|
}
|
|
@@ -5950,7 +6067,7 @@ function writeFile(filePath, content, options = {}) {
|
|
|
5950
6067
|
}
|
|
5951
6068
|
}
|
|
5952
6069
|
function mergeJsonFile(filePath, updates, options = {}) {
|
|
5953
|
-
const exists =
|
|
6070
|
+
const exists = existsSync7(filePath);
|
|
5954
6071
|
if (!exists) {
|
|
5955
6072
|
try {
|
|
5956
6073
|
ensureDir(dirname2(filePath));
|
|
@@ -5967,7 +6084,7 @@ function mergeJsonFile(filePath, updates, options = {}) {
|
|
|
5967
6084
|
}
|
|
5968
6085
|
}
|
|
5969
6086
|
try {
|
|
5970
|
-
const existing = JSON.parse(
|
|
6087
|
+
const existing = JSON.parse(readFileSync6(filePath, "utf-8"));
|
|
5971
6088
|
if (updates.mcpServers && existing.mcpServers) {
|
|
5972
6089
|
const existingServers = existing.mcpServers;
|
|
5973
6090
|
const updateServers = updates.mcpServers;
|
|
@@ -6000,7 +6117,7 @@ function mergeJsonFile(filePath, updates, options = {}) {
|
|
|
6000
6117
|
}
|
|
6001
6118
|
}
|
|
6002
6119
|
function appendToToml(filePath, section, content, options = {}) {
|
|
6003
|
-
const exists =
|
|
6120
|
+
const exists = existsSync7(filePath);
|
|
6004
6121
|
if (!exists) {
|
|
6005
6122
|
try {
|
|
6006
6123
|
ensureDir(dirname2(filePath));
|
|
@@ -6015,7 +6132,7 @@ function appendToToml(filePath, section, content, options = {}) {
|
|
|
6015
6132
|
}
|
|
6016
6133
|
}
|
|
6017
6134
|
try {
|
|
6018
|
-
const existing =
|
|
6135
|
+
const existing = readFileSync6(filePath, "utf-8");
|
|
6019
6136
|
if (existing.includes(section)) {
|
|
6020
6137
|
if (options.force) {
|
|
6021
6138
|
const updated = existing.replace(new RegExp(`\\[${section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\[|$)`), content.trim() + `
|
|
@@ -6039,7 +6156,7 @@ function appendToToml(filePath, section, content, options = {}) {
|
|
|
6039
6156
|
}
|
|
6040
6157
|
async function writeFilesWithProgress(files, options = {}) {
|
|
6041
6158
|
const results = [];
|
|
6042
|
-
const home =
|
|
6159
|
+
const home = homedir5();
|
|
6043
6160
|
const spinner2 = p2.spinner();
|
|
6044
6161
|
spinner2.start("Writing configuration files...");
|
|
6045
6162
|
for (const file of files) {
|
|
@@ -6076,10 +6193,10 @@ function getWriteSummary(files, options = {}) {
|
|
|
6076
6193
|
const toCreate = [];
|
|
6077
6194
|
const toUpdate = [];
|
|
6078
6195
|
const toSkip = [];
|
|
6079
|
-
const home =
|
|
6196
|
+
const home = homedir5();
|
|
6080
6197
|
for (const file of files) {
|
|
6081
6198
|
const displayPath = formatPath(file.path, home);
|
|
6082
|
-
const exists =
|
|
6199
|
+
const exists = existsSync7(file.path);
|
|
6083
6200
|
if (exists && !options.force) {
|
|
6084
6201
|
toSkip.push(displayPath);
|
|
6085
6202
|
} else if (exists) {
|
|
@@ -6092,7 +6209,7 @@ function getWriteSummary(files, options = {}) {
|
|
|
6092
6209
|
}
|
|
6093
6210
|
|
|
6094
6211
|
// src/tui/setup.ts
|
|
6095
|
-
var GLOBAL_SKILLS_DIR =
|
|
6212
|
+
var GLOBAL_SKILLS_DIR = join7(homedir6(), ".agents", "skills");
|
|
6096
6213
|
var API_URL = "https://app.gethmy.com/api";
|
|
6097
6214
|
async function registerMcpServer() {
|
|
6098
6215
|
try {
|
|
@@ -6106,16 +6223,16 @@ async function registerMcpServer() {
|
|
|
6106
6223
|
}
|
|
6107
6224
|
}
|
|
6108
6225
|
async function writeMcpConfigFallback(home) {
|
|
6109
|
-
const { readFileSync:
|
|
6110
|
-
const settingsPath =
|
|
6226
|
+
const { readFileSync: readFileSync7, writeFileSync: writeFileSync5, mkdirSync: mkdirSync6, existsSync: existsSync9 } = await import("node:fs");
|
|
6227
|
+
const settingsPath = join7(home, ".claude", "settings.json");
|
|
6111
6228
|
const settingsDir = dirname3(settingsPath);
|
|
6112
|
-
if (!
|
|
6229
|
+
if (!existsSync9(settingsDir)) {
|
|
6113
6230
|
mkdirSync6(settingsDir, { recursive: true });
|
|
6114
6231
|
}
|
|
6115
6232
|
let settings = {};
|
|
6116
|
-
if (
|
|
6233
|
+
if (existsSync9(settingsPath)) {
|
|
6117
6234
|
try {
|
|
6118
|
-
settings = JSON.parse(
|
|
6235
|
+
settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
|
|
6119
6236
|
} catch {}
|
|
6120
6237
|
}
|
|
6121
6238
|
const mcpServers = settings.mcpServers || {};
|
|
@@ -6206,7 +6323,7 @@ async function resolveProjectSlug(apiKey, slug) {
|
|
|
6206
6323
|
return { workspaceId: data.workspaceId, projectId: data.projectId };
|
|
6207
6324
|
}
|
|
6208
6325
|
async function getAgentFiles(agentId, cwd, installMode = "global") {
|
|
6209
|
-
const home =
|
|
6326
|
+
const home = homedir6();
|
|
6210
6327
|
const files = [];
|
|
6211
6328
|
const symlinks = [];
|
|
6212
6329
|
switch (agentId) {
|
|
@@ -6221,17 +6338,17 @@ async function getAgentFiles(agentId, cwd, installMode = "global") {
|
|
|
6221
6338
|
const content = buildSkillFile(fetched);
|
|
6222
6339
|
if (installMode === "global") {
|
|
6223
6340
|
files.push({
|
|
6224
|
-
path:
|
|
6341
|
+
path: join7(GLOBAL_SKILLS_DIR, name, "SKILL.md"),
|
|
6225
6342
|
content,
|
|
6226
6343
|
type: "text"
|
|
6227
6344
|
});
|
|
6228
6345
|
symlinks.push({
|
|
6229
|
-
target:
|
|
6230
|
-
link:
|
|
6346
|
+
target: join7(GLOBAL_SKILLS_DIR, name),
|
|
6347
|
+
link: join7(home, ".claude", "skills", name)
|
|
6231
6348
|
});
|
|
6232
6349
|
} else {
|
|
6233
6350
|
files.push({
|
|
6234
|
-
path:
|
|
6351
|
+
path: join7(cwd, ".claude", "skills", name, "SKILL.md"),
|
|
6235
6352
|
content,
|
|
6236
6353
|
type: "text"
|
|
6237
6354
|
});
|
|
@@ -6254,13 +6371,13 @@ ${summary}`);
|
|
|
6254
6371
|
throw new Error(`hmy-update-check integrity check failed: expected ${updateCheckFetched.sha256}, got ${actualHash}`);
|
|
6255
6372
|
}
|
|
6256
6373
|
files.push({
|
|
6257
|
-
path:
|
|
6374
|
+
path: join7(home, ".hmy", "bin", "hmy-update-check"),
|
|
6258
6375
|
content: updateCheckFetched.content,
|
|
6259
6376
|
type: "text",
|
|
6260
6377
|
mode: 493
|
|
6261
6378
|
});
|
|
6262
6379
|
files.push({
|
|
6263
|
-
path:
|
|
6380
|
+
path: join7(home, ".hmy", "VERSION"),
|
|
6264
6381
|
content: versionInfo.version,
|
|
6265
6382
|
type: "text"
|
|
6266
6383
|
});
|
|
@@ -6306,7 +6423,7 @@ Skip if: work was already started with a card reference, or no matching card exi
|
|
|
6306
6423
|
- \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
|
|
6307
6424
|
`;
|
|
6308
6425
|
files.push({
|
|
6309
|
-
path:
|
|
6426
|
+
path: join7(cwd, "AGENTS.md"),
|
|
6310
6427
|
content: agentsContent,
|
|
6311
6428
|
type: "text"
|
|
6312
6429
|
});
|
|
@@ -6323,17 +6440,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "{{card}}").replace("Your agent
|
|
|
6323
6440
|
`;
|
|
6324
6441
|
if (installMode === "global") {
|
|
6325
6442
|
files.push({
|
|
6326
|
-
path:
|
|
6443
|
+
path: join7(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
|
|
6327
6444
|
content: promptContent,
|
|
6328
6445
|
type: "text"
|
|
6329
6446
|
});
|
|
6330
6447
|
symlinks.push({
|
|
6331
|
-
target:
|
|
6332
|
-
link:
|
|
6448
|
+
target: join7(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
|
|
6449
|
+
link: join7(home, ".codex", "prompts", "hmy.md")
|
|
6333
6450
|
});
|
|
6334
6451
|
} else {
|
|
6335
6452
|
files.push({
|
|
6336
|
-
path:
|
|
6453
|
+
path: join7(home, ".codex", "prompts", "hmy.md"),
|
|
6337
6454
|
content: promptContent,
|
|
6338
6455
|
type: "text"
|
|
6339
6456
|
});
|
|
@@ -6345,7 +6462,7 @@ command = "npx"
|
|
|
6345
6462
|
args = ["-y", "@gethmy/mcp@latest", "serve"]
|
|
6346
6463
|
`;
|
|
6347
6464
|
files.push({
|
|
6348
|
-
path:
|
|
6465
|
+
path: join7(home, ".codex", "config.toml"),
|
|
6349
6466
|
content: tomlContent,
|
|
6350
6467
|
type: "toml",
|
|
6351
6468
|
tomlSection: "mcp_servers.harmony"
|
|
@@ -6354,7 +6471,7 @@ args = ["-y", "@gethmy/mcp@latest", "serve"]
|
|
|
6354
6471
|
}
|
|
6355
6472
|
case "cursor": {
|
|
6356
6473
|
files.push({
|
|
6357
|
-
path:
|
|
6474
|
+
path: join7(cwd, ".cursor", "mcp.json"),
|
|
6358
6475
|
content: JSON.stringify({
|
|
6359
6476
|
mcpServers: {
|
|
6360
6477
|
harmony: {
|
|
@@ -6380,17 +6497,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
6380
6497
|
`;
|
|
6381
6498
|
if (installMode === "global") {
|
|
6382
6499
|
files.push({
|
|
6383
|
-
path:
|
|
6500
|
+
path: join7(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
|
|
6384
6501
|
content: ruleContent,
|
|
6385
6502
|
type: "text"
|
|
6386
6503
|
});
|
|
6387
6504
|
symlinks.push({
|
|
6388
|
-
target:
|
|
6389
|
-
link:
|
|
6505
|
+
target: join7(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
|
|
6506
|
+
link: join7(home, ".cursor", "rules", "harmony.mdc")
|
|
6390
6507
|
});
|
|
6391
6508
|
} else {
|
|
6392
6509
|
files.push({
|
|
6393
|
-
path:
|
|
6510
|
+
path: join7(cwd, ".cursor", "rules", "harmony.mdc"),
|
|
6394
6511
|
content: ruleContent,
|
|
6395
6512
|
type: "text"
|
|
6396
6513
|
});
|
|
@@ -6399,7 +6516,7 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
6399
6516
|
}
|
|
6400
6517
|
case "windsurf": {
|
|
6401
6518
|
files.push({
|
|
6402
|
-
path:
|
|
6519
|
+
path: join7(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
6403
6520
|
content: JSON.stringify({
|
|
6404
6521
|
mcpServers: {
|
|
6405
6522
|
harmony: {
|
|
@@ -6425,17 +6542,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
6425
6542
|
`;
|
|
6426
6543
|
if (installMode === "global") {
|
|
6427
6544
|
files.push({
|
|
6428
|
-
path:
|
|
6545
|
+
path: join7(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
|
|
6429
6546
|
content: ruleContent,
|
|
6430
6547
|
type: "text"
|
|
6431
6548
|
});
|
|
6432
6549
|
symlinks.push({
|
|
6433
|
-
target:
|
|
6434
|
-
link:
|
|
6550
|
+
target: join7(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
|
|
6551
|
+
link: join7(home, ".codeium", "windsurf", "rules", "harmony.md")
|
|
6435
6552
|
});
|
|
6436
6553
|
} else {
|
|
6437
6554
|
files.push({
|
|
6438
|
-
path:
|
|
6555
|
+
path: join7(cwd, ".windsurf", "rules", "harmony.md"),
|
|
6439
6556
|
content: ruleContent,
|
|
6440
6557
|
type: "text"
|
|
6441
6558
|
});
|
|
@@ -6447,7 +6564,7 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
6447
6564
|
}
|
|
6448
6565
|
async function runSetup(options = {}) {
|
|
6449
6566
|
const cwd = process.cwd();
|
|
6450
|
-
const home =
|
|
6567
|
+
const home = homedir6();
|
|
6451
6568
|
console.clear();
|
|
6452
6569
|
console.log(messages.header());
|
|
6453
6570
|
const existingConfig = loadConfig();
|
|
@@ -6855,7 +6972,7 @@ async function runSetup(options = {}) {
|
|
|
6855
6972
|
for (const symlink of allSymlinks) {
|
|
6856
6973
|
try {
|
|
6857
6974
|
const linkDir = dirname3(symlink.link);
|
|
6858
|
-
if (!
|
|
6975
|
+
if (!existsSync8(linkDir)) {
|
|
6859
6976
|
mkdirSync5(linkDir, { recursive: true });
|
|
6860
6977
|
}
|
|
6861
6978
|
let linkExists = false;
|
|
@@ -6884,7 +7001,7 @@ async function runSetup(options = {}) {
|
|
|
6884
7001
|
} else {
|
|
6885
7002
|
try {
|
|
6886
7003
|
await writeMcpConfigFallback(home);
|
|
6887
|
-
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(
|
|
7004
|
+
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join7(home, ".claude", "settings.json"), home))} ${colors.dim("(updated)")}`);
|
|
6888
7005
|
} catch {
|
|
6889
7006
|
p3.log.warning("Could not register MCP server. Run manually: claude mcp add --transport stdio harmony -- npx -y @gethmy/mcp@latest serve");
|
|
6890
7007
|
}
|
|
@@ -6951,9 +7068,9 @@ program.command("serve").description("Start the MCP server (stdio transport)").a
|
|
|
6951
7068
|
console.error("Run: npx @gethmy/mcp setup");
|
|
6952
7069
|
process.exit(1);
|
|
6953
7070
|
}
|
|
6954
|
-
await refreshSkills();
|
|
7071
|
+
const { updated } = await refreshSkills();
|
|
6955
7072
|
const server = new HarmonyMCPServer;
|
|
6956
|
-
await server.run();
|
|
7073
|
+
await server.run({ skillsUpdated: updated });
|
|
6957
7074
|
});
|
|
6958
7075
|
program.command("status").description("Show configuration status").action(() => {
|
|
6959
7076
|
const globalConfig = loadConfig();
|