@gethmy/mcp 2.8.2 → 2.8.3

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
@@ -6357,9 +6357,10 @@ function appendToToml(filePath, section, content, options = {}) {
6357
6357
  }
6358
6358
  try {
6359
6359
  const existing = readFileSync6(filePath, "utf-8");
6360
- if (existing.includes(section)) {
6360
+ if (existing.includes(`[${section}]`)) {
6361
6361
  if (options.force) {
6362
- const updated = existing.replace(new RegExp(`\\[${section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\[|$)`), content.trim() + `
6362
+ const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6363
+ const updated = existing.replace(new RegExp(`(?:#[^\\n]*\\n)*\\[${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`), content.trim() + `
6363
6364
 
6364
6365
  `);
6365
6366
  writeFileSync4(filePath, updated, { mode: 420 });
@@ -6433,6 +6434,53 @@ function getWriteSummary(files, options = {}) {
6433
6434
  }
6434
6435
 
6435
6436
  // src/tui/setup.ts
6437
+ var SAFE_HARMONY_TOOLS = [
6438
+ "harmony_get_card",
6439
+ "harmony_get_card_by_short_id",
6440
+ "harmony_search_cards",
6441
+ "harmony_get_board",
6442
+ "harmony_get_context",
6443
+ "harmony_list_projects",
6444
+ "harmony_list_workspaces",
6445
+ "harmony_get_card_links",
6446
+ "harmony_get_card_attachments",
6447
+ "harmony_get_card_external_links",
6448
+ "harmony_get_comments",
6449
+ "harmony_get_plan",
6450
+ "harmony_list_plans",
6451
+ "harmony_get_agent_session",
6452
+ "harmony_get_workspace_members",
6453
+ "harmony_resolve_links",
6454
+ "harmony_recall",
6455
+ "harmony_memory_search",
6456
+ "harmony_generate_prompt",
6457
+ "harmony_create_card",
6458
+ "harmony_update_card",
6459
+ "harmony_move_card",
6460
+ "harmony_assign_card",
6461
+ "harmony_create_subtask",
6462
+ "harmony_toggle_subtask",
6463
+ "harmony_add_label_to_card",
6464
+ "harmony_remove_label_from_card",
6465
+ "harmony_create_label",
6466
+ "harmony_add_comment",
6467
+ "harmony_update_comment",
6468
+ "harmony_add_link_to_card",
6469
+ "harmony_remove_link_from_card",
6470
+ "harmony_start_agent_session",
6471
+ "harmony_update_agent_progress",
6472
+ "harmony_end_agent_session",
6473
+ "harmony_set_project_context",
6474
+ "harmony_set_workspace_context",
6475
+ "harmony_create_plan",
6476
+ "harmony_update_plan",
6477
+ "harmony_advance_plan",
6478
+ "harmony_remember",
6479
+ "harmony_relate",
6480
+ "harmony_update_memory",
6481
+ "harmony_process_command",
6482
+ "harmony_sync"
6483
+ ];
6436
6484
  var GLOBAL_SKILLS_DIR = join7(homedir6(), ".agents", "skills");
6437
6485
  var API_URL = "https://app.gethmy.com/api";
6438
6486
  async function registerMcpServer() {
@@ -6455,9 +6503,7 @@ async function writeMcpConfigFallback(home) {
6455
6503
  }
6456
6504
  let settings = {};
6457
6505
  if (existsSync9(settingsPath)) {
6458
- try {
6459
- settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
6460
- } catch {}
6506
+ settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
6461
6507
  }
6462
6508
  const mcpServers = settings.mcpServers || {};
6463
6509
  mcpServers.harmony = {
@@ -6467,6 +6513,31 @@ async function writeMcpConfigFallback(home) {
6467
6513
  settings.mcpServers = mcpServers;
6468
6514
  writeFileSync5(settingsPath, JSON.stringify(settings, null, 2));
6469
6515
  }
6516
+ async function allowlistHarmonyTools(home, allowAll) {
6517
+ const { readFileSync: readFileSync7, writeFileSync: writeFileSync5, mkdirSync: mkdirSync6, existsSync: existsSync9 } = await import("node:fs");
6518
+ const settingsPath = join7(home, ".claude", "settings.json");
6519
+ const settingsDir = dirname3(settingsPath);
6520
+ if (!existsSync9(settingsDir)) {
6521
+ mkdirSync6(settingsDir, { recursive: true });
6522
+ }
6523
+ let settings = {};
6524
+ if (existsSync9(settingsPath)) {
6525
+ settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
6526
+ }
6527
+ const permissions = settings.permissions || {};
6528
+ const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
6529
+ const hasBlanket = allow.includes("mcp__harmony") || allow.includes("mcp__harmony__*");
6530
+ const wanted = allowAll ? ["mcp__harmony"] : SAFE_HARMONY_TOOLS.map((t) => `mcp__harmony__${t}`);
6531
+ const missing = hasBlanket ? [] : wanted.filter((r) => !allow.includes(r));
6532
+ if (missing.length === 0) {
6533
+ return "already";
6534
+ }
6535
+ allow.push(...missing);
6536
+ permissions.allow = allow;
6537
+ settings.permissions = permissions;
6538
+ writeFileSync5(settingsPath, JSON.stringify(settings, null, 2));
6539
+ return "added";
6540
+ }
6470
6541
  async function validateApiKey(apiKey, apiUrl = API_URL) {
6471
6542
  try {
6472
6543
  const response = await fetch(`${apiUrl}/v1/workspaces`, {
@@ -7231,6 +7302,26 @@ async function runSetup(options = {}) {
7231
7302
  }
7232
7303
  }
7233
7304
  }
7305
+ if (claudeDetected || selectedAgents.includes("claude")) {
7306
+ const allowAll = options.allowAllTools === true;
7307
+ 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.";
7308
+ const allowTools = await p3.confirm({ message, initialValue: true });
7309
+ if (p3.isCancel(allowTools)) {
7310
+ p3.cancel("Setup cancelled.");
7311
+ process.exit(0);
7312
+ }
7313
+ if (allowTools) {
7314
+ try {
7315
+ const result = await allowlistHarmonyTools(home, allowAll);
7316
+ const scope = allowAll ? "all tools" : "safe tools";
7317
+ console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join7(home, ".claude", "settings.json"), home))} ${colors.dim(result === "added" ? `(${scope} allowlisted)` : `(${scope} already allowlisted)`)}`);
7318
+ } catch {
7319
+ 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.");
7320
+ }
7321
+ } else {
7322
+ console.log(` ${colors.dim("Skipped tool allowlist — you'll be prompted per tool, or run /permissions in Claude Code later.")}`);
7323
+ }
7324
+ }
7234
7325
  if (selectedWorkspaceId || selectedProjectId) {
7235
7326
  const localConfig = {};
7236
7327
  if (selectedWorkspaceId)
@@ -7361,7 +7452,7 @@ program.command("reset").description("Remove stored configuration").action(() =>
7361
7452
  console.log(`
7362
7453
  To reconfigure, run: npx @gethmy/mcp setup`);
7363
7454
  });
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) => {
7455
+ 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
7456
  await runSetup({
7366
7457
  force: options.force,
7367
7458
  apiKey: options.apiKey,
@@ -7374,7 +7465,8 @@ program.command("setup").description("Smart setup wizard for Harmony MCP (recomm
7374
7465
  skipContext: options.skipContext,
7375
7466
  skipDocs: options.skipDocs,
7376
7467
  newAccount: options.new,
7377
- name: options.name
7468
+ name: options.name,
7469
+ allowAllTools: options.allowAllTools
7378
7470
  });
7379
7471
  });
7380
7472
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.8.2",
3
+ "version": "2.8.3",
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/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/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 });