@gethmy/mcp 2.0.0 → 2.1.1

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.
Files changed (62) hide show
  1. package/README.md +6 -1
  2. package/dist/cli.js +711 -59
  3. package/dist/index.js +5 -3
  4. package/dist/lib/__tests__/active-learning.test.js +386 -0
  5. package/dist/lib/__tests__/agent-performance-profiles.test.js +325 -0
  6. package/dist/lib/__tests__/auto-session.test.js +661 -0
  7. package/dist/lib/__tests__/context-assembly.test.js +362 -0
  8. package/dist/lib/__tests__/graph-expansion.test.js +150 -0
  9. package/dist/lib/__tests__/integration-memory-crud.test.js +797 -0
  10. package/dist/lib/__tests__/integration-memory-system.test.js +281 -0
  11. package/dist/lib/__tests__/lifecycle-maintenance.test.js +207 -0
  12. package/dist/lib/__tests__/pattern-detection.test.js +295 -0
  13. package/dist/lib/__tests__/prompt-builder.test.js +418 -0
  14. package/dist/lib/active-learning.js +878 -0
  15. package/dist/lib/api-client.js +550 -0
  16. package/dist/lib/auto-session.js +173 -0
  17. package/dist/lib/cli.js +127 -0
  18. package/dist/lib/config.js +205 -0
  19. package/dist/lib/consolidation.js +243 -0
  20. package/dist/lib/context-assembly.js +606 -0
  21. package/dist/lib/graph-expansion.js +163 -0
  22. package/dist/lib/http.js +174 -0
  23. package/dist/lib/index.js +7 -0
  24. package/dist/lib/lifecycle-maintenance.js +88 -0
  25. package/dist/lib/prompt-builder.js +483 -0
  26. package/dist/lib/remote.js +166 -0
  27. package/dist/lib/server.js +3132 -0
  28. package/dist/lib/tui/agents.js +116 -0
  29. package/dist/lib/tui/docs.js +744 -0
  30. package/dist/lib/tui/setup.js +1068 -0
  31. package/dist/lib/tui/theme.js +95 -0
  32. package/dist/lib/tui/writer.js +200 -0
  33. package/package.json +15 -6
  34. package/src/__tests__/active-learning.test.ts +483 -0
  35. package/src/__tests__/agent-performance-profiles.test.ts +468 -0
  36. package/src/__tests__/auto-session.test.ts +912 -0
  37. package/src/__tests__/context-assembly.test.ts +506 -0
  38. package/src/__tests__/graph-expansion.test.ts +285 -0
  39. package/src/__tests__/integration-memory-crud.test.ts +948 -0
  40. package/src/__tests__/integration-memory-system.test.ts +321 -0
  41. package/src/__tests__/lifecycle-maintenance.test.ts +238 -0
  42. package/src/__tests__/pattern-detection.test.ts +438 -0
  43. package/src/__tests__/prompt-builder.test.ts +505 -0
  44. package/src/active-learning.ts +1227 -0
  45. package/src/api-client.ts +969 -0
  46. package/src/auto-session.ts +218 -0
  47. package/src/cli.ts +166 -0
  48. package/src/config.ts +285 -0
  49. package/src/consolidation.ts +314 -0
  50. package/src/context-assembly.ts +842 -0
  51. package/src/graph-expansion.ts +234 -0
  52. package/src/http.ts +265 -0
  53. package/src/index.ts +8 -0
  54. package/src/lifecycle-maintenance.ts +120 -0
  55. package/src/prompt-builder.ts +681 -0
  56. package/src/remote.ts +227 -0
  57. package/src/server.ts +3858 -0
  58. package/src/tui/agents.ts +154 -0
  59. package/src/tui/docs.ts +863 -0
  60. package/src/tui/setup.ts +1281 -0
  61. package/src/tui/theme.ts +114 -0
  62. package/src/tui/writer.ts +260 -0
@@ -0,0 +1,114 @@
1
+ import * as pc from "picocolors";
2
+
3
+ /**
4
+ * Consistent theme for Harmony MCP TUI
5
+ * Uses picocolors for lightweight color output
6
+ */
7
+
8
+ // Brand colors and symbols
9
+ export const symbols = {
10
+ harmony: "\u25B2", // Triangle for Harmony logo
11
+ check: "\u2713",
12
+ cross: "\u2717",
13
+ bullet: "\u2022",
14
+ arrow: "\u2192",
15
+ arrowRight: "\u25B8",
16
+ dot: "\u25CF",
17
+ dotEmpty: "\u25CB",
18
+ info: "\u2139",
19
+ warning: "\u26A0",
20
+ pointer: "\u276F",
21
+ };
22
+
23
+ // Color functions
24
+ export const colors = {
25
+ // Brand
26
+ brand: (text: string) => pc.cyan(text),
27
+ brandBold: (text: string) => pc.bold(pc.cyan(text)),
28
+
29
+ // Status
30
+ success: (text: string) => pc.green(text),
31
+ error: (text: string) => pc.red(text),
32
+ warning: (text: string) => pc.yellow(text),
33
+ info: (text: string) => pc.blue(text),
34
+
35
+ // Text
36
+ dim: (text: string) => pc.dim(text),
37
+ bold: (text: string) => pc.bold(text),
38
+ muted: (text: string) => pc.gray(text),
39
+
40
+ // Highlights
41
+ highlight: (text: string) => pc.cyan(text),
42
+ link: (text: string) => pc.underline(pc.cyan(text)),
43
+ };
44
+
45
+ // Styled messages
46
+ export const messages = {
47
+ header: () => {
48
+ return `
49
+ ${colors.brandBold(" HARMONY")}
50
+ ${colors.dim(" Project management for AI agents")}
51
+ `;
52
+ },
53
+
54
+ done: (message: string) => {
55
+ return `${colors.success(symbols.check)} ${message}`;
56
+ },
57
+
58
+ fail: (message: string) => {
59
+ return `${colors.error(symbols.cross)} ${message}`;
60
+ },
61
+
62
+ skip: (message: string) => {
63
+ return `${colors.dim("-")} ${colors.dim(message)}`;
64
+ },
65
+
66
+ step: (number: number, total: number, message: string) => {
67
+ return `${colors.dim(`[${number}/${total}]`)} ${message}`;
68
+ },
69
+
70
+ fileCreated: (path: string) => {
71
+ return ` ${colors.success(symbols.check)} ${colors.dim(path)}`;
72
+ },
73
+
74
+ fileSkipped: (path: string) => {
75
+ return ` ${colors.dim(symbols.bullet)} ${colors.dim(path)} ${colors.dim("(exists)")}`;
76
+ },
77
+
78
+ fileError: (path: string, error: string) => {
79
+ return ` ${colors.error(symbols.cross)} ${path}: ${colors.error(error)}`;
80
+ },
81
+ };
82
+
83
+ // Box drawing for sections
84
+ export function box(title: string, content: string): string {
85
+ const lines = content.split("\n");
86
+ const maxWidth = Math.max(title.length, ...lines.map((l) => l.length));
87
+ const width = maxWidth + 4;
88
+
89
+ const top = `\u250C${"─".repeat(width)}\u2510`;
90
+ const bottom = `\u2514${"─".repeat(width)}\u2518`;
91
+ const titleLine = `\u2502 ${colors.bold(title)}${" ".repeat(width - title.length - 1)}\u2502`;
92
+ const contentLines = lines
93
+ .map((line) => `\u2502 ${line}${" ".repeat(width - line.length - 1)}\u2502`)
94
+ .join("\n");
95
+
96
+ return `${top}\n${titleLine}\n${contentLines}\n${bottom}`;
97
+ }
98
+
99
+ // Format file paths for display (shorten home directory)
100
+ export function formatPath(path: string, homeDir: string): string {
101
+ if (path.startsWith(homeDir)) {
102
+ return `~${path.slice(homeDir.length)}`;
103
+ }
104
+ return path;
105
+ }
106
+
107
+ // Indent helper
108
+ export function indent(text: string, spaces = 2): string {
109
+ const pad = " ".repeat(spaces);
110
+ return text
111
+ .split("\n")
112
+ .map((line) => `${pad}${line}`)
113
+ .join("\n");
114
+ }
@@ -0,0 +1,260 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname } from "node:path";
4
+ import * as p from "@clack/prompts";
5
+ import { colors, formatPath, messages } from "./theme.js";
6
+
7
+ export type FileAction = "create" | "update" | "merge" | "skip";
8
+
9
+ export interface FileResult {
10
+ path: string;
11
+ action: FileAction;
12
+ error?: string;
13
+ }
14
+
15
+ interface WriteOptions {
16
+ force?: boolean;
17
+ merge?: boolean;
18
+ }
19
+
20
+ /**
21
+ * Ensure directory exists
22
+ */
23
+ function ensureDir(dirPath: string): void {
24
+ if (!existsSync(dirPath)) {
25
+ mkdirSync(dirPath, { recursive: true, mode: 0o755 });
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Write a file, optionally skipping if exists
31
+ */
32
+ export function writeFile(
33
+ filePath: string,
34
+ content: string,
35
+ options: WriteOptions = {},
36
+ ): FileResult {
37
+ const exists = existsSync(filePath);
38
+
39
+ if (exists && !options.force) {
40
+ return { path: filePath, action: "skip" };
41
+ }
42
+
43
+ try {
44
+ ensureDir(dirname(filePath));
45
+ // Use restrictive permissions for config files
46
+ const mode = filePath.includes(".harmony-mcp") ? 0o600 : 0o644;
47
+ writeFileSync(filePath, content, { mode });
48
+ return { path: filePath, action: exists ? "update" : "create" };
49
+ } catch (error) {
50
+ return {
51
+ path: filePath,
52
+ action: "skip",
53
+ error: error instanceof Error ? error.message : String(error),
54
+ };
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Merge JSON content into existing file
60
+ */
61
+ export function mergeJsonFile(
62
+ filePath: string,
63
+ updates: Record<string, unknown>,
64
+ options: WriteOptions = {},
65
+ ): FileResult {
66
+ const exists = existsSync(filePath);
67
+
68
+ if (!exists) {
69
+ try {
70
+ ensureDir(dirname(filePath));
71
+ writeFileSync(filePath, JSON.stringify(updates, null, 2), {
72
+ mode: 0o644,
73
+ });
74
+ return { path: filePath, action: "create" };
75
+ } catch (error) {
76
+ return {
77
+ path: filePath,
78
+ action: "skip",
79
+ error: error instanceof Error ? error.message : String(error),
80
+ };
81
+ }
82
+ }
83
+
84
+ try {
85
+ const existing = JSON.parse(readFileSync(filePath, "utf-8"));
86
+
87
+ // Deep merge mcpServers if present (always update harmony config)
88
+ if (updates.mcpServers && existing.mcpServers) {
89
+ const existingServers = existing.mcpServers as Record<string, unknown>;
90
+ const updateServers = updates.mcpServers as Record<string, unknown>;
91
+ existing.mcpServers = { ...existingServers, ...updateServers };
92
+ } else {
93
+ Object.assign(existing, updates);
94
+ }
95
+
96
+ writeFileSync(filePath, JSON.stringify(existing, null, 2), { mode: 0o644 });
97
+ return { path: filePath, action: "merge" };
98
+ } catch {
99
+ if (options.force) {
100
+ try {
101
+ writeFileSync(filePath, JSON.stringify(updates, null, 2), {
102
+ mode: 0o644,
103
+ });
104
+ return { path: filePath, action: "update" };
105
+ } catch (error) {
106
+ return {
107
+ path: filePath,
108
+ action: "skip",
109
+ error: error instanceof Error ? error.message : String(error),
110
+ };
111
+ }
112
+ }
113
+ return {
114
+ path: filePath,
115
+ action: "skip",
116
+ error: "Could not parse existing file",
117
+ };
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Append to TOML file (for Codex config)
123
+ */
124
+ export function appendToToml(
125
+ filePath: string,
126
+ section: string,
127
+ content: string,
128
+ options: WriteOptions = {},
129
+ ): FileResult {
130
+ const exists = existsSync(filePath);
131
+
132
+ if (!exists) {
133
+ try {
134
+ ensureDir(dirname(filePath));
135
+ writeFileSync(filePath, content, { mode: 0o644 });
136
+ return { path: filePath, action: "create" };
137
+ } catch (error) {
138
+ return {
139
+ path: filePath,
140
+ action: "skip",
141
+ error: error instanceof Error ? error.message : String(error),
142
+ };
143
+ }
144
+ }
145
+
146
+ try {
147
+ const existing = readFileSync(filePath, "utf-8");
148
+
149
+ if (existing.includes(section)) {
150
+ if (options.force) {
151
+ // Replace existing section
152
+ const updated = existing.replace(
153
+ new RegExp(
154
+ `\\[${section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\][\\s\\S]*?(?=\\[|$)`,
155
+ ),
156
+ content.trim() + "\n\n",
157
+ );
158
+ writeFileSync(filePath, updated, { mode: 0o644 });
159
+ return { path: filePath, action: "update" };
160
+ }
161
+ return { path: filePath, action: "skip" };
162
+ }
163
+
164
+ // Append new section
165
+ writeFileSync(filePath, existing + "\n" + content, { mode: 0o644 });
166
+ return { path: filePath, action: "merge" };
167
+ } catch (error) {
168
+ return {
169
+ path: filePath,
170
+ action: "skip",
171
+ error: error instanceof Error ? error.message : String(error),
172
+ };
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Write multiple files with progress display
178
+ */
179
+ export async function writeFilesWithProgress(
180
+ files: Array<{
181
+ path: string;
182
+ content: string;
183
+ type: "text" | "json" | "toml";
184
+ jsonKey?: string;
185
+ tomlSection?: string;
186
+ }>,
187
+ options: WriteOptions = {},
188
+ ): Promise<FileResult[]> {
189
+ const results: FileResult[] = [];
190
+ const home = homedir();
191
+
192
+ const spinner = p.spinner();
193
+ spinner.start("Writing configuration files...");
194
+
195
+ for (const file of files) {
196
+ let result: FileResult;
197
+
198
+ if (file.type === "json") {
199
+ const jsonContent = JSON.parse(file.content);
200
+ result = mergeJsonFile(file.path, jsonContent, options);
201
+ } else if (file.type === "toml" && file.tomlSection) {
202
+ result = appendToToml(file.path, file.tomlSection, file.content, options);
203
+ } else {
204
+ result = writeFile(file.path, file.content, options);
205
+ }
206
+
207
+ results.push(result);
208
+
209
+ // Small delay for visual effect
210
+ await new Promise((resolve) => setTimeout(resolve, 50));
211
+ }
212
+
213
+ spinner.stop("Files written");
214
+
215
+ // Display results
216
+ for (const result of results) {
217
+ const displayPath = formatPath(result.path, home);
218
+
219
+ if (result.error) {
220
+ console.log(messages.fileError(displayPath, result.error));
221
+ } else if (result.action === "skip") {
222
+ console.log(messages.fileSkipped(displayPath));
223
+ } else {
224
+ const actionLabel = result.action === "merge" ? "updated" : "created";
225
+ console.log(
226
+ ` ${colors.success("\u2713")} ${colors.dim(displayPath)} ${colors.dim(`(${actionLabel})`)}`,
227
+ );
228
+ }
229
+ }
230
+
231
+ return results;
232
+ }
233
+
234
+ /**
235
+ * Get summary of what will be written
236
+ */
237
+ export function getWriteSummary(
238
+ files: Array<{ path: string; type: string }>,
239
+ options: WriteOptions = {},
240
+ ): { toCreate: string[]; toUpdate: string[]; toSkip: string[] } {
241
+ const toCreate: string[] = [];
242
+ const toUpdate: string[] = [];
243
+ const toSkip: string[] = [];
244
+ const home = homedir();
245
+
246
+ for (const file of files) {
247
+ const displayPath = formatPath(file.path, home);
248
+ const exists = existsSync(file.path);
249
+
250
+ if (exists && !options.force) {
251
+ toSkip.push(displayPath);
252
+ } else if (exists) {
253
+ toUpdate.push(displayPath);
254
+ } else {
255
+ toCreate.push(displayPath);
256
+ }
257
+ }
258
+
259
+ return { toCreate, toUpdate, toSkip };
260
+ }