@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 +150 -10
- package/dist/index.js +51 -3
- package/dist/lib/api-client.js +3 -0
- package/package.json +1 -1
- package/src/api-client.ts +8 -0
- package/src/auto-session.ts +1 -0
- package/src/cli.ts +5 -0
- package/src/server.ts +59 -0
- package/src/tui/setup.ts +155 -6
- package/src/tui/writer.ts +6 -5
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
|
|
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
|
-
|
|
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);
|
package/dist/lib/api-client.js
CHANGED
|
@@ -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
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
|
}
|
package/src/auto-session.ts
CHANGED
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
|
|
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
|
-
|
|
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 });
|