@gethmy/mcp 2.8.1 → 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 +323 -7
- package/dist/index.js +224 -0
- package/dist/lib/api-client.js +121 -0
- package/package.json +1 -1
- package/src/api-client.ts +65 -1
- package/src/cli.ts +5 -0
- package/src/server.ts +131 -0
- package/src/tui/setup.ts +155 -6
- package/src/tui/writer.ts +6 -5
package/dist/cli.js
CHANGED
|
@@ -1121,6 +1121,87 @@ function getDisplayLinkType(linkType, direction) {
|
|
|
1121
1121
|
return linkType;
|
|
1122
1122
|
return LINK_TYPE_INVERSES[linkType];
|
|
1123
1123
|
}
|
|
1124
|
+
// ../harmony-shared/dist/commentSerializer.js
|
|
1125
|
+
var CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
|
|
1126
|
+
function authorLabel(c) {
|
|
1127
|
+
if (c.author_type === "agent")
|
|
1128
|
+
return "AI agent";
|
|
1129
|
+
return c.author?.full_name || c.author?.email || "teammate";
|
|
1130
|
+
}
|
|
1131
|
+
function criticalIds(comments) {
|
|
1132
|
+
const keep = new Set;
|
|
1133
|
+
for (const c of comments) {
|
|
1134
|
+
if (c.comment_type === "decision")
|
|
1135
|
+
keep.add(c.id);
|
|
1136
|
+
if (c.supersedes_id) {
|
|
1137
|
+
keep.add(c.id);
|
|
1138
|
+
keep.add(c.supersedes_id);
|
|
1139
|
+
}
|
|
1140
|
+
if (c.confirms_id) {
|
|
1141
|
+
keep.add(c.id);
|
|
1142
|
+
keep.add(c.confirms_id);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return keep;
|
|
1146
|
+
}
|
|
1147
|
+
function serializeCommentThread(comments, options = {}) {
|
|
1148
|
+
const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
|
|
1149
|
+
const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
1150
|
+
if (visible.length === 0)
|
|
1151
|
+
return "";
|
|
1152
|
+
const indexById = new Map;
|
|
1153
|
+
visible.forEach((c, i) => {
|
|
1154
|
+
indexById.set(c.id, i + 1);
|
|
1155
|
+
});
|
|
1156
|
+
let rendered = visible;
|
|
1157
|
+
let elidedCount = 0;
|
|
1158
|
+
if (maxComments && visible.length > maxComments) {
|
|
1159
|
+
const keep = criticalIds(visible);
|
|
1160
|
+
const recentThreshold = visible.length - maxComments;
|
|
1161
|
+
rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
|
|
1162
|
+
elidedCount = visible.length - rendered.length;
|
|
1163
|
+
}
|
|
1164
|
+
const ref = (id) => {
|
|
1165
|
+
const n = indexById.get(id);
|
|
1166
|
+
return n ? `#${n}` : `#${id.slice(0, 8)}`;
|
|
1167
|
+
};
|
|
1168
|
+
const lines = [];
|
|
1169
|
+
if (elidedCount > 0) {
|
|
1170
|
+
lines.push({
|
|
1171
|
+
at: visible[0]?.created_at ?? "",
|
|
1172
|
+
text: `(${elidedCount} earlier comment(s) omitted for brevity)`
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
for (const c of rendered) {
|
|
1176
|
+
const tags = [];
|
|
1177
|
+
if (c.edited_at)
|
|
1178
|
+
tags.push("edited");
|
|
1179
|
+
if (c.supersedes_id)
|
|
1180
|
+
tags.push(`supersedes ${ref(c.supersedes_id)}`);
|
|
1181
|
+
if (c.confirms_id)
|
|
1182
|
+
tags.push(`confirms ${ref(c.confirms_id)}`);
|
|
1183
|
+
if (c.resolved_at)
|
|
1184
|
+
tags.push("resolved");
|
|
1185
|
+
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
1186
|
+
const header = `[${ref(c.id)} | ${c.author_type} | ${authorLabel(c)} | ${c.comment_type} | ${c.created_at}${tagStr}]`;
|
|
1187
|
+
lines.push({ at: c.created_at, text: `${header}
|
|
1188
|
+
${c.body.trim()}` });
|
|
1189
|
+
}
|
|
1190
|
+
for (const a of activity) {
|
|
1191
|
+
const actor = a.actor ? `${a.actor} ` : "";
|
|
1192
|
+
lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
|
|
1193
|
+
}
|
|
1194
|
+
lines.sort((a, b) => a.at.localeCompare(b.at));
|
|
1195
|
+
const body = lines.map((l) => l.text).join(`
|
|
1196
|
+
|
|
1197
|
+
`);
|
|
1198
|
+
const instruction = includeInstructions ? `
|
|
1199
|
+
|
|
1200
|
+
${CONFLICT_INSTRUCTION}` : "";
|
|
1201
|
+
return `## ${heading} (oldest → newest)
|
|
1202
|
+
|
|
1203
|
+
${body}${instruction}`;
|
|
1204
|
+
}
|
|
1124
1205
|
// ../harmony-shared/dist/constants.js
|
|
1125
1206
|
var TIMINGS = {
|
|
1126
1207
|
SEARCH_DEBOUNCE: 300,
|
|
@@ -1473,6 +1554,28 @@ class HarmonyApiClient {
|
|
|
1473
1554
|
async deleteSubtask(subtaskId) {
|
|
1474
1555
|
return this.request("DELETE", `/subtasks/${subtaskId}`);
|
|
1475
1556
|
}
|
|
1557
|
+
async addComment(cardId, body, opts) {
|
|
1558
|
+
return this.request("POST", `/cards/${cardId}/comments`, {
|
|
1559
|
+
body,
|
|
1560
|
+
authorType: "agent",
|
|
1561
|
+
commentType: opts?.commentType,
|
|
1562
|
+
supersedesId: opts?.supersedesId,
|
|
1563
|
+
confirmsId: opts?.confirmsId,
|
|
1564
|
+
agentSessionId: opts?.agentSessionId
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
async getComments(cardId, opts) {
|
|
1568
|
+
const qs = new URLSearchParams;
|
|
1569
|
+
if (opts?.limit != null)
|
|
1570
|
+
qs.set("limit", String(opts.limit));
|
|
1571
|
+
if (opts?.offset != null)
|
|
1572
|
+
qs.set("offset", String(opts.offset));
|
|
1573
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
1574
|
+
return this.request("GET", `/cards/${cardId}/comments${suffix}`);
|
|
1575
|
+
}
|
|
1576
|
+
async updateComment(commentId, updates) {
|
|
1577
|
+
return this.request("PATCH", `/comments/${commentId}`, updates);
|
|
1578
|
+
}
|
|
1476
1579
|
async startAgentSession(cardId, data) {
|
|
1477
1580
|
return this.request("POST", `/cards/${cardId}/agent-context`, data);
|
|
1478
1581
|
}
|
|
@@ -1830,6 +1933,24 @@ class HarmonyApiClient {
|
|
|
1830
1933
|
assembledContext: assembledContextStr,
|
|
1831
1934
|
assemblyId
|
|
1832
1935
|
});
|
|
1936
|
+
try {
|
|
1937
|
+
const { comments } = await this.getComments(options.cardId, {
|
|
1938
|
+
limit: 200
|
|
1939
|
+
});
|
|
1940
|
+
if (Array.isArray(comments) && comments.length > 0) {
|
|
1941
|
+
const section = serializeCommentThread(comments, {
|
|
1942
|
+
heading: "Comments",
|
|
1943
|
+
maxComments: 40
|
|
1944
|
+
});
|
|
1945
|
+
if (section)
|
|
1946
|
+
result.prompt = `${result.prompt}
|
|
1947
|
+
|
|
1948
|
+
${section}`;
|
|
1949
|
+
}
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1952
|
+
console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
|
|
1953
|
+
}
|
|
1833
1954
|
try {
|
|
1834
1955
|
await this.recordPromptHistory({
|
|
1835
1956
|
cardId: cardData.id,
|
|
@@ -3132,6 +3253,66 @@ var TOOLS = {
|
|
|
3132
3253
|
required: ["subtaskId"]
|
|
3133
3254
|
}
|
|
3134
3255
|
},
|
|
3256
|
+
harmony_add_comment: {
|
|
3257
|
+
description: "Post a comment on a card as the agent. Use this to converse with the human in the open: report progress, ask a question, record a decision, or note a finding — instead of editing the card description. Set supersedesId to correct an earlier comment, confirmsId to reaffirm one. When the thread conflicts, prefer the latest comment unless a later one confirms an earlier finding; cite the comment id(s) you relied on.",
|
|
3258
|
+
inputSchema: {
|
|
3259
|
+
type: "object",
|
|
3260
|
+
properties: {
|
|
3261
|
+
cardId: { type: "string", description: "Card UUID to comment on" },
|
|
3262
|
+
body: { type: "string", description: "Comment body (Markdown)" },
|
|
3263
|
+
commentType: {
|
|
3264
|
+
type: "string",
|
|
3265
|
+
enum: [
|
|
3266
|
+
"message",
|
|
3267
|
+
"progress",
|
|
3268
|
+
"question",
|
|
3269
|
+
"blocker",
|
|
3270
|
+
"decision",
|
|
3271
|
+
"summary",
|
|
3272
|
+
"finding"
|
|
3273
|
+
],
|
|
3274
|
+
description: "Type of comment. 'question'/'blocker' signal you need a human; default 'message'."
|
|
3275
|
+
},
|
|
3276
|
+
supersedesId: {
|
|
3277
|
+
type: "string",
|
|
3278
|
+
description: "Comment id this comment corrects/updates"
|
|
3279
|
+
},
|
|
3280
|
+
confirmsId: {
|
|
3281
|
+
type: "string",
|
|
3282
|
+
description: "Comment id this comment reaffirms"
|
|
3283
|
+
}
|
|
3284
|
+
},
|
|
3285
|
+
required: ["cardId", "body"]
|
|
3286
|
+
}
|
|
3287
|
+
},
|
|
3288
|
+
harmony_get_comments: {
|
|
3289
|
+
description: "Get the comment thread on a card (oldest → newest). Returns human + agent comments with author, type, edited/resolved state, and supersede/confirm links. Read this before acting so you weigh later comments over earlier ones they contradict.",
|
|
3290
|
+
inputSchema: {
|
|
3291
|
+
type: "object",
|
|
3292
|
+
properties: {
|
|
3293
|
+
cardId: { type: "string" },
|
|
3294
|
+
limit: { type: "number" },
|
|
3295
|
+
offset: { type: "number" }
|
|
3296
|
+
},
|
|
3297
|
+
required: ["cardId"]
|
|
3298
|
+
}
|
|
3299
|
+
},
|
|
3300
|
+
harmony_update_comment: {
|
|
3301
|
+
description: "Update one of your own comments: edit the body, pin it, or resolve a question/blocker once it has been answered.",
|
|
3302
|
+
inputSchema: {
|
|
3303
|
+
type: "object",
|
|
3304
|
+
properties: {
|
|
3305
|
+
commentId: { type: "string" },
|
|
3306
|
+
body: { type: "string" },
|
|
3307
|
+
pinned: { type: "boolean" },
|
|
3308
|
+
resolve: {
|
|
3309
|
+
type: "boolean",
|
|
3310
|
+
description: "Mark (true) or clear (false) the resolved state"
|
|
3311
|
+
}
|
|
3312
|
+
},
|
|
3313
|
+
required: ["commentId"]
|
|
3314
|
+
}
|
|
3315
|
+
},
|
|
3135
3316
|
harmony_list_workspaces: {
|
|
3136
3317
|
description: "List all workspaces the user has access to",
|
|
3137
3318
|
inputSchema: {
|
|
@@ -4393,6 +4574,49 @@ async function handleToolCall(name, args, deps) {
|
|
|
4393
4574
|
await client3.deleteSubtask(subtaskId);
|
|
4394
4575
|
return { success: true };
|
|
4395
4576
|
}
|
|
4577
|
+
case "harmony_add_comment": {
|
|
4578
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
4579
|
+
const body = z.string().min(1).max(1e4).parse(args.body);
|
|
4580
|
+
const commentType = args.commentType !== undefined ? z.enum([
|
|
4581
|
+
"message",
|
|
4582
|
+
"progress",
|
|
4583
|
+
"question",
|
|
4584
|
+
"blocker",
|
|
4585
|
+
"decision",
|
|
4586
|
+
"summary",
|
|
4587
|
+
"finding"
|
|
4588
|
+
]).parse(args.commentType) : undefined;
|
|
4589
|
+
const supersedesId = args.supersedesId !== undefined ? z.string().uuid().parse(args.supersedesId) : undefined;
|
|
4590
|
+
const confirmsId = args.confirmsId !== undefined ? z.string().uuid().parse(args.confirmsId) : undefined;
|
|
4591
|
+
const result = await client3.addComment(cardId, body, {
|
|
4592
|
+
commentType,
|
|
4593
|
+
supersedesId,
|
|
4594
|
+
confirmsId
|
|
4595
|
+
});
|
|
4596
|
+
return { success: true, ...result };
|
|
4597
|
+
}
|
|
4598
|
+
case "harmony_get_comments": {
|
|
4599
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
4600
|
+
const limit = args.limit !== undefined ? z.number().int().min(1).max(500).parse(args.limit) : undefined;
|
|
4601
|
+
const offset = args.offset !== undefined ? z.number().int().min(0).parse(args.offset) : undefined;
|
|
4602
|
+
const result = await client3.getComments(cardId, { limit, offset });
|
|
4603
|
+
return { success: true, ...result };
|
|
4604
|
+
}
|
|
4605
|
+
case "harmony_update_comment": {
|
|
4606
|
+
const commentId = z.string().uuid().parse(args.commentId);
|
|
4607
|
+
const updates = {};
|
|
4608
|
+
if (args.body !== undefined) {
|
|
4609
|
+
updates.body = z.string().min(1).max(1e4).parse(args.body);
|
|
4610
|
+
}
|
|
4611
|
+
if (args.pinned !== undefined) {
|
|
4612
|
+
updates.pinned = z.boolean().parse(args.pinned);
|
|
4613
|
+
}
|
|
4614
|
+
if (args.resolve !== undefined) {
|
|
4615
|
+
updates.resolve = z.boolean().parse(args.resolve);
|
|
4616
|
+
}
|
|
4617
|
+
const result = await client3.updateComment(commentId, updates);
|
|
4618
|
+
return { success: true, ...result };
|
|
4619
|
+
}
|
|
4396
4620
|
case "harmony_list_workspaces": {
|
|
4397
4621
|
const result = await client3.listWorkspaces();
|
|
4398
4622
|
return { success: true, ...result };
|
|
@@ -6133,9 +6357,10 @@ function appendToToml(filePath, section, content, options = {}) {
|
|
|
6133
6357
|
}
|
|
6134
6358
|
try {
|
|
6135
6359
|
const existing = readFileSync6(filePath, "utf-8");
|
|
6136
|
-
if (existing.includes(section)) {
|
|
6360
|
+
if (existing.includes(`[${section}]`)) {
|
|
6137
6361
|
if (options.force) {
|
|
6138
|
-
const
|
|
6362
|
+
const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6363
|
+
const updated = existing.replace(new RegExp(`(?:#[^\\n]*\\n)*\\[${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`), content.trim() + `
|
|
6139
6364
|
|
|
6140
6365
|
`);
|
|
6141
6366
|
writeFileSync4(filePath, updated, { mode: 420 });
|
|
@@ -6209,6 +6434,53 @@ function getWriteSummary(files, options = {}) {
|
|
|
6209
6434
|
}
|
|
6210
6435
|
|
|
6211
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
|
+
];
|
|
6212
6484
|
var GLOBAL_SKILLS_DIR = join7(homedir6(), ".agents", "skills");
|
|
6213
6485
|
var API_URL = "https://app.gethmy.com/api";
|
|
6214
6486
|
async function registerMcpServer() {
|
|
@@ -6231,9 +6503,7 @@ async function writeMcpConfigFallback(home) {
|
|
|
6231
6503
|
}
|
|
6232
6504
|
let settings = {};
|
|
6233
6505
|
if (existsSync9(settingsPath)) {
|
|
6234
|
-
|
|
6235
|
-
settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
|
|
6236
|
-
} catch {}
|
|
6506
|
+
settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
|
|
6237
6507
|
}
|
|
6238
6508
|
const mcpServers = settings.mcpServers || {};
|
|
6239
6509
|
mcpServers.harmony = {
|
|
@@ -6243,6 +6513,31 @@ async function writeMcpConfigFallback(home) {
|
|
|
6243
6513
|
settings.mcpServers = mcpServers;
|
|
6244
6514
|
writeFileSync5(settingsPath, JSON.stringify(settings, null, 2));
|
|
6245
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
|
+
}
|
|
6246
6541
|
async function validateApiKey(apiKey, apiUrl = API_URL) {
|
|
6247
6542
|
try {
|
|
6248
6543
|
const response = await fetch(`${apiUrl}/v1/workspaces`, {
|
|
@@ -7007,6 +7302,26 @@ async function runSetup(options = {}) {
|
|
|
7007
7302
|
}
|
|
7008
7303
|
}
|
|
7009
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
|
+
}
|
|
7010
7325
|
if (selectedWorkspaceId || selectedProjectId) {
|
|
7011
7326
|
const localConfig = {};
|
|
7012
7327
|
if (selectedWorkspaceId)
|
|
@@ -7137,7 +7452,7 @@ program.command("reset").description("Remove stored configuration").action(() =>
|
|
|
7137
7452
|
console.log(`
|
|
7138
7453
|
To reconfigure, run: npx @gethmy/mcp setup`);
|
|
7139
7454
|
});
|
|
7140
|
-
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) => {
|
|
7141
7456
|
await runSetup({
|
|
7142
7457
|
force: options.force,
|
|
7143
7458
|
apiKey: options.apiKey,
|
|
@@ -7150,7 +7465,8 @@ program.command("setup").description("Smart setup wizard for Harmony MCP (recomm
|
|
|
7150
7465
|
skipContext: options.skipContext,
|
|
7151
7466
|
skipDocs: options.skipDocs,
|
|
7152
7467
|
newAccount: options.new,
|
|
7153
|
-
name: options.name
|
|
7468
|
+
name: options.name,
|
|
7469
|
+
allowAllTools: options.allowAllTools
|
|
7154
7470
|
});
|
|
7155
7471
|
});
|
|
7156
7472
|
program.parse();
|
package/dist/index.js
CHANGED
|
@@ -932,6 +932,87 @@ function getDisplayLinkType(linkType, direction) {
|
|
|
932
932
|
return linkType;
|
|
933
933
|
return LINK_TYPE_INVERSES[linkType];
|
|
934
934
|
}
|
|
935
|
+
// ../harmony-shared/dist/commentSerializer.js
|
|
936
|
+
var CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
|
|
937
|
+
function authorLabel(c) {
|
|
938
|
+
if (c.author_type === "agent")
|
|
939
|
+
return "AI agent";
|
|
940
|
+
return c.author?.full_name || c.author?.email || "teammate";
|
|
941
|
+
}
|
|
942
|
+
function criticalIds(comments) {
|
|
943
|
+
const keep = new Set;
|
|
944
|
+
for (const c of comments) {
|
|
945
|
+
if (c.comment_type === "decision")
|
|
946
|
+
keep.add(c.id);
|
|
947
|
+
if (c.supersedes_id) {
|
|
948
|
+
keep.add(c.id);
|
|
949
|
+
keep.add(c.supersedes_id);
|
|
950
|
+
}
|
|
951
|
+
if (c.confirms_id) {
|
|
952
|
+
keep.add(c.id);
|
|
953
|
+
keep.add(c.confirms_id);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return keep;
|
|
957
|
+
}
|
|
958
|
+
function serializeCommentThread(comments, options = {}) {
|
|
959
|
+
const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
|
|
960
|
+
const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
961
|
+
if (visible.length === 0)
|
|
962
|
+
return "";
|
|
963
|
+
const indexById = new Map;
|
|
964
|
+
visible.forEach((c, i) => {
|
|
965
|
+
indexById.set(c.id, i + 1);
|
|
966
|
+
});
|
|
967
|
+
let rendered = visible;
|
|
968
|
+
let elidedCount = 0;
|
|
969
|
+
if (maxComments && visible.length > maxComments) {
|
|
970
|
+
const keep = criticalIds(visible);
|
|
971
|
+
const recentThreshold = visible.length - maxComments;
|
|
972
|
+
rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
|
|
973
|
+
elidedCount = visible.length - rendered.length;
|
|
974
|
+
}
|
|
975
|
+
const ref = (id) => {
|
|
976
|
+
const n = indexById.get(id);
|
|
977
|
+
return n ? `#${n}` : `#${id.slice(0, 8)}`;
|
|
978
|
+
};
|
|
979
|
+
const lines = [];
|
|
980
|
+
if (elidedCount > 0) {
|
|
981
|
+
lines.push({
|
|
982
|
+
at: visible[0]?.created_at ?? "",
|
|
983
|
+
text: `(${elidedCount} earlier comment(s) omitted for brevity)`
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
for (const c of rendered) {
|
|
987
|
+
const tags = [];
|
|
988
|
+
if (c.edited_at)
|
|
989
|
+
tags.push("edited");
|
|
990
|
+
if (c.supersedes_id)
|
|
991
|
+
tags.push(`supersedes ${ref(c.supersedes_id)}`);
|
|
992
|
+
if (c.confirms_id)
|
|
993
|
+
tags.push(`confirms ${ref(c.confirms_id)}`);
|
|
994
|
+
if (c.resolved_at)
|
|
995
|
+
tags.push("resolved");
|
|
996
|
+
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
997
|
+
const header = `[${ref(c.id)} | ${c.author_type} | ${authorLabel(c)} | ${c.comment_type} | ${c.created_at}${tagStr}]`;
|
|
998
|
+
lines.push({ at: c.created_at, text: `${header}
|
|
999
|
+
${c.body.trim()}` });
|
|
1000
|
+
}
|
|
1001
|
+
for (const a of activity) {
|
|
1002
|
+
const actor = a.actor ? `${a.actor} ` : "";
|
|
1003
|
+
lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
|
|
1004
|
+
}
|
|
1005
|
+
lines.sort((a, b) => a.at.localeCompare(b.at));
|
|
1006
|
+
const body = lines.map((l) => l.text).join(`
|
|
1007
|
+
|
|
1008
|
+
`);
|
|
1009
|
+
const instruction = includeInstructions ? `
|
|
1010
|
+
|
|
1011
|
+
${CONFLICT_INSTRUCTION}` : "";
|
|
1012
|
+
return `## ${heading} (oldest → newest)
|
|
1013
|
+
|
|
1014
|
+
${body}${instruction}`;
|
|
1015
|
+
}
|
|
935
1016
|
// ../harmony-shared/dist/constants.js
|
|
936
1017
|
var TIMINGS = {
|
|
937
1018
|
SEARCH_DEBOUNCE: 300,
|
|
@@ -1469,6 +1550,28 @@ class HarmonyApiClient {
|
|
|
1469
1550
|
async deleteSubtask(subtaskId) {
|
|
1470
1551
|
return this.request("DELETE", `/subtasks/${subtaskId}`);
|
|
1471
1552
|
}
|
|
1553
|
+
async addComment(cardId, body, opts) {
|
|
1554
|
+
return this.request("POST", `/cards/${cardId}/comments`, {
|
|
1555
|
+
body,
|
|
1556
|
+
authorType: "agent",
|
|
1557
|
+
commentType: opts?.commentType,
|
|
1558
|
+
supersedesId: opts?.supersedesId,
|
|
1559
|
+
confirmsId: opts?.confirmsId,
|
|
1560
|
+
agentSessionId: opts?.agentSessionId
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
async getComments(cardId, opts) {
|
|
1564
|
+
const qs = new URLSearchParams;
|
|
1565
|
+
if (opts?.limit != null)
|
|
1566
|
+
qs.set("limit", String(opts.limit));
|
|
1567
|
+
if (opts?.offset != null)
|
|
1568
|
+
qs.set("offset", String(opts.offset));
|
|
1569
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
1570
|
+
return this.request("GET", `/cards/${cardId}/comments${suffix}`);
|
|
1571
|
+
}
|
|
1572
|
+
async updateComment(commentId, updates) {
|
|
1573
|
+
return this.request("PATCH", `/comments/${commentId}`, updates);
|
|
1574
|
+
}
|
|
1472
1575
|
async startAgentSession(cardId, data) {
|
|
1473
1576
|
return this.request("POST", `/cards/${cardId}/agent-context`, data);
|
|
1474
1577
|
}
|
|
@@ -1826,6 +1929,24 @@ class HarmonyApiClient {
|
|
|
1826
1929
|
assembledContext: assembledContextStr,
|
|
1827
1930
|
assemblyId
|
|
1828
1931
|
});
|
|
1932
|
+
try {
|
|
1933
|
+
const { comments } = await this.getComments(options.cardId, {
|
|
1934
|
+
limit: 200
|
|
1935
|
+
});
|
|
1936
|
+
if (Array.isArray(comments) && comments.length > 0) {
|
|
1937
|
+
const section = serializeCommentThread(comments, {
|
|
1938
|
+
heading: "Comments",
|
|
1939
|
+
maxComments: 40
|
|
1940
|
+
});
|
|
1941
|
+
if (section)
|
|
1942
|
+
result.prompt = `${result.prompt}
|
|
1943
|
+
|
|
1944
|
+
${section}`;
|
|
1945
|
+
}
|
|
1946
|
+
} catch (err) {
|
|
1947
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1948
|
+
console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
|
|
1949
|
+
}
|
|
1829
1950
|
try {
|
|
1830
1951
|
await this.recordPromptHistory({
|
|
1831
1952
|
cardId: cardData.id,
|
|
@@ -3128,6 +3249,66 @@ var TOOLS = {
|
|
|
3128
3249
|
required: ["subtaskId"]
|
|
3129
3250
|
}
|
|
3130
3251
|
},
|
|
3252
|
+
harmony_add_comment: {
|
|
3253
|
+
description: "Post a comment on a card as the agent. Use this to converse with the human in the open: report progress, ask a question, record a decision, or note a finding — instead of editing the card description. Set supersedesId to correct an earlier comment, confirmsId to reaffirm one. When the thread conflicts, prefer the latest comment unless a later one confirms an earlier finding; cite the comment id(s) you relied on.",
|
|
3254
|
+
inputSchema: {
|
|
3255
|
+
type: "object",
|
|
3256
|
+
properties: {
|
|
3257
|
+
cardId: { type: "string", description: "Card UUID to comment on" },
|
|
3258
|
+
body: { type: "string", description: "Comment body (Markdown)" },
|
|
3259
|
+
commentType: {
|
|
3260
|
+
type: "string",
|
|
3261
|
+
enum: [
|
|
3262
|
+
"message",
|
|
3263
|
+
"progress",
|
|
3264
|
+
"question",
|
|
3265
|
+
"blocker",
|
|
3266
|
+
"decision",
|
|
3267
|
+
"summary",
|
|
3268
|
+
"finding"
|
|
3269
|
+
],
|
|
3270
|
+
description: "Type of comment. 'question'/'blocker' signal you need a human; default 'message'."
|
|
3271
|
+
},
|
|
3272
|
+
supersedesId: {
|
|
3273
|
+
type: "string",
|
|
3274
|
+
description: "Comment id this comment corrects/updates"
|
|
3275
|
+
},
|
|
3276
|
+
confirmsId: {
|
|
3277
|
+
type: "string",
|
|
3278
|
+
description: "Comment id this comment reaffirms"
|
|
3279
|
+
}
|
|
3280
|
+
},
|
|
3281
|
+
required: ["cardId", "body"]
|
|
3282
|
+
}
|
|
3283
|
+
},
|
|
3284
|
+
harmony_get_comments: {
|
|
3285
|
+
description: "Get the comment thread on a card (oldest → newest). Returns human + agent comments with author, type, edited/resolved state, and supersede/confirm links. Read this before acting so you weigh later comments over earlier ones they contradict.",
|
|
3286
|
+
inputSchema: {
|
|
3287
|
+
type: "object",
|
|
3288
|
+
properties: {
|
|
3289
|
+
cardId: { type: "string" },
|
|
3290
|
+
limit: { type: "number" },
|
|
3291
|
+
offset: { type: "number" }
|
|
3292
|
+
},
|
|
3293
|
+
required: ["cardId"]
|
|
3294
|
+
}
|
|
3295
|
+
},
|
|
3296
|
+
harmony_update_comment: {
|
|
3297
|
+
description: "Update one of your own comments: edit the body, pin it, or resolve a question/blocker once it has been answered.",
|
|
3298
|
+
inputSchema: {
|
|
3299
|
+
type: "object",
|
|
3300
|
+
properties: {
|
|
3301
|
+
commentId: { type: "string" },
|
|
3302
|
+
body: { type: "string" },
|
|
3303
|
+
pinned: { type: "boolean" },
|
|
3304
|
+
resolve: {
|
|
3305
|
+
type: "boolean",
|
|
3306
|
+
description: "Mark (true) or clear (false) the resolved state"
|
|
3307
|
+
}
|
|
3308
|
+
},
|
|
3309
|
+
required: ["commentId"]
|
|
3310
|
+
}
|
|
3311
|
+
},
|
|
3131
3312
|
harmony_list_workspaces: {
|
|
3132
3313
|
description: "List all workspaces the user has access to",
|
|
3133
3314
|
inputSchema: {
|
|
@@ -4389,6 +4570,49 @@ async function handleToolCall(name, args, deps) {
|
|
|
4389
4570
|
await client3.deleteSubtask(subtaskId);
|
|
4390
4571
|
return { success: true };
|
|
4391
4572
|
}
|
|
4573
|
+
case "harmony_add_comment": {
|
|
4574
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
4575
|
+
const body = z.string().min(1).max(1e4).parse(args.body);
|
|
4576
|
+
const commentType = args.commentType !== undefined ? z.enum([
|
|
4577
|
+
"message",
|
|
4578
|
+
"progress",
|
|
4579
|
+
"question",
|
|
4580
|
+
"blocker",
|
|
4581
|
+
"decision",
|
|
4582
|
+
"summary",
|
|
4583
|
+
"finding"
|
|
4584
|
+
]).parse(args.commentType) : undefined;
|
|
4585
|
+
const supersedesId = args.supersedesId !== undefined ? z.string().uuid().parse(args.supersedesId) : undefined;
|
|
4586
|
+
const confirmsId = args.confirmsId !== undefined ? z.string().uuid().parse(args.confirmsId) : undefined;
|
|
4587
|
+
const result = await client3.addComment(cardId, body, {
|
|
4588
|
+
commentType,
|
|
4589
|
+
supersedesId,
|
|
4590
|
+
confirmsId
|
|
4591
|
+
});
|
|
4592
|
+
return { success: true, ...result };
|
|
4593
|
+
}
|
|
4594
|
+
case "harmony_get_comments": {
|
|
4595
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
4596
|
+
const limit = args.limit !== undefined ? z.number().int().min(1).max(500).parse(args.limit) : undefined;
|
|
4597
|
+
const offset = args.offset !== undefined ? z.number().int().min(0).parse(args.offset) : undefined;
|
|
4598
|
+
const result = await client3.getComments(cardId, { limit, offset });
|
|
4599
|
+
return { success: true, ...result };
|
|
4600
|
+
}
|
|
4601
|
+
case "harmony_update_comment": {
|
|
4602
|
+
const commentId = z.string().uuid().parse(args.commentId);
|
|
4603
|
+
const updates = {};
|
|
4604
|
+
if (args.body !== undefined) {
|
|
4605
|
+
updates.body = z.string().min(1).max(1e4).parse(args.body);
|
|
4606
|
+
}
|
|
4607
|
+
if (args.pinned !== undefined) {
|
|
4608
|
+
updates.pinned = z.boolean().parse(args.pinned);
|
|
4609
|
+
}
|
|
4610
|
+
if (args.resolve !== undefined) {
|
|
4611
|
+
updates.resolve = z.boolean().parse(args.resolve);
|
|
4612
|
+
}
|
|
4613
|
+
const result = await client3.updateComment(commentId, updates);
|
|
4614
|
+
return { success: true, ...result };
|
|
4615
|
+
}
|
|
4392
4616
|
case "harmony_list_workspaces": {
|
|
4393
4617
|
const result = await client3.listWorkspaces();
|
|
4394
4618
|
return { success: true, ...result };
|
package/dist/lib/api-client.js
CHANGED
|
@@ -724,6 +724,87 @@ function getDisplayLinkType(linkType, direction) {
|
|
|
724
724
|
return linkType;
|
|
725
725
|
return LINK_TYPE_INVERSES[linkType];
|
|
726
726
|
}
|
|
727
|
+
// ../harmony-shared/dist/commentSerializer.js
|
|
728
|
+
var CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
|
|
729
|
+
function authorLabel(c) {
|
|
730
|
+
if (c.author_type === "agent")
|
|
731
|
+
return "AI agent";
|
|
732
|
+
return c.author?.full_name || c.author?.email || "teammate";
|
|
733
|
+
}
|
|
734
|
+
function criticalIds(comments) {
|
|
735
|
+
const keep = new Set;
|
|
736
|
+
for (const c of comments) {
|
|
737
|
+
if (c.comment_type === "decision")
|
|
738
|
+
keep.add(c.id);
|
|
739
|
+
if (c.supersedes_id) {
|
|
740
|
+
keep.add(c.id);
|
|
741
|
+
keep.add(c.supersedes_id);
|
|
742
|
+
}
|
|
743
|
+
if (c.confirms_id) {
|
|
744
|
+
keep.add(c.id);
|
|
745
|
+
keep.add(c.confirms_id);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return keep;
|
|
749
|
+
}
|
|
750
|
+
function serializeCommentThread(comments, options = {}) {
|
|
751
|
+
const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
|
|
752
|
+
const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
753
|
+
if (visible.length === 0)
|
|
754
|
+
return "";
|
|
755
|
+
const indexById = new Map;
|
|
756
|
+
visible.forEach((c, i) => {
|
|
757
|
+
indexById.set(c.id, i + 1);
|
|
758
|
+
});
|
|
759
|
+
let rendered = visible;
|
|
760
|
+
let elidedCount = 0;
|
|
761
|
+
if (maxComments && visible.length > maxComments) {
|
|
762
|
+
const keep = criticalIds(visible);
|
|
763
|
+
const recentThreshold = visible.length - maxComments;
|
|
764
|
+
rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
|
|
765
|
+
elidedCount = visible.length - rendered.length;
|
|
766
|
+
}
|
|
767
|
+
const ref = (id) => {
|
|
768
|
+
const n = indexById.get(id);
|
|
769
|
+
return n ? `#${n}` : `#${id.slice(0, 8)}`;
|
|
770
|
+
};
|
|
771
|
+
const lines = [];
|
|
772
|
+
if (elidedCount > 0) {
|
|
773
|
+
lines.push({
|
|
774
|
+
at: visible[0]?.created_at ?? "",
|
|
775
|
+
text: `(${elidedCount} earlier comment(s) omitted for brevity)`
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
for (const c of rendered) {
|
|
779
|
+
const tags = [];
|
|
780
|
+
if (c.edited_at)
|
|
781
|
+
tags.push("edited");
|
|
782
|
+
if (c.supersedes_id)
|
|
783
|
+
tags.push(`supersedes ${ref(c.supersedes_id)}`);
|
|
784
|
+
if (c.confirms_id)
|
|
785
|
+
tags.push(`confirms ${ref(c.confirms_id)}`);
|
|
786
|
+
if (c.resolved_at)
|
|
787
|
+
tags.push("resolved");
|
|
788
|
+
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
789
|
+
const header = `[${ref(c.id)} | ${c.author_type} | ${authorLabel(c)} | ${c.comment_type} | ${c.created_at}${tagStr}]`;
|
|
790
|
+
lines.push({ at: c.created_at, text: `${header}
|
|
791
|
+
${c.body.trim()}` });
|
|
792
|
+
}
|
|
793
|
+
for (const a of activity) {
|
|
794
|
+
const actor = a.actor ? `${a.actor} ` : "";
|
|
795
|
+
lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
|
|
796
|
+
}
|
|
797
|
+
lines.sort((a, b) => a.at.localeCompare(b.at));
|
|
798
|
+
const body = lines.map((l) => l.text).join(`
|
|
799
|
+
|
|
800
|
+
`);
|
|
801
|
+
const instruction = includeInstructions ? `
|
|
802
|
+
|
|
803
|
+
${CONFLICT_INSTRUCTION}` : "";
|
|
804
|
+
return `## ${heading} (oldest → newest)
|
|
805
|
+
|
|
806
|
+
${body}${instruction}`;
|
|
807
|
+
}
|
|
727
808
|
// ../harmony-shared/dist/constants.js
|
|
728
809
|
var TIMINGS = {
|
|
729
810
|
SEARCH_DEBOUNCE: 300,
|
|
@@ -1076,6 +1157,28 @@ class HarmonyApiClient {
|
|
|
1076
1157
|
async deleteSubtask(subtaskId) {
|
|
1077
1158
|
return this.request("DELETE", `/subtasks/${subtaskId}`);
|
|
1078
1159
|
}
|
|
1160
|
+
async addComment(cardId, body, opts) {
|
|
1161
|
+
return this.request("POST", `/cards/${cardId}/comments`, {
|
|
1162
|
+
body,
|
|
1163
|
+
authorType: "agent",
|
|
1164
|
+
commentType: opts?.commentType,
|
|
1165
|
+
supersedesId: opts?.supersedesId,
|
|
1166
|
+
confirmsId: opts?.confirmsId,
|
|
1167
|
+
agentSessionId: opts?.agentSessionId
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
async getComments(cardId, opts) {
|
|
1171
|
+
const qs = new URLSearchParams;
|
|
1172
|
+
if (opts?.limit != null)
|
|
1173
|
+
qs.set("limit", String(opts.limit));
|
|
1174
|
+
if (opts?.offset != null)
|
|
1175
|
+
qs.set("offset", String(opts.offset));
|
|
1176
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
1177
|
+
return this.request("GET", `/cards/${cardId}/comments${suffix}`);
|
|
1178
|
+
}
|
|
1179
|
+
async updateComment(commentId, updates) {
|
|
1180
|
+
return this.request("PATCH", `/comments/${commentId}`, updates);
|
|
1181
|
+
}
|
|
1079
1182
|
async startAgentSession(cardId, data) {
|
|
1080
1183
|
return this.request("POST", `/cards/${cardId}/agent-context`, data);
|
|
1081
1184
|
}
|
|
@@ -1433,6 +1536,24 @@ class HarmonyApiClient {
|
|
|
1433
1536
|
assembledContext: assembledContextStr,
|
|
1434
1537
|
assemblyId
|
|
1435
1538
|
});
|
|
1539
|
+
try {
|
|
1540
|
+
const { comments } = await this.getComments(options.cardId, {
|
|
1541
|
+
limit: 200
|
|
1542
|
+
});
|
|
1543
|
+
if (Array.isArray(comments) && comments.length > 0) {
|
|
1544
|
+
const section = serializeCommentThread(comments, {
|
|
1545
|
+
heading: "Comments",
|
|
1546
|
+
maxComments: 40
|
|
1547
|
+
});
|
|
1548
|
+
if (section)
|
|
1549
|
+
result.prompt = `${result.prompt}
|
|
1550
|
+
|
|
1551
|
+
${section}`;
|
|
1552
|
+
}
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1555
|
+
console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
|
|
1556
|
+
}
|
|
1436
1557
|
try {
|
|
1437
1558
|
await this.recordPromptHistory({
|
|
1438
1559
|
cardId: cardData.id,
|
package/package.json
CHANGED
package/src/api-client.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type Comment,
|
|
3
|
+
getDisplayLinkType,
|
|
4
|
+
serializeCommentThread,
|
|
5
|
+
} from "@harmony/shared";
|
|
2
6
|
import { getApiKey, getApiUrl } from "./config.js";
|
|
3
7
|
|
|
4
8
|
export interface ApiResponse<T = unknown> {
|
|
@@ -617,6 +621,46 @@ export class HarmonyApiClient {
|
|
|
617
621
|
return this.request("DELETE", `/subtasks/${subtaskId}`);
|
|
618
622
|
}
|
|
619
623
|
|
|
624
|
+
// ============ COMMENT OPERATIONS ============
|
|
625
|
+
|
|
626
|
+
async addComment(
|
|
627
|
+
cardId: string,
|
|
628
|
+
body: string,
|
|
629
|
+
opts?: {
|
|
630
|
+
commentType?: string;
|
|
631
|
+
supersedesId?: string;
|
|
632
|
+
confirmsId?: string;
|
|
633
|
+
agentSessionId?: string;
|
|
634
|
+
},
|
|
635
|
+
): Promise<{ comment: unknown }> {
|
|
636
|
+
return this.request("POST", `/cards/${cardId}/comments`, {
|
|
637
|
+
body,
|
|
638
|
+
authorType: "agent",
|
|
639
|
+
commentType: opts?.commentType,
|
|
640
|
+
supersedesId: opts?.supersedesId,
|
|
641
|
+
confirmsId: opts?.confirmsId,
|
|
642
|
+
agentSessionId: opts?.agentSessionId,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async getComments(
|
|
647
|
+
cardId: string,
|
|
648
|
+
opts?: { limit?: number; offset?: number },
|
|
649
|
+
): Promise<{ comments: unknown[] }> {
|
|
650
|
+
const qs = new URLSearchParams();
|
|
651
|
+
if (opts?.limit != null) qs.set("limit", String(opts.limit));
|
|
652
|
+
if (opts?.offset != null) qs.set("offset", String(opts.offset));
|
|
653
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
654
|
+
return this.request("GET", `/cards/${cardId}/comments${suffix}`);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async updateComment(
|
|
658
|
+
commentId: string,
|
|
659
|
+
updates: { body?: string; pinned?: boolean; resolve?: boolean },
|
|
660
|
+
): Promise<{ comment: unknown }> {
|
|
661
|
+
return this.request("PATCH", `/comments/${commentId}`, updates);
|
|
662
|
+
}
|
|
663
|
+
|
|
620
664
|
// ============ AGENT CONTEXT OPERATIONS ============
|
|
621
665
|
|
|
622
666
|
async startAgentSession(
|
|
@@ -1448,6 +1492,26 @@ export class HarmonyApiClient {
|
|
|
1448
1492
|
assemblyId,
|
|
1449
1493
|
});
|
|
1450
1494
|
|
|
1495
|
+
// Append the card's comment thread (people + agents). This is the central
|
|
1496
|
+
// injection point: every caller of harmony_generate_prompt (daemon, Claude
|
|
1497
|
+
// Code, in-app builder) gets the recency-ordered thread + conflict rule.
|
|
1498
|
+
// Best-effort — never fail prompt generation on a comments fetch error.
|
|
1499
|
+
try {
|
|
1500
|
+
const { comments } = await this.getComments(options.cardId, {
|
|
1501
|
+
limit: 200,
|
|
1502
|
+
});
|
|
1503
|
+
if (Array.isArray(comments) && comments.length > 0) {
|
|
1504
|
+
const section = serializeCommentThread(comments as Comment[], {
|
|
1505
|
+
heading: "Comments",
|
|
1506
|
+
maxComments: 40,
|
|
1507
|
+
});
|
|
1508
|
+
if (section) result.prompt = `${result.prompt}\n\n${section}`;
|
|
1509
|
+
}
|
|
1510
|
+
} catch (err) {
|
|
1511
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1512
|
+
console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1451
1515
|
// AGP P2: persist a session-linked snapshot. Best-effort — never fail
|
|
1452
1516
|
// prompt generation just because logging didn't land.
|
|
1453
1517
|
try {
|
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
|
@@ -636,6 +636,72 @@ export const TOOLS = {
|
|
|
636
636
|
},
|
|
637
637
|
},
|
|
638
638
|
|
|
639
|
+
// Comment operations
|
|
640
|
+
harmony_add_comment: {
|
|
641
|
+
description:
|
|
642
|
+
"Post a comment on a card as the agent. Use this to converse with the human in the open: report progress, ask a question, record a decision, or note a finding — instead of editing the card description. Set supersedesId to correct an earlier comment, confirmsId to reaffirm one. When the thread conflicts, prefer the latest comment unless a later one confirms an earlier finding; cite the comment id(s) you relied on.",
|
|
643
|
+
inputSchema: {
|
|
644
|
+
type: "object",
|
|
645
|
+
properties: {
|
|
646
|
+
cardId: { type: "string", description: "Card UUID to comment on" },
|
|
647
|
+
body: { type: "string", description: "Comment body (Markdown)" },
|
|
648
|
+
commentType: {
|
|
649
|
+
type: "string",
|
|
650
|
+
enum: [
|
|
651
|
+
"message",
|
|
652
|
+
"progress",
|
|
653
|
+
"question",
|
|
654
|
+
"blocker",
|
|
655
|
+
"decision",
|
|
656
|
+
"summary",
|
|
657
|
+
"finding",
|
|
658
|
+
],
|
|
659
|
+
description:
|
|
660
|
+
"Type of comment. 'question'/'blocker' signal you need a human; default 'message'.",
|
|
661
|
+
},
|
|
662
|
+
supersedesId: {
|
|
663
|
+
type: "string",
|
|
664
|
+
description: "Comment id this comment corrects/updates",
|
|
665
|
+
},
|
|
666
|
+
confirmsId: {
|
|
667
|
+
type: "string",
|
|
668
|
+
description: "Comment id this comment reaffirms",
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
required: ["cardId", "body"],
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
harmony_get_comments: {
|
|
675
|
+
description:
|
|
676
|
+
"Get the comment thread on a card (oldest → newest). Returns human + agent comments with author, type, edited/resolved state, and supersede/confirm links. Read this before acting so you weigh later comments over earlier ones they contradict.",
|
|
677
|
+
inputSchema: {
|
|
678
|
+
type: "object",
|
|
679
|
+
properties: {
|
|
680
|
+
cardId: { type: "string" },
|
|
681
|
+
limit: { type: "number" },
|
|
682
|
+
offset: { type: "number" },
|
|
683
|
+
},
|
|
684
|
+
required: ["cardId"],
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
harmony_update_comment: {
|
|
688
|
+
description:
|
|
689
|
+
"Update one of your own comments: edit the body, pin it, or resolve a question/blocker once it has been answered.",
|
|
690
|
+
inputSchema: {
|
|
691
|
+
type: "object",
|
|
692
|
+
properties: {
|
|
693
|
+
commentId: { type: "string" },
|
|
694
|
+
body: { type: "string" },
|
|
695
|
+
pinned: { type: "boolean" },
|
|
696
|
+
resolve: {
|
|
697
|
+
type: "boolean",
|
|
698
|
+
description: "Mark (true) or clear (false) the resolved state",
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
required: ["commentId"],
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
|
|
639
705
|
// Context operations
|
|
640
706
|
harmony_list_workspaces: {
|
|
641
707
|
description: "List all workspaces the user has access to",
|
|
@@ -2177,6 +2243,71 @@ async function handleToolCall(
|
|
|
2177
2243
|
return { success: true };
|
|
2178
2244
|
}
|
|
2179
2245
|
|
|
2246
|
+
// Comment operations
|
|
2247
|
+
case "harmony_add_comment": {
|
|
2248
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
2249
|
+
const body = z.string().min(1).max(10_000).parse(args.body);
|
|
2250
|
+
const commentType =
|
|
2251
|
+
args.commentType !== undefined
|
|
2252
|
+
? z
|
|
2253
|
+
.enum([
|
|
2254
|
+
"message",
|
|
2255
|
+
"progress",
|
|
2256
|
+
"question",
|
|
2257
|
+
"blocker",
|
|
2258
|
+
"decision",
|
|
2259
|
+
"summary",
|
|
2260
|
+
"finding",
|
|
2261
|
+
])
|
|
2262
|
+
.parse(args.commentType)
|
|
2263
|
+
: undefined;
|
|
2264
|
+
const supersedesId =
|
|
2265
|
+
args.supersedesId !== undefined
|
|
2266
|
+
? z.string().uuid().parse(args.supersedesId)
|
|
2267
|
+
: undefined;
|
|
2268
|
+
const confirmsId =
|
|
2269
|
+
args.confirmsId !== undefined
|
|
2270
|
+
? z.string().uuid().parse(args.confirmsId)
|
|
2271
|
+
: undefined;
|
|
2272
|
+
const result = await client.addComment(cardId, body, {
|
|
2273
|
+
commentType,
|
|
2274
|
+
supersedesId,
|
|
2275
|
+
confirmsId,
|
|
2276
|
+
});
|
|
2277
|
+
return { success: true, ...result };
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
case "harmony_get_comments": {
|
|
2281
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
2282
|
+
const limit =
|
|
2283
|
+
args.limit !== undefined
|
|
2284
|
+
? z.number().int().min(1).max(500).parse(args.limit)
|
|
2285
|
+
: undefined;
|
|
2286
|
+
const offset =
|
|
2287
|
+
args.offset !== undefined
|
|
2288
|
+
? z.number().int().min(0).parse(args.offset)
|
|
2289
|
+
: undefined;
|
|
2290
|
+
const result = await client.getComments(cardId, { limit, offset });
|
|
2291
|
+
return { success: true, ...result };
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
case "harmony_update_comment": {
|
|
2295
|
+
const commentId = z.string().uuid().parse(args.commentId);
|
|
2296
|
+
const updates: { body?: string; pinned?: boolean; resolve?: boolean } =
|
|
2297
|
+
{};
|
|
2298
|
+
if (args.body !== undefined) {
|
|
2299
|
+
updates.body = z.string().min(1).max(10_000).parse(args.body);
|
|
2300
|
+
}
|
|
2301
|
+
if (args.pinned !== undefined) {
|
|
2302
|
+
updates.pinned = z.boolean().parse(args.pinned);
|
|
2303
|
+
}
|
|
2304
|
+
if (args.resolve !== undefined) {
|
|
2305
|
+
updates.resolve = z.boolean().parse(args.resolve);
|
|
2306
|
+
}
|
|
2307
|
+
const result = await client.updateComment(commentId, updates);
|
|
2308
|
+
return { success: true, ...result };
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2180
2311
|
// Context operations
|
|
2181
2312
|
case "harmony_list_workspaces": {
|
|
2182
2313
|
const result = await client.listWorkspaces();
|
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 });
|