@gethmy/mcp 2.8.2 → 2.8.4

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 CHANGED
@@ -1551,6 +1551,9 @@ class HarmonyApiClient {
1551
1551
  async toggleSubtask(subtaskId) {
1552
1552
  return this.request("POST", `/subtasks/${subtaskId}/toggle`);
1553
1553
  }
1554
+ async updateSubtask(subtaskId, updates) {
1555
+ return this.request("PATCH", `/subtasks/${subtaskId}`, updates);
1556
+ }
1554
1557
  async deleteSubtask(subtaskId) {
1555
1558
  return this.request("DELETE", `/subtasks/${subtaskId}`);
1556
1559
  }
@@ -2026,7 +2029,8 @@ var AUTO_START_TRIGGERS = new Set([
2026
2029
  "harmony_generate_prompt",
2027
2030
  "harmony_update_card",
2028
2031
  "harmony_create_subtask",
2029
- "harmony_toggle_subtask"
2032
+ "harmony_toggle_subtask",
2033
+ "harmony_update_subtask"
2030
2034
  ]);
2031
2035
  var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
2032
2036
  var CHECK_INTERVAL_MS = 60 * 1000;
@@ -2944,7 +2948,12 @@ var TOOLS = {
2944
2948
  priority: { type: "string", enum: ["low", "medium", "high", "urgent"] },
2945
2949
  assigneeId: { type: "string", nullable: true },
2946
2950
  dueDate: { type: "string", nullable: true },
2947
- done: { type: "boolean", description: "Mark card as done or not done" }
2951
+ done: { type: "boolean", description: "Mark card as done or not done" },
2952
+ planId: {
2953
+ type: "string",
2954
+ nullable: true,
2955
+ description: "Plan ID to link this card to, or null to unlink. Sets cards.plan_id. The plan must exist and belong to the same project."
2956
+ }
2948
2957
  },
2949
2958
  required: ["cardId"]
2950
2959
  }
@@ -3243,6 +3252,25 @@ var TOOLS = {
3243
3252
  required: ["subtaskId"]
3244
3253
  }
3245
3254
  },
3255
+ harmony_update_subtask: {
3256
+ description: "Update a subtask: rename its title, set an explicit completion state, and/or reorder it. Unlike harmony_toggle_subtask (which flips completion), `completed` here sets an explicit value — safe to call idempotently. At least one of title/completed/position is required.",
3257
+ inputSchema: {
3258
+ type: "object",
3259
+ properties: {
3260
+ subtaskId: { type: "string" },
3261
+ title: { type: "string", description: "New title (1–500 chars)" },
3262
+ completed: {
3263
+ type: "boolean",
3264
+ description: "Explicit completion state (set, not toggle)"
3265
+ },
3266
+ position: {
3267
+ type: "number",
3268
+ description: "New position (0-based ordering within the card)"
3269
+ }
3270
+ },
3271
+ required: ["subtaskId"]
3272
+ }
3273
+ },
3246
3274
  harmony_delete_subtask: {
3247
3275
  description: "Delete a subtask",
3248
3276
  inputSchema: {
@@ -4360,13 +4388,15 @@ async function handleToolCall(name, args, deps) {
4360
4388
  }
4361
4389
  case "harmony_update_card": {
4362
4390
  const cardId = z.string().uuid().parse(args.cardId);
4391
+ const planId = args.planId === undefined ? undefined : args.planId === null ? null : z.string().uuid().parse(args.planId);
4363
4392
  const result = await client3.updateCard(cardId, {
4364
4393
  title: args.title,
4365
4394
  description: args.description,
4366
4395
  priority: args.priority,
4367
4396
  assigneeId: args.assigneeId,
4368
4397
  dueDate: args.dueDate,
4369
- done: args.done
4398
+ done: args.done,
4399
+ planId
4370
4400
  });
4371
4401
  return { success: true, ...result };
4372
4402
  }
@@ -4569,6 +4599,24 @@ async function handleToolCall(name, args, deps) {
4569
4599
  const result = await client3.toggleSubtask(subtaskId);
4570
4600
  return { success: true, ...result };
4571
4601
  }
4602
+ case "harmony_update_subtask": {
4603
+ const subtaskId = z.string().uuid().parse(args.subtaskId);
4604
+ const updates = {};
4605
+ if (args.title !== undefined) {
4606
+ updates.title = z.string().min(1).max(500).parse(args.title);
4607
+ }
4608
+ if (args.completed !== undefined) {
4609
+ updates.completed = z.boolean().parse(args.completed);
4610
+ }
4611
+ if (args.position !== undefined) {
4612
+ updates.position = z.number().int().min(0).parse(args.position);
4613
+ }
4614
+ if (Object.keys(updates).length === 0) {
4615
+ throw new Error("harmony_update_subtask requires at least one of: title, completed, position");
4616
+ }
4617
+ const result = await client3.updateSubtask(subtaskId, updates);
4618
+ return { success: true, ...result };
4619
+ }
4572
4620
  case "harmony_delete_subtask": {
4573
4621
  const subtaskId = z.string().uuid().parse(args.subtaskId);
4574
4622
  await client3.deleteSubtask(subtaskId);
@@ -6357,9 +6405,10 @@ function appendToToml(filePath, section, content, options = {}) {
6357
6405
  }
6358
6406
  try {
6359
6407
  const existing = readFileSync6(filePath, "utf-8");
6360
- if (existing.includes(section)) {
6408
+ if (existing.includes(`[${section}]`)) {
6361
6409
  if (options.force) {
6362
- const updated = existing.replace(new RegExp(`\\[${section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\[|$)`), content.trim() + `
6410
+ const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6411
+ const updated = existing.replace(new RegExp(`(?:#[^\\n]*\\n)*\\[${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`), content.trim() + `
6363
6412
 
6364
6413
  `);
6365
6414
  writeFileSync4(filePath, updated, { mode: 420 });
@@ -6433,6 +6482,53 @@ function getWriteSummary(files, options = {}) {
6433
6482
  }
6434
6483
 
6435
6484
  // src/tui/setup.ts
6485
+ var SAFE_HARMONY_TOOLS = [
6486
+ "harmony_get_card",
6487
+ "harmony_get_card_by_short_id",
6488
+ "harmony_search_cards",
6489
+ "harmony_get_board",
6490
+ "harmony_get_context",
6491
+ "harmony_list_projects",
6492
+ "harmony_list_workspaces",
6493
+ "harmony_get_card_links",
6494
+ "harmony_get_card_attachments",
6495
+ "harmony_get_card_external_links",
6496
+ "harmony_get_comments",
6497
+ "harmony_get_plan",
6498
+ "harmony_list_plans",
6499
+ "harmony_get_agent_session",
6500
+ "harmony_get_workspace_members",
6501
+ "harmony_resolve_links",
6502
+ "harmony_recall",
6503
+ "harmony_memory_search",
6504
+ "harmony_generate_prompt",
6505
+ "harmony_create_card",
6506
+ "harmony_update_card",
6507
+ "harmony_move_card",
6508
+ "harmony_assign_card",
6509
+ "harmony_create_subtask",
6510
+ "harmony_toggle_subtask",
6511
+ "harmony_add_label_to_card",
6512
+ "harmony_remove_label_from_card",
6513
+ "harmony_create_label",
6514
+ "harmony_add_comment",
6515
+ "harmony_update_comment",
6516
+ "harmony_add_link_to_card",
6517
+ "harmony_remove_link_from_card",
6518
+ "harmony_start_agent_session",
6519
+ "harmony_update_agent_progress",
6520
+ "harmony_end_agent_session",
6521
+ "harmony_set_project_context",
6522
+ "harmony_set_workspace_context",
6523
+ "harmony_create_plan",
6524
+ "harmony_update_plan",
6525
+ "harmony_advance_plan",
6526
+ "harmony_remember",
6527
+ "harmony_relate",
6528
+ "harmony_update_memory",
6529
+ "harmony_process_command",
6530
+ "harmony_sync"
6531
+ ];
6436
6532
  var GLOBAL_SKILLS_DIR = join7(homedir6(), ".agents", "skills");
6437
6533
  var API_URL = "https://app.gethmy.com/api";
6438
6534
  async function registerMcpServer() {
@@ -6455,9 +6551,7 @@ async function writeMcpConfigFallback(home) {
6455
6551
  }
6456
6552
  let settings = {};
6457
6553
  if (existsSync9(settingsPath)) {
6458
- try {
6459
- settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
6460
- } catch {}
6554
+ settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
6461
6555
  }
6462
6556
  const mcpServers = settings.mcpServers || {};
6463
6557
  mcpServers.harmony = {
@@ -6467,6 +6561,31 @@ async function writeMcpConfigFallback(home) {
6467
6561
  settings.mcpServers = mcpServers;
6468
6562
  writeFileSync5(settingsPath, JSON.stringify(settings, null, 2));
6469
6563
  }
6564
+ async function allowlistHarmonyTools(home, allowAll) {
6565
+ const { readFileSync: readFileSync7, writeFileSync: writeFileSync5, mkdirSync: mkdirSync6, existsSync: existsSync9 } = await import("node:fs");
6566
+ const settingsPath = join7(home, ".claude", "settings.json");
6567
+ const settingsDir = dirname3(settingsPath);
6568
+ if (!existsSync9(settingsDir)) {
6569
+ mkdirSync6(settingsDir, { recursive: true });
6570
+ }
6571
+ let settings = {};
6572
+ if (existsSync9(settingsPath)) {
6573
+ settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
6574
+ }
6575
+ const permissions = settings.permissions || {};
6576
+ const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
6577
+ const hasBlanket = allow.includes("mcp__harmony") || allow.includes("mcp__harmony__*");
6578
+ const wanted = allowAll ? ["mcp__harmony"] : SAFE_HARMONY_TOOLS.map((t) => `mcp__harmony__${t}`);
6579
+ const missing = hasBlanket ? [] : wanted.filter((r) => !allow.includes(r));
6580
+ if (missing.length === 0) {
6581
+ return "already";
6582
+ }
6583
+ allow.push(...missing);
6584
+ permissions.allow = allow;
6585
+ settings.permissions = permissions;
6586
+ writeFileSync5(settingsPath, JSON.stringify(settings, null, 2));
6587
+ return "added";
6588
+ }
6470
6589
  async function validateApiKey(apiKey, apiUrl = API_URL) {
6471
6590
  try {
6472
6591
  const response = await fetch(`${apiUrl}/v1/workspaces`, {
@@ -7231,6 +7350,26 @@ async function runSetup(options = {}) {
7231
7350
  }
7232
7351
  }
7233
7352
  }
7353
+ if (claudeDetected || selectedAgents.includes("claude")) {
7354
+ const allowAll = options.allowAllTools === true;
7355
+ const message = allowAll ? "Allowlist EVERY Harmony tool without confirmation, including destructive ones (delete/archive/api-key/invite)?" : "Allowlist common Harmony tools (reads + create/update/move/comment) so /hmy doesn't prompt each time? Destructive tools (delete/archive/api-key/invite) will still ask.";
7356
+ const allowTools = await p3.confirm({ message, initialValue: true });
7357
+ if (p3.isCancel(allowTools)) {
7358
+ p3.cancel("Setup cancelled.");
7359
+ process.exit(0);
7360
+ }
7361
+ if (allowTools) {
7362
+ try {
7363
+ const result = await allowlistHarmonyTools(home, allowAll);
7364
+ const scope = allowAll ? "all tools" : "safe tools";
7365
+ console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join7(home, ".claude", "settings.json"), home))} ${colors.dim(result === "added" ? `(${scope} allowlisted)` : `(${scope} already allowlisted)`)}`);
7366
+ } catch {
7367
+ p3.log.warning("Could not allowlist Harmony tools. Run /permissions in Claude Code and choose “always allow” for Harmony, or add mcp__harmony to permissions.allow in ~/.claude/settings.json.");
7368
+ }
7369
+ } else {
7370
+ console.log(` ${colors.dim("Skipped tool allowlist — you'll be prompted per tool, or run /permissions in Claude Code later.")}`);
7371
+ }
7372
+ }
7234
7373
  if (selectedWorkspaceId || selectedProjectId) {
7235
7374
  const localConfig = {};
7236
7375
  if (selectedWorkspaceId)
@@ -7361,7 +7500,7 @@ program.command("reset").description("Remove stored configuration").action(() =>
7361
7500
  console.log(`
7362
7501
  To reconfigure, run: npx @gethmy/mcp setup`);
7363
7502
  });
7364
- program.command("setup").description("Smart setup wizard for Harmony MCP (recommended)").argument("[slug]", "Project slug — resolves to workspace + project in one step (e.g. harmony-6590761b)").option("-f, --force", "Overwrite existing configuration files").option("-k, --api-key <key>", "API key (skips prompt)").option("-e, --email <email>", "Your email for auto-assignment").option("-a, --agents <agents...>", "Agents to configure: claude, codex, cursor, windsurf").option("-l, --local", "Install skills locally in project directory").option("-g, --global", "Install skills globally (recommended)").option("-w, --workspace <id>", "Set workspace context (UUID)").option("-p, --project <id>", "Set project context (UUID)").option("--skip-context", "Skip workspace/project selection").option("--skip-docs", "Skip project docs scaffold/verification").option("--new", "Create a new account (skip the choice prompt)").option("-n, --name <name>", "Full name (for account creation)").action(async (slug, options) => {
7503
+ program.command("setup").description("Smart setup wizard for Harmony MCP (recommended)").argument("[slug]", "Project slug — resolves to workspace + project in one step (e.g. harmony-6590761b)").option("-f, --force", "Overwrite existing configuration files").option("-k, --api-key <key>", "API key (skips prompt)").option("-e, --email <email>", "Your email for auto-assignment").option("-a, --agents <agents...>", "Agents to configure: claude, codex, cursor, windsurf").option("-l, --local", "Install skills locally in project directory").option("-g, --global", "Install skills globally (recommended)").option("-w, --workspace <id>", "Set workspace context (UUID)").option("-p, --project <id>", "Set project context (UUID)").option("--skip-context", "Skip workspace/project selection").option("--skip-docs", "Skip project docs scaffold/verification").option("--new", "Create a new account (skip the choice prompt)").option("-n, --name <name>", "Full name (for account creation)").option("--allow-all-tools", "Allowlist every Harmony tool (incl. destructive: delete/archive/api-key/invite) without confirmation. Default allowlists only read + routine-write tools; destructive tools keep prompting.").action(async (slug, options) => {
7365
7504
  await runSetup({
7366
7505
  force: options.force,
7367
7506
  apiKey: options.apiKey,
@@ -7374,7 +7513,8 @@ program.command("setup").description("Smart setup wizard for Harmony MCP (recomm
7374
7513
  skipContext: options.skipContext,
7375
7514
  skipDocs: options.skipDocs,
7376
7515
  newAccount: options.new,
7377
- name: options.name
7516
+ name: options.name,
7517
+ allowAllTools: options.allowAllTools
7378
7518
  });
7379
7519
  });
7380
7520
  program.parse();
package/dist/index.js CHANGED
@@ -1547,6 +1547,9 @@ class HarmonyApiClient {
1547
1547
  async toggleSubtask(subtaskId) {
1548
1548
  return this.request("POST", `/subtasks/${subtaskId}/toggle`);
1549
1549
  }
1550
+ async updateSubtask(subtaskId, updates) {
1551
+ return this.request("PATCH", `/subtasks/${subtaskId}`, updates);
1552
+ }
1550
1553
  async deleteSubtask(subtaskId) {
1551
1554
  return this.request("DELETE", `/subtasks/${subtaskId}`);
1552
1555
  }
@@ -2022,7 +2025,8 @@ var AUTO_START_TRIGGERS = new Set([
2022
2025
  "harmony_generate_prompt",
2023
2026
  "harmony_update_card",
2024
2027
  "harmony_create_subtask",
2025
- "harmony_toggle_subtask"
2028
+ "harmony_toggle_subtask",
2029
+ "harmony_update_subtask"
2026
2030
  ]);
2027
2031
  var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
2028
2032
  var CHECK_INTERVAL_MS = 60 * 1000;
@@ -2940,7 +2944,12 @@ var TOOLS = {
2940
2944
  priority: { type: "string", enum: ["low", "medium", "high", "urgent"] },
2941
2945
  assigneeId: { type: "string", nullable: true },
2942
2946
  dueDate: { type: "string", nullable: true },
2943
- done: { type: "boolean", description: "Mark card as done or not done" }
2947
+ done: { type: "boolean", description: "Mark card as done or not done" },
2948
+ planId: {
2949
+ type: "string",
2950
+ nullable: true,
2951
+ description: "Plan ID to link this card to, or null to unlink. Sets cards.plan_id. The plan must exist and belong to the same project."
2952
+ }
2944
2953
  },
2945
2954
  required: ["cardId"]
2946
2955
  }
@@ -3239,6 +3248,25 @@ var TOOLS = {
3239
3248
  required: ["subtaskId"]
3240
3249
  }
3241
3250
  },
3251
+ harmony_update_subtask: {
3252
+ description: "Update a subtask: rename its title, set an explicit completion state, and/or reorder it. Unlike harmony_toggle_subtask (which flips completion), `completed` here sets an explicit value — safe to call idempotently. At least one of title/completed/position is required.",
3253
+ inputSchema: {
3254
+ type: "object",
3255
+ properties: {
3256
+ subtaskId: { type: "string" },
3257
+ title: { type: "string", description: "New title (1–500 chars)" },
3258
+ completed: {
3259
+ type: "boolean",
3260
+ description: "Explicit completion state (set, not toggle)"
3261
+ },
3262
+ position: {
3263
+ type: "number",
3264
+ description: "New position (0-based ordering within the card)"
3265
+ }
3266
+ },
3267
+ required: ["subtaskId"]
3268
+ }
3269
+ },
3242
3270
  harmony_delete_subtask: {
3243
3271
  description: "Delete a subtask",
3244
3272
  inputSchema: {
@@ -4356,13 +4384,15 @@ async function handleToolCall(name, args, deps) {
4356
4384
  }
4357
4385
  case "harmony_update_card": {
4358
4386
  const cardId = z.string().uuid().parse(args.cardId);
4387
+ const planId = args.planId === undefined ? undefined : args.planId === null ? null : z.string().uuid().parse(args.planId);
4359
4388
  const result = await client3.updateCard(cardId, {
4360
4389
  title: args.title,
4361
4390
  description: args.description,
4362
4391
  priority: args.priority,
4363
4392
  assigneeId: args.assigneeId,
4364
4393
  dueDate: args.dueDate,
4365
- done: args.done
4394
+ done: args.done,
4395
+ planId
4366
4396
  });
4367
4397
  return { success: true, ...result };
4368
4398
  }
@@ -4565,6 +4595,24 @@ async function handleToolCall(name, args, deps) {
4565
4595
  const result = await client3.toggleSubtask(subtaskId);
4566
4596
  return { success: true, ...result };
4567
4597
  }
4598
+ case "harmony_update_subtask": {
4599
+ const subtaskId = z.string().uuid().parse(args.subtaskId);
4600
+ const updates = {};
4601
+ if (args.title !== undefined) {
4602
+ updates.title = z.string().min(1).max(500).parse(args.title);
4603
+ }
4604
+ if (args.completed !== undefined) {
4605
+ updates.completed = z.boolean().parse(args.completed);
4606
+ }
4607
+ if (args.position !== undefined) {
4608
+ updates.position = z.number().int().min(0).parse(args.position);
4609
+ }
4610
+ if (Object.keys(updates).length === 0) {
4611
+ throw new Error("harmony_update_subtask requires at least one of: title, completed, position");
4612
+ }
4613
+ const result = await client3.updateSubtask(subtaskId, updates);
4614
+ return { success: true, ...result };
4615
+ }
4568
4616
  case "harmony_delete_subtask": {
4569
4617
  const subtaskId = z.string().uuid().parse(args.subtaskId);
4570
4618
  await client3.deleteSubtask(subtaskId);
@@ -1154,6 +1154,9 @@ class HarmonyApiClient {
1154
1154
  async toggleSubtask(subtaskId) {
1155
1155
  return this.request("POST", `/subtasks/${subtaskId}/toggle`);
1156
1156
  }
1157
+ async updateSubtask(subtaskId, updates) {
1158
+ return this.request("PATCH", `/subtasks/${subtaskId}`, updates);
1159
+ }
1157
1160
  async deleteSubtask(subtaskId) {
1158
1161
  return this.request("DELETE", `/subtasks/${subtaskId}`);
1159
1162
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.8.2",
3
+ "version": "2.8.4",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/api-client.ts CHANGED
@@ -474,6 +474,7 @@ export class HarmonyApiClient {
474
474
  dueDate?: string | null;
475
475
  done?: boolean;
476
476
  archivedAt?: string | null;
477
+ planId?: string | null;
477
478
  },
478
479
  ): Promise<{ card: unknown }> {
479
480
  return this.request("PATCH", `/cards/${cardId}`, updates);
@@ -617,6 +618,13 @@ export class HarmonyApiClient {
617
618
  return this.request("POST", `/subtasks/${subtaskId}/toggle`);
618
619
  }
619
620
 
621
+ async updateSubtask(
622
+ subtaskId: string,
623
+ updates: { title?: string; completed?: boolean; position?: number },
624
+ ): Promise<{ subtask: unknown }> {
625
+ return this.request("PATCH", `/subtasks/${subtaskId}`, updates);
626
+ }
627
+
620
628
  async deleteSubtask(subtaskId: string): Promise<{ success: boolean }> {
621
629
  return this.request("DELETE", `/subtasks/${subtaskId}`);
622
630
  }
@@ -78,6 +78,7 @@ export const AUTO_START_TRIGGERS = new Set([
78
78
  "harmony_update_card",
79
79
  "harmony_create_subtask",
80
80
  "harmony_toggle_subtask",
81
+ "harmony_update_subtask",
81
82
  ]);
82
83
 
83
84
  export const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
package/src/cli.ts CHANGED
@@ -158,6 +158,10 @@ program
158
158
  .option("--skip-docs", "Skip project docs scaffold/verification")
159
159
  .option("--new", "Create a new account (skip the choice prompt)")
160
160
  .option("-n, --name <name>", "Full name (for account creation)")
161
+ .option(
162
+ "--allow-all-tools",
163
+ "Allowlist every Harmony tool (incl. destructive: delete/archive/api-key/invite) without confirmation. Default allowlists only read + routine-write tools; destructive tools keep prompting.",
164
+ )
161
165
  .action(async (slug, options) => {
162
166
  await runSetup({
163
167
  force: options.force,
@@ -176,6 +180,7 @@ program
176
180
  skipDocs: options.skipDocs,
177
181
  newAccount: options.new,
178
182
  name: options.name,
183
+ allowAllTools: options.allowAllTools,
179
184
  });
180
185
  });
181
186
 
package/src/server.ts CHANGED
@@ -308,6 +308,12 @@ export const TOOLS = {
308
308
  assigneeId: { type: "string", nullable: true },
309
309
  dueDate: { type: "string", nullable: true },
310
310
  done: { type: "boolean", description: "Mark card as done or not done" },
311
+ planId: {
312
+ type: "string",
313
+ nullable: true,
314
+ description:
315
+ "Plan ID to link this card to, or null to unlink. Sets cards.plan_id. The plan must exist and belong to the same project.",
316
+ },
311
317
  },
312
318
  required: ["cardId"],
313
319
  },
@@ -625,6 +631,26 @@ export const TOOLS = {
625
631
  required: ["subtaskId"],
626
632
  },
627
633
  },
634
+ harmony_update_subtask: {
635
+ description:
636
+ "Update a subtask: rename its title, set an explicit completion state, and/or reorder it. Unlike harmony_toggle_subtask (which flips completion), `completed` here sets an explicit value — safe to call idempotently. At least one of title/completed/position is required.",
637
+ inputSchema: {
638
+ type: "object",
639
+ properties: {
640
+ subtaskId: { type: "string" },
641
+ title: { type: "string", description: "New title (1–500 chars)" },
642
+ completed: {
643
+ type: "boolean",
644
+ description: "Explicit completion state (set, not toggle)",
645
+ },
646
+ position: {
647
+ type: "number",
648
+ description: "New position (0-based ordering within the card)",
649
+ },
650
+ },
651
+ required: ["subtaskId"],
652
+ },
653
+ },
628
654
  harmony_delete_subtask: {
629
655
  description: "Delete a subtask",
630
656
  inputSchema: {
@@ -1944,6 +1970,13 @@ async function handleToolCall(
1944
1970
 
1945
1971
  case "harmony_update_card": {
1946
1972
  const cardId = z.string().uuid().parse(args.cardId);
1973
+ // null = unlink, undefined = leave untouched, otherwise must be a UUID.
1974
+ const planId =
1975
+ args.planId === undefined
1976
+ ? undefined
1977
+ : args.planId === null
1978
+ ? null
1979
+ : z.string().uuid().parse(args.planId);
1947
1980
  const result = await client.updateCard(cardId, {
1948
1981
  title: args.title as string | undefined,
1949
1982
  description: args.description as string | undefined,
@@ -1951,6 +1984,7 @@ async function handleToolCall(
1951
1984
  assigneeId: args.assigneeId as string | null | undefined,
1952
1985
  dueDate: args.dueDate as string | null | undefined,
1953
1986
  done: args.done as boolean | undefined,
1987
+ planId,
1954
1988
  });
1955
1989
  return { success: true, ...result };
1956
1990
  }
@@ -2237,6 +2271,31 @@ async function handleToolCall(
2237
2271
  return { success: true, ...result };
2238
2272
  }
2239
2273
 
2274
+ case "harmony_update_subtask": {
2275
+ const subtaskId = z.string().uuid().parse(args.subtaskId);
2276
+ const updates: {
2277
+ title?: string;
2278
+ completed?: boolean;
2279
+ position?: number;
2280
+ } = {};
2281
+ if (args.title !== undefined) {
2282
+ updates.title = z.string().min(1).max(500).parse(args.title);
2283
+ }
2284
+ if (args.completed !== undefined) {
2285
+ updates.completed = z.boolean().parse(args.completed);
2286
+ }
2287
+ if (args.position !== undefined) {
2288
+ updates.position = z.number().int().min(0).parse(args.position);
2289
+ }
2290
+ if (Object.keys(updates).length === 0) {
2291
+ throw new Error(
2292
+ "harmony_update_subtask requires at least one of: title, completed, position",
2293
+ );
2294
+ }
2295
+ const result = await client.updateSubtask(subtaskId, updates);
2296
+ return { success: true, ...result };
2297
+ }
2298
+
2240
2299
  case "harmony_delete_subtask": {
2241
2300
  const subtaskId = z.string().uuid().parse(args.subtaskId);
2242
2301
  await client.deleteSubtask(subtaskId);
package/src/tui/setup.ts CHANGED
@@ -44,8 +44,68 @@ export interface SetupOptions {
44
44
  skipDocs?: boolean;
45
45
  newAccount?: boolean;
46
46
  name?: string;
47
+ allowAllTools?: boolean;
47
48
  }
48
49
 
50
+ /**
51
+ * Harmony tools that are safe to run without per-call confirmation: reads and
52
+ * routine writes the /hmy flow leans on. Destructive or sensitive tools are
53
+ * deliberately ABSENT so they keep prompting — deletes, archives, API-key
54
+ * minting, and invitations are exactly where the permission prompt earns its
55
+ * keep, especially under autonomous agent runs. New tools default to prompting
56
+ * until someone classifies them here (safe failure mode). `--allow-all-tools`
57
+ * overrides this with a blanket grant.
58
+ */
59
+ const SAFE_HARMONY_TOOLS = [
60
+ // Reads
61
+ "harmony_get_card",
62
+ "harmony_get_card_by_short_id",
63
+ "harmony_search_cards",
64
+ "harmony_get_board",
65
+ "harmony_get_context",
66
+ "harmony_list_projects",
67
+ "harmony_list_workspaces",
68
+ "harmony_get_card_links",
69
+ "harmony_get_card_attachments",
70
+ "harmony_get_card_external_links",
71
+ "harmony_get_comments",
72
+ "harmony_get_plan",
73
+ "harmony_list_plans",
74
+ "harmony_get_agent_session",
75
+ "harmony_get_workspace_members",
76
+ "harmony_resolve_links",
77
+ "harmony_recall",
78
+ "harmony_memory_search",
79
+ "harmony_generate_prompt",
80
+ // Routine writes
81
+ "harmony_create_card",
82
+ "harmony_update_card",
83
+ "harmony_move_card",
84
+ "harmony_assign_card",
85
+ "harmony_create_subtask",
86
+ "harmony_toggle_subtask",
87
+ "harmony_add_label_to_card",
88
+ "harmony_remove_label_from_card",
89
+ "harmony_create_label",
90
+ "harmony_add_comment",
91
+ "harmony_update_comment",
92
+ "harmony_add_link_to_card",
93
+ "harmony_remove_link_from_card",
94
+ "harmony_start_agent_session",
95
+ "harmony_update_agent_progress",
96
+ "harmony_end_agent_session",
97
+ "harmony_set_project_context",
98
+ "harmony_set_workspace_context",
99
+ "harmony_create_plan",
100
+ "harmony_update_plan",
101
+ "harmony_advance_plan",
102
+ "harmony_remember",
103
+ "harmony_relate",
104
+ "harmony_update_memory",
105
+ "harmony_process_command",
106
+ "harmony_sync",
107
+ ];
108
+
49
109
  // Central skills directory for global installation
50
110
  const GLOBAL_SKILLS_DIR = join(homedir(), ".agents", "skills");
51
111
 
@@ -87,14 +147,12 @@ async function writeMcpConfigFallback(home: string): Promise<void> {
87
147
  mkdirSync(settingsDir, { recursive: true });
88
148
  }
89
149
 
90
- // Read existing settings or start fresh
150
+ // Read existing settings. If the file exists but doesn't parse, bail rather
151
+ // than overwrite — don't wipe a user's hand-edited settings.json to register
152
+ // one MCP server. The caller falls back to a manual-setup instruction.
91
153
  let settings: Record<string, unknown> = {};
92
154
  if (existsSync(settingsPath)) {
93
- try {
94
- settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
95
- } catch {
96
- // Invalid JSON, start fresh
97
- }
155
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
98
156
  }
99
157
 
100
158
  // Merge mcpServers config
@@ -109,6 +167,65 @@ async function writeMcpConfigFallback(home: string): Promise<void> {
109
167
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
110
168
  }
111
169
 
170
+ /**
171
+ * Allowlist Harmony MCP tools in Claude Code settings so the /hmy flow runs
172
+ * without per-tool permission prompts. Merges rules into permissions.allow in
173
+ * ~/.claude/settings.json, preserving existing settings. Idempotent.
174
+ *
175
+ * Default (allowAll=false): writes a per-tool rule (`mcp__harmony__<tool>`) for
176
+ * each entry in SAFE_HARMONY_TOOLS only — destructive tools keep prompting.
177
+ * allowAll=true: writes the bare `mcp__harmony` rule (every tool, incl.
178
+ * destructive). The bare form grants all tools and is supported across all
179
+ * Claude Code versions.
180
+ */
181
+ async function allowlistHarmonyTools(
182
+ home: string,
183
+ allowAll: boolean,
184
+ ): Promise<"added" | "already"> {
185
+ const { readFileSync, writeFileSync, mkdirSync, existsSync } = await import(
186
+ "node:fs"
187
+ );
188
+ const settingsPath = join(home, ".claude", "settings.json");
189
+ const settingsDir = dirname(settingsPath);
190
+
191
+ if (!existsSync(settingsDir)) {
192
+ mkdirSync(settingsDir, { recursive: true });
193
+ }
194
+
195
+ // Read existing settings. If the file exists but doesn't parse, bail rather
196
+ // than overwrite — clobbering a user's hand-edited settings.json to add a
197
+ // permission rule is never worth it. The caller surfaces the manual-fix path.
198
+ let settings: Record<string, unknown> = {};
199
+ if (existsSync(settingsPath)) {
200
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
201
+ }
202
+
203
+ const permissions = (settings.permissions as Record<string, unknown>) || {};
204
+ const allow = Array.isArray(permissions.allow)
205
+ ? (permissions.allow as string[])
206
+ : [];
207
+
208
+ // A pre-existing blanket grant already covers everything either mode adds.
209
+ const hasBlanket =
210
+ allow.includes("mcp__harmony") || allow.includes("mcp__harmony__*");
211
+
212
+ const wanted = allowAll
213
+ ? ["mcp__harmony"]
214
+ : SAFE_HARMONY_TOOLS.map((t) => `mcp__harmony__${t}`);
215
+
216
+ const missing = hasBlanket ? [] : wanted.filter((r) => !allow.includes(r));
217
+ if (missing.length === 0) {
218
+ return "already";
219
+ }
220
+
221
+ allow.push(...missing);
222
+ permissions.allow = allow;
223
+ settings.permissions = permissions;
224
+
225
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
226
+ return "added";
227
+ }
228
+
112
229
  interface Workspace {
113
230
  id: string;
114
231
  name: string;
@@ -1187,6 +1304,38 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1187
1304
  }
1188
1305
  }
1189
1306
 
1307
+ // Step 8c: Allowlist Harmony tools so /hmy runs without permission prompts.
1308
+ // Default = safe subset (reads + routine writes); destructive tools still
1309
+ // prompt. `--allow-all-tools` opts into a blanket grant.
1310
+ if (claudeDetected || selectedAgents.includes("claude")) {
1311
+ const allowAll = options.allowAllTools === true;
1312
+ const message = allowAll
1313
+ ? "Allowlist EVERY Harmony tool without confirmation, including destructive ones (delete/archive/api-key/invite)?"
1314
+ : "Allowlist common Harmony tools (reads + create/update/move/comment) so /hmy doesn't prompt each time? Destructive tools (delete/archive/api-key/invite) will still ask.";
1315
+ const allowTools = await p.confirm({ message, initialValue: true });
1316
+ if (p.isCancel(allowTools)) {
1317
+ p.cancel("Setup cancelled.");
1318
+ process.exit(0);
1319
+ }
1320
+ if (allowTools) {
1321
+ try {
1322
+ const result = await allowlistHarmonyTools(home, allowAll);
1323
+ const scope = allowAll ? "all tools" : "safe tools";
1324
+ console.log(
1325
+ ` ${colors.success("✓")} ${colors.dim(formatPath(join(home, ".claude", "settings.json"), home))} ${colors.dim(result === "added" ? `(${scope} allowlisted)` : `(${scope} already allowlisted)`)}`,
1326
+ );
1327
+ } catch {
1328
+ p.log.warning(
1329
+ "Could not allowlist Harmony tools. Run /permissions in Claude Code and choose “always allow” for Harmony, or add mcp__harmony to permissions.allow in ~/.claude/settings.json.",
1330
+ );
1331
+ }
1332
+ } else {
1333
+ console.log(
1334
+ ` ${colors.dim("Skipped tool allowlist — you'll be prompted per tool, or run /permissions in Claude Code later.")}`,
1335
+ );
1336
+ }
1337
+ }
1338
+
1190
1339
  // Step 9: Save local context
1191
1340
  if (selectedWorkspaceId || selectedProjectId) {
1192
1341
  const localConfig: { workspaceId?: string; projectId?: string } = {};
package/src/tui/writer.ts CHANGED
@@ -156,13 +156,14 @@ export function appendToToml(
156
156
  try {
157
157
  const existing = readFileSync(filePath, "utf-8");
158
158
 
159
- if (existing.includes(section)) {
159
+ if (existing.includes(`[${section}]`)) {
160
160
  if (options.force) {
161
- // Replace existing section
161
+ // Replace existing section, including any comment lines directly above
162
+ // it. The end anchor is `\n[` (a table header at line start) or EOF —
163
+ // NOT a bare `[`, so we don't stop early on the `[` inside `args = [...]`.
164
+ const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
162
165
  const updated = existing.replace(
163
- new RegExp(
164
- `\\[${section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\[|$)`,
165
- ),
166
+ new RegExp(`(?:#[^\\n]*\\n)*\\[${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`),
166
167
  content.trim() + "\n\n",
167
168
  );
168
169
  writeFileSync(filePath, updated, { mode: 0o644 });