@gethmy/mcp 2.3.1 → 2.3.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/lib/api-client.js +2099 -648
- package/dist/lib/config.js +217 -201
- package/package.json +9 -5
- package/src/memory-cleanup.ts +2 -4
- package/dist/lib/__tests__/active-learning.test.js +0 -386
- package/dist/lib/__tests__/agent-performance-profiles.test.js +0 -325
- package/dist/lib/__tests__/auto-session.test.js +0 -661
- package/dist/lib/__tests__/context-assembly.test.js +0 -362
- package/dist/lib/__tests__/graph-expansion.test.js +0 -150
- package/dist/lib/__tests__/integration-memory-crud.test.js +0 -797
- package/dist/lib/__tests__/integration-memory-system.test.js +0 -281
- package/dist/lib/__tests__/lifecycle-maintenance.test.js +0 -207
- package/dist/lib/__tests__/pattern-detection.test.js +0 -295
- package/dist/lib/__tests__/prompt-builder.test.js +0 -418
- package/dist/lib/active-learning.js +0 -822
- package/dist/lib/auto-session.js +0 -214
- package/dist/lib/cli.js +0 -138
- package/dist/lib/consolidation.js +0 -303
- package/dist/lib/context-assembly.js +0 -884
- package/dist/lib/graph-expansion.js +0 -163
- package/dist/lib/http.js +0 -175
- package/dist/lib/index.js +0 -7
- package/dist/lib/lifecycle-maintenance.js +0 -88
- package/dist/lib/memory-cleanup.js +0 -455
- package/dist/lib/onboard.js +0 -36
- package/dist/lib/prompt-builder.js +0 -488
- package/dist/lib/remote.js +0 -166
- package/dist/lib/server.js +0 -3365
- package/dist/lib/skills.js +0 -593
- package/dist/lib/tui/agents.js +0 -116
- package/dist/lib/tui/docs.js +0 -744
- package/dist/lib/tui/setup.js +0 -934
- package/dist/lib/tui/theme.js +0 -95
- package/dist/lib/tui/writer.js +0 -200
|
@@ -1,488 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prompt Builder for Harmony MCP Server
|
|
3
|
-
*
|
|
4
|
-
* Generates AI-ready prompts from Harmony cards with role-based framing,
|
|
5
|
-
* context extraction, and variant-specific instructions.
|
|
6
|
-
*/
|
|
7
|
-
// Label name to category mapping
|
|
8
|
-
const LABEL_CATEGORY_MAP = {
|
|
9
|
-
bug: "bug",
|
|
10
|
-
fix: "bug",
|
|
11
|
-
hotfix: "bug",
|
|
12
|
-
defect: "bug",
|
|
13
|
-
issue: "bug",
|
|
14
|
-
error: "bug",
|
|
15
|
-
feature: "feature",
|
|
16
|
-
enhancement: "feature",
|
|
17
|
-
improvement: "feature",
|
|
18
|
-
new: "feature",
|
|
19
|
-
design: "design",
|
|
20
|
-
ui: "design",
|
|
21
|
-
ux: "design",
|
|
22
|
-
frontend: "design",
|
|
23
|
-
styling: "design",
|
|
24
|
-
review: "review",
|
|
25
|
-
"code review": "review",
|
|
26
|
-
pr: "review",
|
|
27
|
-
feedback: "review",
|
|
28
|
-
onboarding: "onboarding",
|
|
29
|
-
documentation: "onboarding",
|
|
30
|
-
docs: "onboarding",
|
|
31
|
-
guide: "onboarding",
|
|
32
|
-
tutorial: "onboarding",
|
|
33
|
-
epic: "epic",
|
|
34
|
-
initiative: "epic",
|
|
35
|
-
project: "epic",
|
|
36
|
-
milestone: "epic",
|
|
37
|
-
};
|
|
38
|
-
// Default role framings
|
|
39
|
-
const DEFAULT_ROLE_FRAMINGS = {
|
|
40
|
-
bug: {
|
|
41
|
-
category: "bug",
|
|
42
|
-
role: "Senior QA Engineer and Software Developer",
|
|
43
|
-
perspective: "You are investigating and fixing a bug report.",
|
|
44
|
-
focus: [
|
|
45
|
-
"Root cause analysis",
|
|
46
|
-
"Steps to reproduce",
|
|
47
|
-
"Impact assessment",
|
|
48
|
-
"Fix implementation",
|
|
49
|
-
"Regression prevention",
|
|
50
|
-
"Test cases to prevent recurrence",
|
|
51
|
-
],
|
|
52
|
-
outputSuggestions: [
|
|
53
|
-
"Bug triage summary",
|
|
54
|
-
"Root cause explanation",
|
|
55
|
-
"Fix implementation plan",
|
|
56
|
-
"Test cases",
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
feature: {
|
|
60
|
-
category: "feature",
|
|
61
|
-
role: "Product Engineer",
|
|
62
|
-
perspective: "You are implementing a new feature or enhancement.",
|
|
63
|
-
focus: [
|
|
64
|
-
"User requirements",
|
|
65
|
-
"Technical specification",
|
|
66
|
-
"Implementation approach",
|
|
67
|
-
"Edge cases",
|
|
68
|
-
"Acceptance criteria",
|
|
69
|
-
"Integration points",
|
|
70
|
-
],
|
|
71
|
-
outputSuggestions: [
|
|
72
|
-
"Technical specification",
|
|
73
|
-
"Implementation tasks",
|
|
74
|
-
"Acceptance criteria checklist",
|
|
75
|
-
"API design",
|
|
76
|
-
],
|
|
77
|
-
},
|
|
78
|
-
design: {
|
|
79
|
-
category: "design",
|
|
80
|
-
role: "UX Designer and Frontend Developer",
|
|
81
|
-
perspective: "You are designing and implementing a user interface.",
|
|
82
|
-
focus: [
|
|
83
|
-
"User experience flow",
|
|
84
|
-
"Visual design consistency",
|
|
85
|
-
"Accessibility (WCAG)",
|
|
86
|
-
"Responsive behavior",
|
|
87
|
-
"Component architecture",
|
|
88
|
-
"Interaction patterns",
|
|
89
|
-
],
|
|
90
|
-
outputSuggestions: [
|
|
91
|
-
"User flow diagram",
|
|
92
|
-
"Component specifications",
|
|
93
|
-
"Accessibility checklist",
|
|
94
|
-
"Responsive breakpoints",
|
|
95
|
-
],
|
|
96
|
-
},
|
|
97
|
-
review: {
|
|
98
|
-
category: "review",
|
|
99
|
-
role: "Code Reviewer and Technical Lead",
|
|
100
|
-
perspective: "You are reviewing code for quality, correctness, and maintainability.",
|
|
101
|
-
focus: [
|
|
102
|
-
"Code correctness",
|
|
103
|
-
"Performance implications",
|
|
104
|
-
"Security considerations",
|
|
105
|
-
"Testing coverage",
|
|
106
|
-
"Documentation",
|
|
107
|
-
"Best practices adherence",
|
|
108
|
-
],
|
|
109
|
-
outputSuggestions: [
|
|
110
|
-
"Review checklist",
|
|
111
|
-
"Suggested improvements",
|
|
112
|
-
"Test scenarios",
|
|
113
|
-
"Security audit",
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
onboarding: {
|
|
117
|
-
category: "onboarding",
|
|
118
|
-
role: "Technical Writer and Developer Advocate",
|
|
119
|
-
perspective: "You are creating documentation or onboarding materials.",
|
|
120
|
-
focus: [
|
|
121
|
-
"Clear step-by-step instructions",
|
|
122
|
-
"Prerequisites and setup",
|
|
123
|
-
"Common pitfalls",
|
|
124
|
-
"Examples and use cases",
|
|
125
|
-
"Troubleshooting guide",
|
|
126
|
-
"Related resources",
|
|
127
|
-
],
|
|
128
|
-
outputSuggestions: [
|
|
129
|
-
"Getting started guide",
|
|
130
|
-
"Step-by-step tutorial",
|
|
131
|
-
"FAQ section",
|
|
132
|
-
"Troubleshooting guide",
|
|
133
|
-
],
|
|
134
|
-
},
|
|
135
|
-
epic: {
|
|
136
|
-
category: "epic",
|
|
137
|
-
role: "Technical Project Manager and Architect",
|
|
138
|
-
perspective: "You are planning and coordinating a large initiative.",
|
|
139
|
-
focus: [
|
|
140
|
-
"Scope definition",
|
|
141
|
-
"Task breakdown",
|
|
142
|
-
"Dependencies",
|
|
143
|
-
"Risk assessment",
|
|
144
|
-
"Timeline considerations",
|
|
145
|
-
"Success metrics",
|
|
146
|
-
],
|
|
147
|
-
outputSuggestions: [
|
|
148
|
-
"Epic breakdown into stories",
|
|
149
|
-
"Dependency graph",
|
|
150
|
-
"Risk mitigation plan",
|
|
151
|
-
"Success criteria",
|
|
152
|
-
],
|
|
153
|
-
},
|
|
154
|
-
custom: {
|
|
155
|
-
category: "custom",
|
|
156
|
-
role: "Software Engineer",
|
|
157
|
-
perspective: "You are working on a software task.",
|
|
158
|
-
focus: [
|
|
159
|
-
"Understanding requirements",
|
|
160
|
-
"Implementation approach",
|
|
161
|
-
"Quality considerations",
|
|
162
|
-
"Testing strategy",
|
|
163
|
-
],
|
|
164
|
-
outputSuggestions: [
|
|
165
|
-
"Implementation plan",
|
|
166
|
-
"Technical notes",
|
|
167
|
-
"Task checklist",
|
|
168
|
-
],
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
// Variant-specific instructions
|
|
172
|
-
const VARIANT_INSTRUCTIONS = {
|
|
173
|
-
analysis: `ANALYSIS MODE: Analyze this task thoroughly. Identify requirements, constraints, edge cases, and potential challenges. Do NOT implement anything yet - focus on understanding and planning.`,
|
|
174
|
-
draft: `DRAFT MODE: Create a detailed implementation plan with code structure, key decisions, and approach. Include pseudocode or skeleton code where helpful. This is for review before full implementation.`,
|
|
175
|
-
execute: `EXECUTE MODE: Implement this task completely. Write production-ready code following best practices. Include necessary tests and documentation.`,
|
|
176
|
-
};
|
|
177
|
-
/**
|
|
178
|
-
* Infer category from labels
|
|
179
|
-
*/
|
|
180
|
-
export function inferCategoryFromLabels(labels) {
|
|
181
|
-
for (const label of labels) {
|
|
182
|
-
const normalizedName = label.name.toLowerCase().trim();
|
|
183
|
-
if (LABEL_CATEGORY_MAP[normalizedName]) {
|
|
184
|
-
return LABEL_CATEGORY_MAP[normalizedName];
|
|
185
|
-
}
|
|
186
|
-
for (const [key, category] of Object.entries(LABEL_CATEGORY_MAP)) {
|
|
187
|
-
if (normalizedName.includes(key) || key.includes(normalizedName)) {
|
|
188
|
-
return category;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return "custom";
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Get role framing for a category
|
|
196
|
-
*/
|
|
197
|
-
export function getRoleFraming(category) {
|
|
198
|
-
return DEFAULT_ROLE_FRAMINGS[category];
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Estimate token count (rough approximation: 1 token per 4 chars)
|
|
202
|
-
*/
|
|
203
|
-
function estimateTokens(text) {
|
|
204
|
-
return Math.ceil(text.length / 4);
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Format subtasks for prompt
|
|
208
|
-
*/
|
|
209
|
-
function formatSubtasks(subtasks) {
|
|
210
|
-
if (subtasks.length === 0)
|
|
211
|
-
return "";
|
|
212
|
-
const completed = subtasks.filter((s) => s.completed).length;
|
|
213
|
-
const lines = subtasks.map((s) => ` ${s.completed ? "[x]" : "[ ]"} ${s.title}`);
|
|
214
|
-
return `\n## Subtasks (${completed}/${subtasks.length} completed)\n${lines.join("\n")}`;
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Format labels for prompt
|
|
218
|
-
*/
|
|
219
|
-
function formatLabels(labels) {
|
|
220
|
-
if (labels.length === 0)
|
|
221
|
-
return "";
|
|
222
|
-
return `\n**Labels:** ${labels.map((l) => l.name).join(", ")}`;
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Format linked cards for prompt
|
|
226
|
-
*/
|
|
227
|
-
function formatLinkedCards(links) {
|
|
228
|
-
if (!links || links.length === 0)
|
|
229
|
-
return "";
|
|
230
|
-
const lines = links.map((link) => {
|
|
231
|
-
const prefix = link.direction === "outgoing" ? "->" : "<-";
|
|
232
|
-
return ` ${prefix} #${link.target_card.short_id}: ${link.target_card.title} (${link.display_type})`;
|
|
233
|
-
});
|
|
234
|
-
return `\n## Related Cards\n${lines.join("\n")}`;
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Generate a prompt from a card
|
|
238
|
-
*/
|
|
239
|
-
export function generatePrompt(options) {
|
|
240
|
-
const { card, column, variant, customConstraints, memories, assembledContext, assemblyId, } = options;
|
|
241
|
-
// Merge context options with defaults
|
|
242
|
-
const contextOpts = {
|
|
243
|
-
includeTitle: true,
|
|
244
|
-
includeDescription: true,
|
|
245
|
-
includeLabels: true,
|
|
246
|
-
includeSubtasks: true,
|
|
247
|
-
includeActivity: false,
|
|
248
|
-
includeAssignee: true,
|
|
249
|
-
includeDueDate: true,
|
|
250
|
-
includePriority: true,
|
|
251
|
-
includeLinks: true,
|
|
252
|
-
includeColumn: true,
|
|
253
|
-
...options.contextOptions,
|
|
254
|
-
};
|
|
255
|
-
const labels = card.labels || [];
|
|
256
|
-
const subtasks = card.subtasks || [];
|
|
257
|
-
const links = card.links || [];
|
|
258
|
-
const category = inferCategoryFromLabels(labels);
|
|
259
|
-
const roleFraming = getRoleFraming(category);
|
|
260
|
-
// Build prompt sections
|
|
261
|
-
const sections = [];
|
|
262
|
-
// Role and perspective
|
|
263
|
-
sections.push(`# Role: ${roleFraming.role}\n`);
|
|
264
|
-
sections.push(roleFraming.perspective);
|
|
265
|
-
sections.push("");
|
|
266
|
-
// Variant instruction
|
|
267
|
-
sections.push(VARIANT_INSTRUCTIONS[variant]);
|
|
268
|
-
sections.push("");
|
|
269
|
-
// Task header
|
|
270
|
-
sections.push(`# Task: ${card.title}`);
|
|
271
|
-
if (contextOpts.includeColumn && column) {
|
|
272
|
-
sections.push(`**Status:** ${column.name}`);
|
|
273
|
-
}
|
|
274
|
-
if (contextOpts.includePriority) {
|
|
275
|
-
sections.push(`**Priority:** ${card.priority}`);
|
|
276
|
-
}
|
|
277
|
-
if (contextOpts.includeDueDate && card.due_date) {
|
|
278
|
-
sections.push(`**Due:** ${card.due_date}`);
|
|
279
|
-
}
|
|
280
|
-
if (contextOpts.includeAssignee && card.assignee) {
|
|
281
|
-
sections.push(`**Assignee:** ${card.assignee.full_name || card.assignee.email}`);
|
|
282
|
-
}
|
|
283
|
-
// Labels
|
|
284
|
-
if (contextOpts.includeLabels && labels.length > 0) {
|
|
285
|
-
sections.push(formatLabels(labels));
|
|
286
|
-
}
|
|
287
|
-
// Description
|
|
288
|
-
if (contextOpts.includeDescription && card.description) {
|
|
289
|
-
sections.push(`\n## Description\n${card.description}`);
|
|
290
|
-
}
|
|
291
|
-
// Subtasks
|
|
292
|
-
if (contextOpts.includeSubtasks && subtasks.length > 0) {
|
|
293
|
-
sections.push(formatSubtasks(subtasks));
|
|
294
|
-
}
|
|
295
|
-
// Linked cards
|
|
296
|
-
if (contextOpts.includeLinks && links.length > 0) {
|
|
297
|
-
sections.push(formatLinkedCards(links));
|
|
298
|
-
}
|
|
299
|
-
// Focus areas
|
|
300
|
-
sections.push(`\n## Focus Areas`);
|
|
301
|
-
roleFraming.focus.forEach((f) => {
|
|
302
|
-
sections.push(`- ${f}`);
|
|
303
|
-
});
|
|
304
|
-
sections.push(`- **Memory:** Store reusable knowledge via \`harmony_remember\`. Only store what a future agent couldn't easily discover from the code itself, applies beyond this specific card, and includes a "because" (not just what, but why).`);
|
|
305
|
-
sections.push(` - GOOD: "BoardContext card state must use moveCard action, never direct setState — optimistic updates depend on action ordering"`);
|
|
306
|
-
sections.push(` - GOOD: "Mobile bottom bar is 64px, overlaps fixed-position drawers — always add pb-16 to drawer content"`);
|
|
307
|
-
sections.push(` - BAD: "Fixed the login button" (no reusable knowledge — the fix is in the code)`);
|
|
308
|
-
sections.push(` - BAD: "Completed card #42" (ephemeral, auto-tracked by session)`);
|
|
309
|
-
// Output suggestions
|
|
310
|
-
sections.push(`\n## Suggested Outputs`);
|
|
311
|
-
roleFraming.outputSuggestions.forEach((s) => {
|
|
312
|
-
sections.push(`- ${s}`);
|
|
313
|
-
});
|
|
314
|
-
// Relevant memories from knowledge graph
|
|
315
|
-
if (assembledContext) {
|
|
316
|
-
// Use pre-assembled context from context assembly engine
|
|
317
|
-
sections.push(`\n${assembledContext}`);
|
|
318
|
-
}
|
|
319
|
-
else if (memories && memories.length > 0) {
|
|
320
|
-
// Fallback: legacy memory format
|
|
321
|
-
sections.push(`\n## Relevant Memories`);
|
|
322
|
-
sections.push(`*${memories.length} memories recalled from knowledge graph:*`);
|
|
323
|
-
for (const memory of memories) {
|
|
324
|
-
const tags = memory.tags.length > 0 ? ` [${memory.tags.join(", ")}]` : "";
|
|
325
|
-
sections.push(`\n### ${memory.title} (${memory.type}, confidence: ${memory.confidence})${tags}`);
|
|
326
|
-
sections.push(memory.content);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
// "One Thing" synthesis — highest-leverage next action
|
|
330
|
-
const oneThingLine = synthesizeOneThing(card, subtasks, links, assembledContext);
|
|
331
|
-
if (oneThingLine) {
|
|
332
|
-
sections.push(`\n## Recommended Next Step\n${oneThingLine}`);
|
|
333
|
-
}
|
|
334
|
-
// Progress tracking (execute variant only)
|
|
335
|
-
if (variant === "execute") {
|
|
336
|
-
sections.push(`\n## Progress Tracking
|
|
337
|
-
Update your progress by calling \`harmony_update_agent_progress\` with \`currentTask\` describing what you're doing now:
|
|
338
|
-
- After exploring the codebase and understanding requirements (~20%)
|
|
339
|
-
- When you start implementing changes (~50%)
|
|
340
|
-
- When you move to testing or verification (~80%)
|
|
341
|
-
- When done, before ending the session (100%)
|
|
342
|
-
|
|
343
|
-
Keep \`currentTask\` specific (e.g., "Refactoring auth middleware" not "Working on card").`);
|
|
344
|
-
}
|
|
345
|
-
// Custom constraints
|
|
346
|
-
if (customConstraints) {
|
|
347
|
-
sections.push(`\n## Additional Instructions\n${customConstraints}`);
|
|
348
|
-
}
|
|
349
|
-
// Card reference footer
|
|
350
|
-
sections.push(`\n---\n*Card #${card.short_id} | Generated for ${variant} mode*`);
|
|
351
|
-
const prompt = sections.join("\n");
|
|
352
|
-
const memoryCount = assembledContext
|
|
353
|
-
? (assembledContext.match(/^### /gm) || []).length
|
|
354
|
-
: memories?.length || 0;
|
|
355
|
-
return {
|
|
356
|
-
prompt,
|
|
357
|
-
variant,
|
|
358
|
-
category,
|
|
359
|
-
role: roleFraming.role,
|
|
360
|
-
contextSummary: {
|
|
361
|
-
hasDescription: !!card.description,
|
|
362
|
-
labelCount: labels.length,
|
|
363
|
-
subtaskCount: subtasks.length,
|
|
364
|
-
completedSubtasks: subtasks.filter((s) => s.completed).length,
|
|
365
|
-
linkedCardCount: links.length,
|
|
366
|
-
memoryCount,
|
|
367
|
-
},
|
|
368
|
-
tokenEstimate: estimateTokens(prompt),
|
|
369
|
-
...(assemblyId && { assemblyId }),
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Extract session insights from assembled context string.
|
|
374
|
-
* Parses session summaries, blockers, and progress data.
|
|
375
|
-
*/
|
|
376
|
-
function extractSessionInsights(assembledContext) {
|
|
377
|
-
const result = {
|
|
378
|
-
lastSessionStatus: null,
|
|
379
|
-
lastSessionTask: null,
|
|
380
|
-
lastSessionProgress: null,
|
|
381
|
-
blockers: [],
|
|
382
|
-
procedureNextStep: null,
|
|
383
|
-
};
|
|
384
|
-
// Find the most recent session summary with status
|
|
385
|
-
const sessionMatches = assembledContext.match(/### Session:.*?\n([\s\S]*?)(?=\n###|\n## |\n---|\n\*Assembly|$)/g);
|
|
386
|
-
if (sessionMatches && sessionMatches.length > 0) {
|
|
387
|
-
const latest = sessionMatches[0];
|
|
388
|
-
if (/Completed work on/i.test(latest)) {
|
|
389
|
-
result.lastSessionStatus = "completed";
|
|
390
|
-
}
|
|
391
|
-
else if (/Paused work on|status:\s*paused/i.test(latest)) {
|
|
392
|
-
result.lastSessionStatus = "paused";
|
|
393
|
-
}
|
|
394
|
-
const taskMatch = latest.match(/Final task:\s*(.+)/);
|
|
395
|
-
if (taskMatch)
|
|
396
|
-
result.lastSessionTask = taskMatch[1].trim();
|
|
397
|
-
const progressMatch = latest.match(/Progress:\s*(\d+)%/);
|
|
398
|
-
if (progressMatch)
|
|
399
|
-
result.lastSessionProgress = parseInt(progressMatch[1], 10);
|
|
400
|
-
}
|
|
401
|
-
// Extract blockers from context
|
|
402
|
-
const blockerMatches = assembledContext.match(/(?:blocker|blocked by|blocking):\s*(.+)/gi);
|
|
403
|
-
if (blockerMatches) {
|
|
404
|
-
result.blockers = blockerMatches.map((m) => m.replace(/(?:blocker|blocked by|blocking):\s*/i, "").trim());
|
|
405
|
-
}
|
|
406
|
-
// Extract next procedure step (first uncompleted step)
|
|
407
|
-
const stepMatches = assembledContext.match(/^\d+\.\s+(?!.*\*\*\[key step\]\*\*.*✓)(.+?)(?:\s*\*\*\[key step\]\*\*)?$/gm);
|
|
408
|
-
if (stepMatches && stepMatches.length > 0) {
|
|
409
|
-
result.procedureNextStep = stepMatches[0]
|
|
410
|
-
.replace(/^\d+\.\s+/, "")
|
|
411
|
-
.replace(/\s*\*\*\[key step\]\*\*.*$/, "")
|
|
412
|
-
.trim();
|
|
413
|
-
}
|
|
414
|
-
return result;
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Synthesize the single highest-leverage next action from card state
|
|
418
|
-
* and assembled context (session history, blockers, procedures).
|
|
419
|
-
* Inspired by ArtemXTech's "One Thing" pattern in /recall.
|
|
420
|
-
*/
|
|
421
|
-
function synthesizeOneThing(card, subtasks, links, assembledContext) {
|
|
422
|
-
// Priority 1: Card is already done
|
|
423
|
-
if (card.done)
|
|
424
|
-
return null;
|
|
425
|
-
// Priority 2: Blocked by another card (via links)
|
|
426
|
-
const blockers = links.filter((l) => l.display_type === "is_blocked_by" && l.direction === "incoming");
|
|
427
|
-
if (blockers.length > 0) {
|
|
428
|
-
const blocker = blockers[0];
|
|
429
|
-
return `Unblock first: resolve #${blocker.target_card.short_id} "${blocker.target_card.title}" which is blocking this card.`;
|
|
430
|
-
}
|
|
431
|
-
// Extract session insights from assembled context
|
|
432
|
-
const session = assembledContext
|
|
433
|
-
? extractSessionInsights(assembledContext)
|
|
434
|
-
: null;
|
|
435
|
-
// Priority 3: Blockers detected in session context
|
|
436
|
-
if (session?.blockers && session.blockers.length > 0) {
|
|
437
|
-
return `Resolve blocker: ${session.blockers[0]}`;
|
|
438
|
-
}
|
|
439
|
-
// Priority 4: Previous session was paused — resume where it left off
|
|
440
|
-
if (session?.lastSessionStatus === "paused" && session.lastSessionTask) {
|
|
441
|
-
const progress = session.lastSessionProgress
|
|
442
|
-
? ` (was ${session.lastSessionProgress}% complete)`
|
|
443
|
-
: "";
|
|
444
|
-
return `Resume previous session${progress}: "${session.lastSessionTask}".`;
|
|
445
|
-
}
|
|
446
|
-
// Priority 5: Has subtasks — find the first incomplete one
|
|
447
|
-
if (subtasks.length > 0) {
|
|
448
|
-
const completed = subtasks.filter((s) => s.completed).length;
|
|
449
|
-
if (completed === subtasks.length) {
|
|
450
|
-
return "All subtasks completed. Review the work and mark the card as done.";
|
|
451
|
-
}
|
|
452
|
-
const nextSubtask = subtasks.find((s) => !s.completed);
|
|
453
|
-
if (nextSubtask) {
|
|
454
|
-
return `Work on next subtask: "${nextSubtask.title}" (${completed}/${subtasks.length} done).`;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
// Priority 6: Procedure has a next step
|
|
458
|
-
if (session?.procedureNextStep) {
|
|
459
|
-
return `Follow procedure: ${session.procedureNextStep}`;
|
|
460
|
-
}
|
|
461
|
-
// Priority 7: Previous session completed — build on it
|
|
462
|
-
if (session?.lastSessionStatus === "completed" && session.lastSessionTask) {
|
|
463
|
-
return `Previous session completed ("${session.lastSessionTask}"). Review results and continue with remaining work.`;
|
|
464
|
-
}
|
|
465
|
-
// Priority 8: High/urgent priority with due date
|
|
466
|
-
if (card.due_date &&
|
|
467
|
-
(card.priority === "urgent" || card.priority === "high")) {
|
|
468
|
-
return `High-priority task with deadline ${card.due_date}. Start implementation immediately.`;
|
|
469
|
-
}
|
|
470
|
-
// Priority 9: Has description — start working
|
|
471
|
-
if (card.description) {
|
|
472
|
-
return "Analyze the description, identify the approach, and begin implementation.";
|
|
473
|
-
}
|
|
474
|
-
// Fallback
|
|
475
|
-
return null;
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Get all available categories
|
|
479
|
-
*/
|
|
480
|
-
export function getAvailableCategories() {
|
|
481
|
-
return ["bug", "feature", "design", "review", "onboarding", "epic", "custom"];
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Get all available variants
|
|
485
|
-
*/
|
|
486
|
-
export function getAvailableVariants() {
|
|
487
|
-
return ["analysis", "draft", "execute"];
|
|
488
|
-
}
|
package/dist/lib/remote.js
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Remote MCP Server for Harmony
|
|
4
|
-
*
|
|
5
|
-
* Hosted MCP endpoint that any AI agent can connect to via HTTP.
|
|
6
|
-
* Auth via API key passed as Bearer token, validated against the Harmony API.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* Claude.ai → POST https://mcp.gethmy.com/mcp (Bearer: hmy_xxx)
|
|
10
|
-
*
|
|
11
|
-
* Env vars:
|
|
12
|
-
* HARMONY_API_URL - Harmony API base URL (default: https://app.gethmy.com/api)
|
|
13
|
-
* PORT - Listen port (default: 3002)
|
|
14
|
-
*/
|
|
15
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
|
-
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
17
|
-
import { serve } from "bun";
|
|
18
|
-
import { Hono } from "hono";
|
|
19
|
-
import { cors } from "hono/cors";
|
|
20
|
-
import { HarmonyApiClient } from "./api-client.js";
|
|
21
|
-
import { registerHandlers } from "./server.js";
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Config from env
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
const HARMONY_API_URL = process.env.HARMONY_API_URL || "https://app.gethmy.com/api";
|
|
26
|
-
const PORT = parseInt(process.env.PORT || "3002", 10);
|
|
27
|
-
async function validateApiKey(apiKey) {
|
|
28
|
-
try {
|
|
29
|
-
const response = await fetch(`${HARMONY_API_URL}/v1/workspaces`, {
|
|
30
|
-
headers: { "X-API-Key": apiKey },
|
|
31
|
-
});
|
|
32
|
-
if (!response.ok)
|
|
33
|
-
return null;
|
|
34
|
-
const data = (await response.json());
|
|
35
|
-
const firstWorkspace = data.workspaces?.[0];
|
|
36
|
-
return { workspaceId: firstWorkspace?.id ?? null };
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const sessions = new Map();
|
|
43
|
-
// Clean up stale sessions every 30 minutes
|
|
44
|
-
setInterval(() => {
|
|
45
|
-
const now = Date.now();
|
|
46
|
-
const maxAge = 60 * 60 * 1000; // 1 hour
|
|
47
|
-
for (const [id, session] of sessions) {
|
|
48
|
-
if (now - session.createdAt > maxAge) {
|
|
49
|
-
session.transport.close().catch(() => { });
|
|
50
|
-
sessions.delete(id);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}, 30 * 60 * 1000);
|
|
54
|
-
function createSession(apiKey, keyInfo) {
|
|
55
|
-
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
56
|
-
sessionIdGenerator: () => crypto.randomUUID(),
|
|
57
|
-
enableJsonResponse: true,
|
|
58
|
-
});
|
|
59
|
-
const server = new Server({ name: "harmony-mcp-remote", version: "1.0.0" }, { capabilities: { tools: {}, resources: {} } });
|
|
60
|
-
const session = {
|
|
61
|
-
transport,
|
|
62
|
-
server,
|
|
63
|
-
apiKey,
|
|
64
|
-
activeWorkspaceId: keyInfo.workspaceId,
|
|
65
|
-
activeProjectId: null,
|
|
66
|
-
createdAt: Date.now(),
|
|
67
|
-
};
|
|
68
|
-
// Create per-session deps
|
|
69
|
-
const client = new HarmonyApiClient({ apiKey, apiUrl: HARMONY_API_URL });
|
|
70
|
-
const deps = {
|
|
71
|
-
getClient: () => client,
|
|
72
|
-
isConfigured: () => true,
|
|
73
|
-
getActiveProjectId: () => session.activeProjectId,
|
|
74
|
-
getActiveWorkspaceId: () => session.activeWorkspaceId,
|
|
75
|
-
setActiveProject: (id) => {
|
|
76
|
-
session.activeProjectId = id;
|
|
77
|
-
},
|
|
78
|
-
setActiveWorkspace: (id) => {
|
|
79
|
-
session.activeWorkspaceId = id;
|
|
80
|
-
},
|
|
81
|
-
getApiUrl: () => HARMONY_API_URL,
|
|
82
|
-
getMemoryDir: () => null, // No local filesystem in remote mode
|
|
83
|
-
getUserEmail: () => null,
|
|
84
|
-
saveConfig: () => { }, // No-op in remote mode
|
|
85
|
-
resetClient: () => { }, // No-op in remote mode
|
|
86
|
-
};
|
|
87
|
-
registerHandlers(server, deps);
|
|
88
|
-
// Clean up session when transport closes
|
|
89
|
-
transport.onclose = () => {
|
|
90
|
-
if (transport.sessionId) {
|
|
91
|
-
sessions.delete(transport.sessionId);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
return session;
|
|
95
|
-
}
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
// Hono app
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
const app = new Hono();
|
|
100
|
-
app.use("/*", cors({
|
|
101
|
-
origin: "*",
|
|
102
|
-
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
103
|
-
allowHeaders: [
|
|
104
|
-
"Content-Type",
|
|
105
|
-
"Authorization",
|
|
106
|
-
"Mcp-Session-Id",
|
|
107
|
-
"Mcp-Protocol-Version",
|
|
108
|
-
],
|
|
109
|
-
exposeHeaders: ["Mcp-Session-Id"],
|
|
110
|
-
}));
|
|
111
|
-
// Health check
|
|
112
|
-
app.get("/health", (c) => c.json({
|
|
113
|
-
status: "ok",
|
|
114
|
-
service: "harmony-mcp-remote",
|
|
115
|
-
sessions: sessions.size,
|
|
116
|
-
}));
|
|
117
|
-
// MCP endpoint - handles POST (JSON-RPC), GET (SSE), DELETE (session close)
|
|
118
|
-
app.all("/mcp", async (c) => {
|
|
119
|
-
const method = c.req.method;
|
|
120
|
-
// Extract API key from Authorization header
|
|
121
|
-
const authHeader = c.req.header("Authorization");
|
|
122
|
-
if (!authHeader?.startsWith("Bearer ")) {
|
|
123
|
-
return c.json({ error: "Missing Authorization: Bearer <api-key>" }, 401);
|
|
124
|
-
}
|
|
125
|
-
const apiKey = authHeader.slice(7);
|
|
126
|
-
// Check for existing session
|
|
127
|
-
const sessionId = c.req.header("Mcp-Session-Id");
|
|
128
|
-
if (sessionId && sessions.has(sessionId)) {
|
|
129
|
-
// Existing session - forward request
|
|
130
|
-
const session = sessions.get(sessionId);
|
|
131
|
-
return session.transport.handleRequest(c.req.raw);
|
|
132
|
-
}
|
|
133
|
-
if (method === "POST") {
|
|
134
|
-
// Could be a new session (initialize) or an existing session we don't know about
|
|
135
|
-
// Validate API key
|
|
136
|
-
const keyInfo = await validateApiKey(apiKey);
|
|
137
|
-
if (!keyInfo) {
|
|
138
|
-
return c.json({ error: "Invalid API key" }, 401);
|
|
139
|
-
}
|
|
140
|
-
// Create new session
|
|
141
|
-
const session = createSession(apiKey, keyInfo);
|
|
142
|
-
// Connect server to transport
|
|
143
|
-
await session.server.connect(session.transport);
|
|
144
|
-
// Store session once transport has a session ID
|
|
145
|
-
const origOnSessionInitialized = session.transport._onsessioninitialized;
|
|
146
|
-
session.transport._onsessioninitialized = (sid) => {
|
|
147
|
-
sessions.set(sid, session);
|
|
148
|
-
origOnSessionInitialized?.(sid);
|
|
149
|
-
};
|
|
150
|
-
// Handle the initialize request
|
|
151
|
-
return session.transport.handleRequest(c.req.raw);
|
|
152
|
-
}
|
|
153
|
-
// GET or DELETE without a valid session
|
|
154
|
-
return c.json({ error: "Invalid or missing session" }, 404);
|
|
155
|
-
});
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
// Start server
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
console.log(`Starting Harmony Remote MCP server on port ${PORT}...`);
|
|
160
|
-
serve({
|
|
161
|
-
fetch: app.fetch,
|
|
162
|
-
port: PORT,
|
|
163
|
-
});
|
|
164
|
-
console.log(`Harmony Remote MCP server running at http://localhost:${PORT}`);
|
|
165
|
-
console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
|
|
166
|
-
console.log(`Health check: http://localhost:${PORT}/health`);
|