@gethmy/mcp 3.7.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18,8 +18,9 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
20
  // src/config.ts
21
+ import { execFileSync } from "node:child_process";
21
22
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
22
- import { homedir } from "node:os";
23
+ import { homedir, tmpdir } from "node:os";
23
24
  import { dirname, join as join2, parse, resolve } from "node:path";
24
25
  function noteLegacyConfigDir(path) {
25
26
  if (warnedLegacyConfigDir)
@@ -31,11 +32,28 @@ function noteLegacyLocalPin(path) {
31
32
  if (warnedLegacyLocalPin)
32
33
  return;
33
34
  warnedLegacyLocalPin = true;
34
- console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} the fallback that finds it is temporary.`);
35
+ console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Run \`harmony-agent doctor --fix\` in this repo to write ` + `${LOCAL_CONFIG_FILENAME}, or rename the file yourself. ` + `The fallback that finds it is temporary.`);
35
36
  }
36
37
  function noteLocalPinRename(from, to) {
37
38
  console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
38
39
  }
40
+ function noteUntrackedLocalPin(path) {
41
+ if (warnedUntrackedLocalPin)
42
+ return;
43
+ warnedUntrackedLocalPin = true;
44
+ console.error(`Harmony: ${path} is ignored by git, so it will not travel with a branch — ` + `a fresh clone, and every worktree the agent daemon cuts, will not have it. ` + `Commit it if you want it to describe this repo everywhere.`);
45
+ }
46
+ function isGitIgnored(path) {
47
+ try {
48
+ execFileSync("git", ["check-ignore", "--quiet", path], {
49
+ cwd: dirname(path),
50
+ stdio: "ignore"
51
+ });
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
39
57
  function getHmyRootDir() {
40
58
  return join2(homedir(), CONFIG_DIR_NAME);
41
59
  }
@@ -150,19 +168,96 @@ function saveLocalConfig(config, cwd) {
150
168
  if (foundPath !== null && foundPath !== localConfigPath) {
151
169
  noteLocalPinRename(foundPath, localConfigPath);
152
170
  }
153
- const existingConfig = loadLocalConfig(cwd) || {
154
- workspaceId: null,
155
- projectId: null
156
- };
157
- const newConfig = { ...existingConfig, ...config };
158
- const cleanConfig = {};
159
- if (newConfig.workspaceId)
160
- cleanConfig.workspaceId = newConfig.workspaceId;
161
- if (newConfig.projectId)
162
- cleanConfig.projectId = newConfig.projectId;
163
- writeFileSync2(localConfigPath, JSON.stringify(cleanConfig, null, 2));
171
+ const existing = readRawLocalConfig(foundPath ?? localConfigPath);
172
+ const merged = { ...existing };
173
+ if ("workspaceId" in config) {
174
+ if (config.workspaceId)
175
+ merged.workspaceId = config.workspaceId;
176
+ else
177
+ delete merged.workspaceId;
178
+ }
179
+ if ("projectId" in config) {
180
+ if (config.projectId)
181
+ merged.projectId = config.projectId;
182
+ else
183
+ delete merged.projectId;
184
+ }
185
+ writeFileSync2(localConfigPath, `${JSON.stringify(merged, null, localIndent(localConfigPath))}
186
+ `);
187
+ if (isGitIgnored(localConfigPath))
188
+ noteUntrackedLocalPin(localConfigPath);
164
189
  return localConfigPath;
165
190
  }
191
+ function readRawLocalConfig(path) {
192
+ let text;
193
+ try {
194
+ text = readFileSync2(path, "utf-8");
195
+ } catch {
196
+ return {};
197
+ }
198
+ try {
199
+ const parsed = JSON.parse(text);
200
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
201
+ return parsed;
202
+ }
203
+ } catch {}
204
+ noteUnparsableLocalPin(path, text);
205
+ return {};
206
+ }
207
+ function noteUnparsableLocalPin(path, contents) {
208
+ let backup = null;
209
+ try {
210
+ backup = join2(tmpdir(), `hmy-pin-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json.bak`);
211
+ writeFileSync2(backup, contents);
212
+ } catch {
213
+ backup = null;
214
+ }
215
+ console.error(`Harmony: ${path} could not be parsed as JSON, so the pin write REPLACED it. ` + (backup ? `The previous contents are in ${backup}.` : "The previous contents could not be backed up and are gone.") + ` If it carried a "commands" block, re-add it.`);
216
+ }
217
+ function localIndent(configPath) {
218
+ const root = dirname(configPath);
219
+ for (const name of ["biome.json", "biome.jsonc"]) {
220
+ const parsed = readJsonish(join2(root, name));
221
+ if (!parsed || typeof parsed !== "object")
222
+ continue;
223
+ const formatter = parsed.formatter;
224
+ if (formatter?.indentStyle === "space") {
225
+ const width = formatter.indentWidth;
226
+ return typeof width === "number" && width > 0 ? width : 2;
227
+ }
228
+ return "\t";
229
+ }
230
+ const editorconfig = readTextSafely(join2(root, ".editorconfig"));
231
+ if (editorconfig) {
232
+ if (/^\s*indent_style\s*=\s*tab\s*$/im.test(editorconfig))
233
+ return "\t";
234
+ const size = editorconfig.match(/^\s*indent_size\s*=\s*(\d+)\s*$/im);
235
+ if (size) {
236
+ const width = Number(size[1]);
237
+ if (width > 0)
238
+ return width;
239
+ }
240
+ }
241
+ return 2;
242
+ }
243
+ function readJsonish(path) {
244
+ const text = readTextSafely(path);
245
+ if (text === null)
246
+ return null;
247
+ try {
248
+ const stripped = text.replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1");
249
+ return JSON.parse(stripped);
250
+ } catch {
251
+ return null;
252
+ }
253
+ }
254
+ function readTextSafely(path) {
255
+ try {
256
+ return readFileSync2(path, "utf-8");
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
166
261
  function hasLocalConfig(cwd) {
167
262
  return findLocalConfigPath(cwd) !== null;
168
263
  }
@@ -296,7 +391,7 @@ function getMemoryDir() {
296
391
  return config.memoryDir;
297
392
  return join2(homedir(), ".harmony", "memory");
298
393
  }
299
- var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false;
394
+ var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false, warnedUntrackedLocalPin = false;
300
395
  var init_config = () => {};
301
396
 
302
397
  // src/prompt-builder.ts
@@ -726,7 +821,7 @@ var init_prompt_builder = __esm(() => {
726
821
  };
727
822
  VARIANT_INSTRUCTIONS = {
728
823
  analysis: `ANALYSIS MODE: Analyze this task thoroughly. Identify requirements, constraints, edge cases, and potential challenges. Do NOT implement anything yet - focus on understanding and planning.`,
729
- draft: `DRAFT MODE: Create a detailed implementation plan with code structure, key decisions, and approach. Include pseudocode or skeleton code where helpful. This is for review before full implementation.`,
824
+ draft: `DRAFT MODE: Draft the approach for review before implementing. Cover the key decisions with their reasons, the data model and the API contracts, and success criteria a test can check. A short signature or schema sketch is fine wherever an interpretation gap would otherwise remain; function bodies, control flow and test code are not - an implementer transcribes a plan faithfully, defects included.`,
730
825
  execute: `EXECUTE MODE: Implement this task completely. Write production-ready code following best practices. Include necessary tests and documentation.`
731
826
  };
732
827
  });
@@ -1037,7 +1132,7 @@ __export(exports_run_state, {
1037
1132
  RUN_STATE_DIR_ENV: () => RUN_STATE_DIR_ENV,
1038
1133
  MAX_POINTER_AGE_MS: () => MAX_POINTER_AGE_MS
1039
1134
  });
1040
- import { execFileSync } from "node:child_process";
1135
+ import { execFileSync as execFileSync2 } from "node:child_process";
1041
1136
  import {
1042
1137
  existsSync as existsSync3,
1043
1138
  mkdirSync as mkdirSync3,
@@ -1094,7 +1189,7 @@ function psParentTable() {
1094
1189
  return psTableCache;
1095
1190
  const table = new Map;
1096
1191
  try {
1097
- const out = execFileSync("ps", ["-Ao", "pid=,ppid="], {
1192
+ const out = execFileSync2("ps", ["-Ao", "pid=,ppid="], {
1098
1193
  encoding: "utf-8",
1099
1194
  timeout: 2000,
1100
1195
  stdio: ["ignore", "pipe", "ignore"]
@@ -1987,6 +2082,60 @@ var REVIEW_DISALLOWED_TOOLS = [
1987
2082
  "mcp__harmony__harmony_delete_subtask",
1988
2083
  "mcp__harmony__harmony_toggle_subtask"
1989
2084
  ];
2085
+ // ../harmony-shared/dist/runEventSanitize.js
2086
+ var REPLACEMENT = "�";
2087
+ function sanitizeRunEventString(value) {
2088
+ let out = "";
2089
+ for (let i = 0;i < value.length; i++) {
2090
+ const code = value.charCodeAt(i);
2091
+ if (code === 0)
2092
+ continue;
2093
+ if (code >= 55296 && code <= 56319) {
2094
+ const next = value.charCodeAt(i + 1);
2095
+ if (next >= 56320 && next <= 57343) {
2096
+ out += value[i] + value[i + 1];
2097
+ i++;
2098
+ continue;
2099
+ }
2100
+ out += REPLACEMENT;
2101
+ continue;
2102
+ }
2103
+ if (code >= 56320 && code <= 57343) {
2104
+ out += REPLACEMENT;
2105
+ continue;
2106
+ }
2107
+ out += value[i];
2108
+ }
2109
+ return out;
2110
+ }
2111
+ function sanitizeRunEventPayload(payload) {
2112
+ return walk(payload, new Map);
2113
+ }
2114
+ function sanitizeRunEventDraft(draft) {
2115
+ return { ...draft, payload: sanitizeRunEventPayload(draft.payload) };
2116
+ }
2117
+ function walk(value, seen) {
2118
+ if (typeof value === "string")
2119
+ return sanitizeRunEventString(value);
2120
+ if (value === null || typeof value !== "object")
2121
+ return value;
2122
+ const already = seen.get(value);
2123
+ if (already !== undefined)
2124
+ return already;
2125
+ if (Array.isArray(value)) {
2126
+ const out2 = [];
2127
+ seen.set(value, out2);
2128
+ for (const entry of value)
2129
+ out2.push(walk(entry, seen));
2130
+ return out2;
2131
+ }
2132
+ const out = {};
2133
+ seen.set(value, out);
2134
+ for (const [key, entry] of Object.entries(value)) {
2135
+ out[sanitizeRunEventString(key)] = walk(entry, seen);
2136
+ }
2137
+ return out;
2138
+ }
1990
2139
  // ../harmony-shared/dist/runRedaction.js
1991
2140
  var MAX_INPUT_CHARS = 2000;
1992
2141
  var MAX_OUTPUT_CHARS = 4000;
@@ -2507,6 +2656,15 @@ class HarmonyApiClient {
2507
2656
  async registerWorkspaceAgent(workspaceId, data) {
2508
2657
  return this.request("POST", `/workspaces/${workspaceId}/agents`, data);
2509
2658
  }
2659
+ async reportAgentConfig(workspaceId, agentId, config) {
2660
+ return this.request("POST", `/workspaces/${workspaceId}/agents/${agentId}/reported-config`, { config });
2661
+ }
2662
+ async getWorkspaceModelConfig(workspaceId) {
2663
+ return this.request("GET", `/workspaces/${workspaceId}/model-config`);
2664
+ }
2665
+ async getModelCatalog() {
2666
+ return this.request("GET", "/model-catalog");
2667
+ }
2510
2668
  async listProjects(workspaceId) {
2511
2669
  return this.request("GET", `/workspaces/${workspaceId}/projects`);
2512
2670
  }
@@ -2655,6 +2813,9 @@ class HarmonyApiClient {
2655
2813
  title
2656
2814
  });
2657
2815
  }
2816
+ async removeExternalLink(cardId, linkId) {
2817
+ return this.request("DELETE", `/cards/${cardId}/external-links/${linkId}`);
2818
+ }
2658
2819
  async uploadArtifact(data) {
2659
2820
  return this.request("POST", "/artifacts", data);
2660
2821
  }
@@ -2747,7 +2908,10 @@ class HarmonyApiClient {
2747
2908
  return this.request("DELETE", `/cards/${cardId}/agent-context`, data);
2748
2909
  }
2749
2910
  async appendAgentRunEvents(cardId, data) {
2750
- return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
2911
+ return this.request("POST", `/cards/${cardId}/agent-run-events`, {
2912
+ ...data,
2913
+ events: data.events.map((event) => sanitizeRunEventDraft(event))
2914
+ });
2751
2915
  }
2752
2916
  async getPendingUserMessages(cardId, sessionId, sinceSeq) {
2753
2917
  return this.request("GET", `/cards/${cardId}/agent-messages?sessionId=${sessionId}&sinceSeq=${sinceSeq}`);
@@ -4400,81 +4564,6 @@ function parseHmyConfig(text) {
4400
4564
  }
4401
4565
 
4402
4566
  // src/skills.ts
4403
- var HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
4404
-
4405
- Start work on a Harmony card. Card reference: $ARGUMENTS
4406
-
4407
- ## 1. Find & Fetch Card
4408
-
4409
- Parse the reference and fetch the card:
4410
- - \`#42\` or \`42\` → \`harmony_get_card\` with \`shortId: 42\`
4411
- - UUID → \`harmony_get_card\` with \`cardId\`
4412
- - Name/text → \`harmony_search_cards\` with \`query\`
4413
-
4414
- ## 2. Get Board State
4415
-
4416
- Call \`harmony_get_board\` to get columns and labels. From the response:
4417
- - Find the "In Progress" (or "Progress") column ID
4418
- - Find the "agent" label ID
4419
-
4420
- ## 3. Setup Card for Work
4421
-
4422
- Execute these in sequence:
4423
- 1. \`harmony_move_card\` → Move to "In Progress" column
4424
- 2. \`harmony_add_label_to_card\` → Add "agent" label
4425
- 3. \`harmony_start_agent_session\`:
4426
- - \`cardId\`: Card UUID
4427
- - \`agentIdentifier\`: Your agent identifier
4428
- - \`agentName\`: Your agent name
4429
- - \`currentTask\`: "Analyzing card requirements"
4430
-
4431
- ## 4. Generate Work Prompt
4432
-
4433
- Call \`harmony_generate_prompt\` with:
4434
- - \`cardId\` or \`shortId\` (+ \`projectId\` if using shortId)
4435
- - \`variant\`: Select based on task:
4436
- - \`"execute"\` (default) → Clear tasks, bug fixes, well-defined work
4437
- - \`"analysis"\` → Complex features, unclear requirements
4438
- - \`"draft"\` → Medium complexity, want feedback first
4439
-
4440
- The generated prompt provides role framing, focus areas, subtasks, linked cards, and suggested outputs.
4441
-
4442
- ## 5. Display Card Summary
4443
-
4444
- Show the user: Card title, short ID, role, priority, labels, due date, description, and subtasks.
4445
-
4446
- ## 6. Implement Solution
4447
-
4448
- Work on the card following the generated prompt's guidance. Update progress at milestones:
4449
- - \`harmony_update_agent_progress\` with \`progressPercent\` (0-100), \`currentTask\`, \`status\`, \`blockers\`
4450
-
4451
- **Progress checkpoints:** 20% (exploration), 50% (implementation), 80% (testing), 100% (done)
4452
-
4453
- ## 7. Complete Work
4454
-
4455
- When finished:
4456
- 1. \`harmony_end_agent_session\` with \`status: "completed"\`, \`progressPercent: 100\`
4457
- 2. \`harmony_move_card\` to "Review" column
4458
- 3. Summarize accomplishments
4459
-
4460
- If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
4461
-
4462
- ## Key Tools Reference
4463
-
4464
- **Cards:** \`harmony_get_card\` (by \`cardId\`, \`shortId\`, or \`shortIds\`), \`harmony_search_cards\`, \`harmony_create_card\`, \`harmony_update_card\`, \`harmony_move_card\`, \`harmony_delete_card\`, \`harmony_assign_card\`
4465
-
4466
- **Subtasks:** \`harmony_create_subtask\`, \`harmony_toggle_subtask\`, \`harmony_delete_subtask\`
4467
-
4468
- **Labels:** \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\`, \`harmony_create_label\`
4469
-
4470
- **Links:** \`harmony_add_link_to_card\`, \`harmony_remove_link_from_card\`, \`harmony_get_card_links\`
4471
-
4472
- **Board:** \`harmony_get_board\`, \`harmony_list_projects\`, \`harmony_get_context\`, \`harmony_set_project_context\`
4473
-
4474
- **Sessions:** \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\`, \`harmony_get_agent_session\`
4475
-
4476
- **AI:** \`harmony_generate_prompt\`, \`harmony_process_command\`
4477
- `;
4478
4567
  function buildSkillFile(skill) {
4479
4568
  const content = stripSkillPreamble(skill.content);
4480
4569
  if (skill.skillVersion !== undefined && !hasMetadataVersion(content)) {
@@ -5437,6 +5526,23 @@ var TOOLS = {
5437
5526
  required: ["cardId", "url"]
5438
5527
  }
5439
5528
  },
5529
+ harmony_remove_external_link: {
5530
+ description: "Remove an external reference URL from a card — the counterpart to harmony_add_external_link. Takes the link id from harmony_get_card_external_links, not the URL.",
5531
+ inputSchema: {
5532
+ type: "object",
5533
+ properties: {
5534
+ cardId: {
5535
+ type: "string",
5536
+ description: "Card UUID"
5537
+ },
5538
+ linkId: {
5539
+ type: "string",
5540
+ description: "External link UUID, as returned by harmony_get_card_external_links"
5541
+ }
5542
+ },
5543
+ required: ["cardId", "linkId"]
5544
+ }
5545
+ },
5440
5546
  harmony_create_subtask: {
5441
5547
  description: "Create a subtask on a card",
5442
5548
  inputSchema: {
@@ -6312,7 +6418,7 @@ var TOOLS = {
6312
6418
  }
6313
6419
  },
6314
6420
  harmony_create_plan: {
6315
- description: "Create a new project plan. Use this to upload implementation plans created during planning. Returns a URL where the plan can be viewed and edited in Harmony.",
6421
+ description: "Create a new project plan. Use this to upload a plan written during planning. Returns a URL where the plan can be viewed and edited in Harmony.",
6316
6422
  inputSchema: {
6317
6423
  type: "object",
6318
6424
  properties: {
@@ -6335,7 +6441,10 @@ var TOOLS = {
6335
6441
  items: {
6336
6442
  type: "object",
6337
6443
  properties: {
6338
- content: { type: "string", description: "Task description" },
6444
+ content: {
6445
+ type: "string",
6446
+ description: 'One success criterion, as a statement about the finished product that a test can check ("the mirror matches the migration chain"), never a work package ("write the mirror script"). One criterion may take several cards.'
6447
+ },
6339
6448
  priority: {
6340
6449
  type: "string",
6341
6450
  enum: ["high", "medium", "low"],
@@ -6349,7 +6458,7 @@ var TOOLS = {
6349
6458
  },
6350
6459
  required: ["content"]
6351
6460
  },
6352
- description: "Optional list of tasks to create with the plan"
6461
+ description: "The plan's success criteria, one entry each - what must be true when the plan is done, not a breakdown of the work to do it."
6353
6462
  }
6354
6463
  },
6355
6464
  required: ["title"]
@@ -6370,7 +6479,7 @@ var TOOLS = {
6370
6479
  }
6371
6480
  },
6372
6481
  harmony_update_plan: {
6373
- description: "Update an existing plan. Can update title, content, or status.",
6482
+ description: "Update an existing plan: its title, content, status, or the timeline dates its bar spans. " + "`startDate`/`endDate` are the plan's OWN schedule, the same pair a person sets by dragging the bar in the timeline view. " + "A plan is pinned on both or on neither: send both to schedule it, or both as null to return it to the span derived from its linked cards. " + "Sending one alone is refused unless the plan is already pinned. " + "They are never adjusted automatically — a card running past `endDate` is drawn as an overrun, and only a person extends the plan.",
6374
6483
  inputSchema: {
6375
6484
  type: "object",
6376
6485
  properties: {
@@ -6384,6 +6493,16 @@ var TOOLS = {
6384
6493
  type: "string",
6385
6494
  enum: ["draft", "active", "archived"],
6386
6495
  description: "New status"
6496
+ },
6497
+ startDate: {
6498
+ type: "string",
6499
+ nullable: true,
6500
+ description: "Timeline start as YYYY-MM-DD, or null to unpin (send endDate null too)."
6501
+ },
6502
+ endDate: {
6503
+ type: "string",
6504
+ nullable: true,
6505
+ description: "Timeline end as YYYY-MM-DD, or null to unpin (send startDate null too). Must not precede startDate."
6387
6506
  }
6388
6507
  },
6389
6508
  required: ["planId"]
@@ -7331,6 +7450,11 @@ ${list}
7331
7450
  const result = await client3.addExternalLink(cardId, url, title);
7332
7451
  return { success: true, ...result };
7333
7452
  }
7453
+ case "harmony_remove_external_link": {
7454
+ const cardId = z.string().uuid().parse(args.cardId);
7455
+ const linkId = z.string().uuid().parse(args.linkId);
7456
+ return await client3.removeExternalLink(cardId, linkId);
7457
+ }
7334
7458
  case "harmony_classify_card":
7335
7459
  return deprecatedRemovedToolResult("harmony_classify_card");
7336
7460
  case "harmony_create_subtask": {
@@ -8363,6 +8487,13 @@ ${options}
8363
8487
  if (args.status !== undefined) {
8364
8488
  updates.status = z.enum(["draft", "active", "archived"]).parse(args.status);
8365
8489
  }
8490
+ const planDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
8491
+ message: "expected a date as YYYY-MM-DD"
8492
+ });
8493
+ if (args.startDate !== undefined)
8494
+ updates.startDate = planDate.nullable().parse(args.startDate);
8495
+ if (args.endDate !== undefined)
8496
+ updates.endDate = planDate.nullable().parse(args.endDate);
8366
8497
  const result = await client3.updatePlan(planId, updates);
8367
8498
  return { success: true, plan: result.plan };
8368
8499
  }