@gethmy/mcp 1.0.0 → 2.1.0

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 (65) hide show
  1. package/README.md +201 -36
  2. package/dist/cli.js +20938 -20249
  3. package/dist/http.js +1957 -0
  4. package/dist/index.js +17833 -17888
  5. package/dist/lib/__tests__/active-learning.test.js +386 -0
  6. package/dist/lib/__tests__/agent-performance-profiles.test.js +325 -0
  7. package/dist/lib/__tests__/auto-session.test.js +661 -0
  8. package/dist/lib/__tests__/context-assembly.test.js +362 -0
  9. package/dist/lib/__tests__/graph-expansion.test.js +150 -0
  10. package/dist/lib/__tests__/integration-memory-crud.test.js +797 -0
  11. package/dist/lib/__tests__/integration-memory-system.test.js +281 -0
  12. package/dist/lib/__tests__/lifecycle-maintenance.test.js +207 -0
  13. package/dist/lib/__tests__/pattern-detection.test.js +295 -0
  14. package/dist/lib/__tests__/prompt-builder.test.js +418 -0
  15. package/dist/lib/active-learning.js +878 -0
  16. package/dist/lib/api-client.js +548 -0
  17. package/dist/lib/auto-session.js +173 -0
  18. package/dist/lib/cli.js +127 -0
  19. package/dist/lib/config.js +205 -0
  20. package/dist/lib/consolidation.js +243 -0
  21. package/dist/lib/context-assembly.js +606 -0
  22. package/dist/lib/graph-expansion.js +163 -0
  23. package/dist/lib/http.js +174 -0
  24. package/dist/lib/index.js +7 -0
  25. package/dist/lib/lifecycle-maintenance.js +88 -0
  26. package/dist/lib/prompt-builder.js +483 -0
  27. package/dist/lib/remote.js +166 -0
  28. package/dist/lib/server.js +3132 -0
  29. package/dist/lib/tui/agents.js +116 -0
  30. package/dist/lib/tui/docs.js +558 -0
  31. package/dist/lib/tui/setup.js +1068 -0
  32. package/dist/lib/tui/theme.js +95 -0
  33. package/dist/lib/tui/writer.js +200 -0
  34. package/dist/remote.js +34534 -0
  35. package/dist/server.js +31967 -0
  36. package/package.json +20 -7
  37. package/src/__tests__/active-learning.test.ts +483 -0
  38. package/src/__tests__/agent-performance-profiles.test.ts +468 -0
  39. package/src/__tests__/auto-session.test.ts +912 -0
  40. package/src/__tests__/context-assembly.test.ts +506 -0
  41. package/src/__tests__/graph-expansion.test.ts +285 -0
  42. package/src/__tests__/integration-memory-crud.test.ts +948 -0
  43. package/src/__tests__/integration-memory-system.test.ts +321 -0
  44. package/src/__tests__/lifecycle-maintenance.test.ts +238 -0
  45. package/src/__tests__/pattern-detection.test.ts +438 -0
  46. package/src/__tests__/prompt-builder.test.ts +505 -0
  47. package/src/active-learning.ts +1227 -0
  48. package/src/api-client.ts +963 -0
  49. package/src/auto-session.ts +218 -0
  50. package/src/cli.ts +166 -0
  51. package/src/config.ts +285 -0
  52. package/src/consolidation.ts +314 -0
  53. package/src/context-assembly.ts +842 -0
  54. package/src/graph-expansion.ts +234 -0
  55. package/src/http.ts +265 -0
  56. package/src/index.ts +8 -0
  57. package/src/lifecycle-maintenance.ts +120 -0
  58. package/src/prompt-builder.ts +681 -0
  59. package/src/remote.ts +227 -0
  60. package/src/server.ts +3858 -0
  61. package/src/tui/agents.ts +154 -0
  62. package/src/tui/docs.ts +650 -0
  63. package/src/tui/setup.ts +1281 -0
  64. package/src/tui/theme.ts +114 -0
  65. package/src/tui/writer.ts +260 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Auto-Session Tracking
3
+ *
4
+ * Automatically detects agent session boundaries by monitoring tool calls.
5
+ * Sessions auto-start when card-mutating tools are called, and auto-end
6
+ * after 10 minutes of inactivity or when a different card is worked on.
7
+ */
8
+ /** Tools that trigger auto-start of a session */
9
+ export const AUTO_START_TRIGGERS = new Set([
10
+ "harmony_generate_prompt",
11
+ "harmony_update_card",
12
+ "harmony_move_card",
13
+ "harmony_create_subtask",
14
+ "harmony_toggle_subtask",
15
+ "harmony_add_label_to_card",
16
+ "harmony_remove_label_from_card",
17
+ ]);
18
+ export const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
19
+ const CHECK_INTERVAL_MS = 60 * 1000; // 60 seconds
20
+ const activeSessions = new Map();
21
+ let inactivityTimer = null;
22
+ let endCallback = null;
23
+ let clientGetter = null;
24
+ /**
25
+ * Initialize auto-session tracking.
26
+ * @param callback Called when an auto-session ends (runs the learning pipeline)
27
+ * @param getClient Function to get the current API client
28
+ */
29
+ export function initAutoSession(callback, getClient) {
30
+ endCallback = callback;
31
+ clientGetter = getClient;
32
+ if (inactivityTimer)
33
+ clearInterval(inactivityTimer);
34
+ inactivityTimer = setInterval(checkInactivity, CHECK_INTERVAL_MS);
35
+ }
36
+ /**
37
+ * Track activity on a card. Auto-starts a session if needed.
38
+ */
39
+ export async function trackActivity(cardId, options) {
40
+ const now = Date.now();
41
+ const existing = activeSessions.get(cardId);
42
+ if (existing) {
43
+ // Update last activity timestamp
44
+ existing.lastActivityAt = now;
45
+ return;
46
+ }
47
+ // Only auto-start if the tool is a trigger
48
+ if (!options?.autoStart)
49
+ return;
50
+ const client = options?.client ?? clientGetter?.();
51
+ if (!client)
52
+ return;
53
+ // Collect auto-sessions on other cards to end (avoid mutating map during iteration)
54
+ const toEnd = [];
55
+ for (const [otherCardId, session] of activeSessions) {
56
+ if (otherCardId !== cardId && !session.isExplicit) {
57
+ toEnd.push(otherCardId);
58
+ }
59
+ }
60
+ for (const otherCardId of toEnd) {
61
+ await autoEndSession(client, otherCardId, "completed");
62
+ }
63
+ // Start a new auto-session
64
+ try {
65
+ await client.startAgentSession(cardId, {
66
+ agentIdentifier: "auto",
67
+ agentName: "Auto-detected Agent",
68
+ status: "working",
69
+ });
70
+ }
71
+ catch {
72
+ // Session start failed (might already have one), still track locally
73
+ }
74
+ activeSessions.set(cardId, {
75
+ cardId,
76
+ startedAt: now,
77
+ lastActivityAt: now,
78
+ isExplicit: false,
79
+ agentIdentifier: "auto",
80
+ agentName: "Auto-detected Agent",
81
+ });
82
+ }
83
+ /**
84
+ * Mark a session as explicitly started (won't be auto-ended by card switching or inactivity).
85
+ */
86
+ export function markExplicit(cardId) {
87
+ const existing = activeSessions.get(cardId);
88
+ if (existing) {
89
+ existing.isExplicit = true;
90
+ }
91
+ else {
92
+ // Track the explicit session even if we didn't auto-start it
93
+ activeSessions.set(cardId, {
94
+ cardId,
95
+ startedAt: Date.now(),
96
+ lastActivityAt: Date.now(),
97
+ isExplicit: true,
98
+ agentIdentifier: "explicit",
99
+ agentName: "Explicit Agent",
100
+ });
101
+ }
102
+ }
103
+ /**
104
+ * Remove a session from tracking (called when session is explicitly ended).
105
+ */
106
+ export function untrack(cardId) {
107
+ activeSessions.delete(cardId);
108
+ }
109
+ /**
110
+ * End all active auto-sessions (called on process shutdown).
111
+ */
112
+ export async function shutdownAllSessions() {
113
+ const client = clientGetter?.();
114
+ if (!client)
115
+ return;
116
+ // Snapshot keys to avoid mutating map during iteration
117
+ const cardIds = [...activeSessions.keys()];
118
+ const promises = cardIds.map((cardId) => autoEndSession(client, cardId, "paused"));
119
+ await Promise.allSettled(promises);
120
+ }
121
+ /**
122
+ * Clean up the interval timer (for tests).
123
+ */
124
+ export function destroyAutoSession() {
125
+ if (inactivityTimer) {
126
+ clearInterval(inactivityTimer);
127
+ inactivityTimer = null;
128
+ }
129
+ activeSessions.clear();
130
+ endCallback = null;
131
+ clientGetter = null;
132
+ }
133
+ /**
134
+ * Get a snapshot of active sessions (for testing/debugging).
135
+ */
136
+ export function getActiveSessions() {
137
+ return activeSessions;
138
+ }
139
+ /**
140
+ * Run inactivity check immediately (exported for testing).
141
+ * In production, called by the setInterval timer every 60s.
142
+ */
143
+ export function checkInactivity() {
144
+ const now = Date.now();
145
+ const client = clientGetter?.();
146
+ if (!client)
147
+ return;
148
+ // Snapshot keys to avoid mutating map during iteration
149
+ const entries = [...activeSessions.entries()];
150
+ for (const [cardId, session] of entries) {
151
+ if (session.isExplicit)
152
+ continue;
153
+ if (now - session.lastActivityAt > INACTIVITY_TIMEOUT_MS) {
154
+ autoEndSession(client, cardId, "completed").catch(() => { });
155
+ }
156
+ }
157
+ }
158
+ // --- Internal ---
159
+ async function autoEndSession(client, cardId, status) {
160
+ activeSessions.delete(cardId);
161
+ try {
162
+ await client.endAgentSession(cardId, { status });
163
+ }
164
+ catch {
165
+ // Best-effort end
166
+ }
167
+ try {
168
+ await endCallback?.(client, cardId, status);
169
+ }
170
+ catch {
171
+ // Best-effort pipeline
172
+ }
173
+ }
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { program } from "commander";
4
+ import { areSkillsInstalled, getActiveProjectId, getActiveWorkspaceId, getConfigPath, getLocalConfigPath, hasLocalConfig, isConfigured, loadConfig, loadLocalConfig, saveConfig, } from "./config.js";
5
+ import { HarmonyMCPServer } from "./server.js";
6
+ import { runSetup } from "./tui/setup.js";
7
+ const require = createRequire(import.meta.url);
8
+ const { version } = require("../package.json");
9
+ program
10
+ .name("@gethmy/mcp")
11
+ .description("MCP server for Harmony Kanban board")
12
+ .version(version);
13
+ program
14
+ .command("serve")
15
+ .description("Start the MCP server (stdio transport)")
16
+ .action(async () => {
17
+ const server = new HarmonyMCPServer();
18
+ await server.run();
19
+ });
20
+ program
21
+ .command("status")
22
+ .description("Show configuration status")
23
+ .action(() => {
24
+ const globalConfig = loadConfig();
25
+ const localConfig = loadLocalConfig();
26
+ const hasLocal = hasLocalConfig();
27
+ const skillsStatus = areSkillsInstalled();
28
+ // Helper to mask email for privacy (u***@example.com)
29
+ const maskEmail = (email) => {
30
+ const [local, domain] = email.split("@");
31
+ if (!domain)
32
+ return email;
33
+ return `${local[0]}***@${domain}`;
34
+ };
35
+ if (isConfigured()) {
36
+ console.log("Status: Configured\n");
37
+ console.log("API:");
38
+ console.log(` Key: ${globalConfig.apiKey?.slice(0, 8)}...`);
39
+ console.log(` URL: ${globalConfig.apiUrl}`);
40
+ console.log(` Email: ${globalConfig.userEmail ? maskEmail(globalConfig.userEmail) : "(not set)"}`);
41
+ console.log("\nSkills:");
42
+ if (skillsStatus.installed) {
43
+ console.log(` Installed: Yes (${skillsStatus.location})`);
44
+ for (const path of skillsStatus.paths) {
45
+ console.log(` ${path}`);
46
+ }
47
+ }
48
+ else {
49
+ console.log(" Installed: No");
50
+ console.log(" Run: npx @gethmy/mcp setup");
51
+ }
52
+ console.log("\nContext:");
53
+ if (hasLocal) {
54
+ console.log(` Local config: ${getLocalConfigPath()}`);
55
+ console.log(` Workspace: ${localConfig?.workspaceId || "(not set)"}`);
56
+ console.log(` Project: ${localConfig?.projectId || "(not set)"}`);
57
+ }
58
+ console.log(` Global config: ${getConfigPath()}`);
59
+ console.log(` Workspace: ${globalConfig.activeWorkspaceId || "(not set)"}`);
60
+ console.log(` Project: ${globalConfig.activeProjectId || "(not set)"}`);
61
+ // Show effective (active) context
62
+ const effectiveWorkspace = getActiveWorkspaceId();
63
+ const effectiveProject = getActiveProjectId();
64
+ const wsSource = localConfig?.workspaceId
65
+ ? "local"
66
+ : globalConfig.activeWorkspaceId
67
+ ? "global"
68
+ : "";
69
+ const projSource = localConfig?.projectId
70
+ ? "local"
71
+ : globalConfig.activeProjectId
72
+ ? "global"
73
+ : "";
74
+ console.log("\n Active (effective):");
75
+ console.log(` Workspace: ${effectiveWorkspace || "(not set)"}${wsSource ? ` ← ${wsSource}` : ""}`);
76
+ console.log(` Project: ${effectiveProject || "(not set)"}${projSource ? ` ← ${projSource}` : ""}`);
77
+ }
78
+ else {
79
+ console.log("Status: Not configured\n");
80
+ console.log("Run: npx @gethmy/mcp setup");
81
+ console.log("Get an API key at: https://gethmy.com/user/keys");
82
+ }
83
+ });
84
+ program
85
+ .command("reset")
86
+ .description("Remove stored configuration")
87
+ .action(() => {
88
+ saveConfig({
89
+ apiKey: null,
90
+ activeWorkspaceId: null,
91
+ activeProjectId: null,
92
+ userEmail: null,
93
+ });
94
+ console.log("Configuration reset successfully");
95
+ console.log("\nTo reconfigure, run: npx @gethmy/mcp setup");
96
+ });
97
+ program
98
+ .command("setup")
99
+ .description("Smart setup wizard for Harmony MCP (recommended)")
100
+ .option("-f, --force", "Overwrite existing configuration files")
101
+ .option("-k, --api-key <key>", "API key (skips prompt)")
102
+ .option("-e, --email <email>", "Your email for auto-assignment")
103
+ .option("-a, --agents <agents...>", "Agents to configure: claude, codex, cursor, windsurf")
104
+ .option("-l, --local", "Install skills locally in project directory")
105
+ .option("-g, --global", "Install skills globally (recommended)")
106
+ .option("-w, --workspace <id>", "Set workspace context")
107
+ .option("-p, --project <id>", "Set project context")
108
+ .option("--skip-context", "Skip workspace/project selection")
109
+ .option("--skip-docs", "Skip project docs scaffold/verification")
110
+ .action(async (options) => {
111
+ await runSetup({
112
+ force: options.force,
113
+ apiKey: options.apiKey,
114
+ userEmail: options.email,
115
+ agents: options.agents,
116
+ installMode: options.global
117
+ ? "global"
118
+ : options.local
119
+ ? "local"
120
+ : undefined,
121
+ workspaceId: options.workspace,
122
+ projectId: options.project,
123
+ skipContext: options.skipContext,
124
+ skipDocs: options.skipDocs,
125
+ });
126
+ });
127
+ program.parse();
@@ -0,0 +1,205 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ const DEFAULT_API_URL = "https://gethmy.com/api";
5
+ const LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
6
+ export function getConfigDir() {
7
+ return join(homedir(), ".harmony-mcp");
8
+ }
9
+ export function getConfigPath() {
10
+ return join(getConfigDir(), "config.json");
11
+ }
12
+ export function getLocalConfigPath(cwd) {
13
+ return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
14
+ }
15
+ export function loadConfig() {
16
+ const configPath = getConfigPath();
17
+ if (!existsSync(configPath)) {
18
+ return {
19
+ apiKey: null,
20
+ apiUrl: DEFAULT_API_URL,
21
+ activeWorkspaceId: null,
22
+ activeProjectId: null,
23
+ userEmail: null,
24
+ memoryDir: null,
25
+ };
26
+ }
27
+ try {
28
+ const data = readFileSync(configPath, "utf-8");
29
+ const config = JSON.parse(data);
30
+ return {
31
+ apiKey: config.apiKey || null,
32
+ apiUrl: config.apiUrl || DEFAULT_API_URL,
33
+ activeWorkspaceId: config.activeWorkspaceId || null,
34
+ activeProjectId: config.activeProjectId || null,
35
+ userEmail: config.userEmail || null,
36
+ memoryDir: config.memoryDir || null,
37
+ };
38
+ }
39
+ catch {
40
+ return {
41
+ apiKey: null,
42
+ apiUrl: DEFAULT_API_URL,
43
+ activeWorkspaceId: null,
44
+ activeProjectId: null,
45
+ userEmail: null,
46
+ memoryDir: null,
47
+ };
48
+ }
49
+ }
50
+ export function saveConfig(config) {
51
+ const configDir = getConfigDir();
52
+ const configPath = getConfigPath();
53
+ if (!existsSync(configDir)) {
54
+ mkdirSync(configDir, { recursive: true, mode: 0o700 });
55
+ }
56
+ const existingConfig = loadConfig();
57
+ const newConfig = { ...existingConfig, ...config };
58
+ writeFileSync(configPath, JSON.stringify(newConfig, null, 2), {
59
+ mode: 0o600,
60
+ });
61
+ }
62
+ export function loadLocalConfig(cwd) {
63
+ const localConfigPath = getLocalConfigPath(cwd);
64
+ if (!existsSync(localConfigPath)) {
65
+ return null;
66
+ }
67
+ try {
68
+ const data = readFileSync(localConfigPath, "utf-8");
69
+ const config = JSON.parse(data);
70
+ return {
71
+ workspaceId: config.workspaceId || null,
72
+ projectId: config.projectId || null,
73
+ };
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ export function saveLocalConfig(config, cwd) {
80
+ const localConfigPath = getLocalConfigPath(cwd);
81
+ const existingConfig = loadLocalConfig(cwd) || {
82
+ workspaceId: null,
83
+ projectId: null,
84
+ };
85
+ const newConfig = { ...existingConfig, ...config };
86
+ // Remove null values from the saved config for cleaner output
87
+ const cleanConfig = {};
88
+ if (newConfig.workspaceId)
89
+ cleanConfig.workspaceId = newConfig.workspaceId;
90
+ if (newConfig.projectId)
91
+ cleanConfig.projectId = newConfig.projectId;
92
+ writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
93
+ }
94
+ export function hasLocalConfig(cwd) {
95
+ return existsSync(getLocalConfigPath(cwd));
96
+ }
97
+ export function getApiKey() {
98
+ const config = loadConfig();
99
+ if (!config.apiKey) {
100
+ throw new Error('Not configured. Run "npx @gethmy/mcp setup" to set your API key.\n' +
101
+ "You can generate an API key at https://gethmy.com → Settings → API Keys.");
102
+ }
103
+ return config.apiKey;
104
+ }
105
+ export function getApiUrl() {
106
+ const config = loadConfig();
107
+ return config.apiUrl;
108
+ }
109
+ export function getUserEmail() {
110
+ const config = loadConfig();
111
+ return config.userEmail;
112
+ }
113
+ export function setUserEmail(email) {
114
+ saveConfig({ userEmail: email });
115
+ }
116
+ export function setActiveWorkspace(workspaceId, options) {
117
+ if (options?.local) {
118
+ saveLocalConfig({ workspaceId }, options.cwd);
119
+ }
120
+ else {
121
+ saveConfig({ activeWorkspaceId: workspaceId });
122
+ }
123
+ }
124
+ export function setActiveProject(projectId, options) {
125
+ if (options?.local) {
126
+ saveLocalConfig({ projectId }, options.cwd);
127
+ }
128
+ else {
129
+ saveConfig({ activeProjectId: projectId });
130
+ }
131
+ }
132
+ export function getActiveWorkspaceId(cwd) {
133
+ // Local config takes precedence over global
134
+ const localConfig = loadLocalConfig(cwd);
135
+ if (localConfig?.workspaceId) {
136
+ return localConfig.workspaceId;
137
+ }
138
+ return loadConfig().activeWorkspaceId;
139
+ }
140
+ export function getActiveProjectId(cwd) {
141
+ // Local config takes precedence over global
142
+ const localConfig = loadLocalConfig(cwd);
143
+ if (localConfig?.projectId) {
144
+ return localConfig.projectId;
145
+ }
146
+ return loadConfig().activeProjectId;
147
+ }
148
+ export function isConfigured() {
149
+ const config = loadConfig();
150
+ return !!config.apiKey;
151
+ }
152
+ /**
153
+ * Check if skills are already installed (globally or locally).
154
+ * Returns installation status and location.
155
+ */
156
+ export function areSkillsInstalled(cwd) {
157
+ const home = homedir();
158
+ const workingDir = cwd || process.cwd();
159
+ const foundPaths = [];
160
+ // Check global skills directory
161
+ const globalSkillsDir = join(home, ".agents", "skills");
162
+ const globalSkillPath = join(globalSkillsDir, "hmy", "SKILL.md");
163
+ if (existsSync(globalSkillPath)) {
164
+ foundPaths.push(globalSkillPath);
165
+ return { installed: true, location: "global", paths: foundPaths };
166
+ }
167
+ // Check Claude global skills (symlinked from global skills)
168
+ const claudeGlobalSkill = join(home, ".claude", "skills", "hmy.md");
169
+ if (existsSync(claudeGlobalSkill)) {
170
+ foundPaths.push(claudeGlobalSkill);
171
+ return { installed: true, location: "global", paths: foundPaths };
172
+ }
173
+ // Check Claude global skills (alternate SKILL.md format)
174
+ const claudeGlobalSkillAlt = join(home, ".claude", "skills", "hmy", "SKILL.md");
175
+ if (existsSync(claudeGlobalSkillAlt)) {
176
+ foundPaths.push(claudeGlobalSkillAlt);
177
+ return { installed: true, location: "global", paths: foundPaths };
178
+ }
179
+ // Check local skills in project directory
180
+ const localSkillPath = join(workingDir, ".claude", "skills", "hmy.md");
181
+ if (existsSync(localSkillPath)) {
182
+ foundPaths.push(localSkillPath);
183
+ return { installed: true, location: "local", paths: foundPaths };
184
+ }
185
+ // Check local skills in project directory (alternate SKILL.md format)
186
+ const localSkillPathAlt = join(workingDir, ".claude", "skills", "hmy", "SKILL.md");
187
+ if (existsSync(localSkillPathAlt)) {
188
+ foundPaths.push(localSkillPathAlt);
189
+ return { installed: true, location: "local", paths: foundPaths };
190
+ }
191
+ return { installed: false, location: null, paths: [] };
192
+ }
193
+ /**
194
+ * Check if project context is configured in the local directory.
195
+ */
196
+ export function hasProjectContext(cwd) {
197
+ const localConfig = loadLocalConfig(cwd);
198
+ return !!(localConfig?.workspaceId || localConfig?.projectId);
199
+ }
200
+ export function getMemoryDir() {
201
+ const config = loadConfig();
202
+ if (config.memoryDir)
203
+ return config.memoryDir;
204
+ return join(homedir(), ".harmony", "memory");
205
+ }