@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.
Files changed (34) hide show
  1. package/dist/lib/api-client.js +2099 -648
  2. package/dist/lib/config.js +217 -201
  3. package/package.json +9 -5
  4. package/src/memory-cleanup.ts +2 -4
  5. package/dist/lib/__tests__/active-learning.test.js +0 -386
  6. package/dist/lib/__tests__/agent-performance-profiles.test.js +0 -325
  7. package/dist/lib/__tests__/auto-session.test.js +0 -661
  8. package/dist/lib/__tests__/context-assembly.test.js +0 -362
  9. package/dist/lib/__tests__/graph-expansion.test.js +0 -150
  10. package/dist/lib/__tests__/integration-memory-crud.test.js +0 -797
  11. package/dist/lib/__tests__/integration-memory-system.test.js +0 -281
  12. package/dist/lib/__tests__/lifecycle-maintenance.test.js +0 -207
  13. package/dist/lib/__tests__/pattern-detection.test.js +0 -295
  14. package/dist/lib/__tests__/prompt-builder.test.js +0 -418
  15. package/dist/lib/active-learning.js +0 -822
  16. package/dist/lib/auto-session.js +0 -214
  17. package/dist/lib/cli.js +0 -138
  18. package/dist/lib/consolidation.js +0 -303
  19. package/dist/lib/context-assembly.js +0 -884
  20. package/dist/lib/graph-expansion.js +0 -163
  21. package/dist/lib/http.js +0 -175
  22. package/dist/lib/index.js +0 -7
  23. package/dist/lib/lifecycle-maintenance.js +0 -88
  24. package/dist/lib/memory-cleanup.js +0 -455
  25. package/dist/lib/onboard.js +0 -36
  26. package/dist/lib/prompt-builder.js +0 -488
  27. package/dist/lib/remote.js +0 -166
  28. package/dist/lib/server.js +0 -3365
  29. package/dist/lib/skills.js +0 -593
  30. package/dist/lib/tui/agents.js +0 -116
  31. package/dist/lib/tui/docs.js +0 -744
  32. package/dist/lib/tui/setup.js +0 -934
  33. package/dist/lib/tui/theme.js +0 -95
  34. package/dist/lib/tui/writer.js +0 -200
@@ -1,934 +0,0 @@
1
- import { existsSync, lstatSync, mkdirSync, symlinkSync, unlinkSync, } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
- import * as p from "@clack/prompts";
5
- import { areSkillsInstalled, getConfigPath, getLocalConfigPath, hasProjectContext, isConfigured, loadConfig, saveConfig, saveLocalConfig, setActiveProject, setActiveWorkspace, } from "../config.js";
6
- import { onboardNewUser } from "../onboard.js";
7
- import { buildSkillFile, HARMONY_WORKFLOW_PROMPT } from "../skills.js";
8
- import { detectAgents } from "./agents.js";
9
- import { runDocsStep } from "./docs.js";
10
- import { colors, formatPath, messages } from "./theme.js";
11
- import { getWriteSummary, writeFilesWithProgress } from "./writer.js";
12
- // Central skills directory for global installation
13
- const GLOBAL_SKILLS_DIR = join(homedir(), ".agents", "skills");
14
- // API base URL
15
- const API_URL = "https://app.gethmy.com/api";
16
- /**
17
- * Register MCP server using Claude CLI
18
- * Returns true if successful, false if CLI unavailable or failed
19
- */
20
- async function registerMcpServer() {
21
- try {
22
- const { execSync } = await import("node:child_process");
23
- // Use the official CLI command to register the MCP server (npx for no global install)
24
- execSync("claude mcp add --transport stdio harmony -- npx -y @gethmy/mcp@latest serve", {
25
- stdio: "pipe",
26
- });
27
- return true;
28
- }
29
- catch {
30
- return false;
31
- }
32
- }
33
- /**
34
- * Write MCP server config directly to settings.json (fallback method)
35
- */
36
- async function writeMcpConfigFallback(home) {
37
- const { readFileSync, writeFileSync, mkdirSync, existsSync } = await import("node:fs");
38
- const settingsPath = join(home, ".claude", "settings.json");
39
- const settingsDir = dirname(settingsPath);
40
- // Ensure directory exists
41
- if (!existsSync(settingsDir)) {
42
- mkdirSync(settingsDir, { recursive: true });
43
- }
44
- // Read existing settings or start fresh
45
- let settings = {};
46
- if (existsSync(settingsPath)) {
47
- try {
48
- settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
49
- }
50
- catch {
51
- // Invalid JSON, start fresh
52
- }
53
- }
54
- // Merge mcpServers config
55
- const mcpServers = settings.mcpServers || {};
56
- mcpServers.harmony = {
57
- command: "npx",
58
- args: ["-y", "@gethmy/mcp@latest", "serve"],
59
- };
60
- settings.mcpServers = mcpServers;
61
- // Write back
62
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
63
- }
64
- /**
65
- * Validate API key by testing connectivity
66
- */
67
- async function validateApiKey(apiKey, apiUrl = API_URL) {
68
- try {
69
- const response = await fetch(`${apiUrl}/v1/workspaces`, {
70
- method: "GET",
71
- headers: {
72
- "Content-Type": "application/json",
73
- "X-API-Key": apiKey,
74
- },
75
- });
76
- if (!response.ok) {
77
- const data = await response.json().catch(() => ({}));
78
- return {
79
- valid: false,
80
- error: data.error || `API returned ${response.status}`,
81
- };
82
- }
83
- // Try to get user info from /me endpoint if available
84
- const meResponse = await fetch(`${apiUrl}/v1/me`, {
85
- method: "GET",
86
- headers: {
87
- "Content-Type": "application/json",
88
- "X-API-Key": apiKey,
89
- },
90
- });
91
- if (meResponse.ok) {
92
- const meData = await meResponse.json();
93
- return { valid: true, email: meData.user?.email };
94
- }
95
- return { valid: true };
96
- }
97
- catch (error) {
98
- return {
99
- valid: false,
100
- error: error instanceof Error ? error.message : "Connection failed",
101
- };
102
- }
103
- }
104
- /**
105
- * Fetch workspaces from API
106
- */
107
- async function fetchWorkspaces(apiKey) {
108
- const response = await fetch(`${API_URL}/v1/workspaces`, {
109
- method: "GET",
110
- headers: {
111
- "Content-Type": "application/json",
112
- "X-API-Key": apiKey,
113
- },
114
- });
115
- if (!response.ok) {
116
- throw new Error(`Failed to fetch workspaces: ${response.status}`);
117
- }
118
- const data = await response.json();
119
- return data.workspaces || [];
120
- }
121
- /**
122
- * Fetch projects from API
123
- */
124
- async function fetchProjects(apiKey, workspaceId) {
125
- const response = await fetch(`${API_URL}/v1/workspaces/${workspaceId}/projects`, {
126
- method: "GET",
127
- headers: {
128
- "Content-Type": "application/json",
129
- "X-API-Key": apiKey,
130
- },
131
- });
132
- if (!response.ok) {
133
- throw new Error(`Failed to fetch projects: ${response.status}`);
134
- }
135
- const data = await response.json();
136
- return data.projects || [];
137
- }
138
- /**
139
- * Generate agent configuration files
140
- */
141
- function getAgentFiles(agentId, cwd, installMode = "global") {
142
- const home = homedir();
143
- const files = [];
144
- const symlinks = [];
145
- switch (agentId) {
146
- case "claude": {
147
- // Claude Code skill files built from the central skill registry
148
- const skillContent = buildSkillFile("hmy", "claude");
149
- const planSkillContent = buildSkillFile("hmy-plan", "claude");
150
- if (installMode === "global") {
151
- // Global mode: Write skills to ~/.agents/skills/, symlink directories to ~/.claude/skills/
152
- files.push({
153
- path: join(GLOBAL_SKILLS_DIR, "hmy", "SKILL.md"),
154
- content: skillContent,
155
- type: "text",
156
- });
157
- symlinks.push({
158
- target: join(GLOBAL_SKILLS_DIR, "hmy"),
159
- link: join(home, ".claude", "skills", "hmy"),
160
- });
161
- files.push({
162
- path: join(GLOBAL_SKILLS_DIR, "hmy-plan", "SKILL.md"),
163
- content: planSkillContent,
164
- type: "text",
165
- });
166
- symlinks.push({
167
- target: join(GLOBAL_SKILLS_DIR, "hmy-plan"),
168
- link: join(home, ".claude", "skills", "hmy-plan"),
169
- });
170
- }
171
- else {
172
- // Local mode: Write skills directly to project directory
173
- files.push({
174
- path: join(cwd, ".claude", "skills", "hmy", "SKILL.md"),
175
- content: skillContent,
176
- type: "text",
177
- });
178
- files.push({
179
- path: join(cwd, ".claude", "skills", "hmy-plan", "SKILL.md"),
180
- content: planSkillContent,
181
- type: "text",
182
- });
183
- }
184
- // Note: MCP server registration is handled separately via `claude mcp add` CLI
185
- // in runSetup() after file writing, with fallback to settings.json if CLI unavailable
186
- break;
187
- }
188
- case "codex": {
189
- // AGENTS.md in project root (always local - project context)
190
- const agentsContent = `# Harmony Integration
191
-
192
- This project uses Harmony for task management. When working on tasks:
193
-
194
- ## Starting Work on a Card
195
-
196
- When given a card reference (e.g., #42 or a card name), follow this workflow:
197
-
198
- 1. Use \`harmony_get_card_by_short_id\` or \`harmony_search_cards\` to find the card
199
- 2. Move the card to "In Progress" using \`harmony_move_card\`
200
- 3. Add the "agent" label using \`harmony_add_label_to_card\`
201
- 4. Start a session with \`harmony_start_agent_session\` (agentIdentifier: "codex", agentName: "OpenAI Codex")
202
- 5. Show the card details to the user
203
- 6. Use \`harmony_generate_prompt\` to get guidance, then implement the solution
204
- 7. Update progress periodically with \`harmony_update_agent_progress\`
205
- 8. When done, call \`harmony_end_agent_session\` and move to "Review"
206
-
207
- ## Auto-Detect Card for Implementation Tasks
208
-
209
- Before implementing a plan or feature, check if it maps to an existing Harmony card:
210
-
211
- 1. Use \`harmony_search_cards\` with keywords from the task description
212
- 2. If a match is found, call \`harmony_start_agent_session\` (agentIdentifier: "claude-code", agentName: "Claude Code", moveToColumn: "In Progress", addLabels: ["agent"])
213
- 3. Update progress with \`harmony_update_agent_progress\` at milestones
214
- 4. When done, call \`harmony_end_agent_session\` with status: "completed", moveToColumn: "Review"
215
-
216
- Skip if: work was already started with a card reference, or no matching card exists.
217
-
218
- ## Available Harmony Tools
219
-
220
- - \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\` - Find cards
221
- - \`harmony_move_card\` - Move cards between columns
222
- - \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\` - Manage labels
223
- - \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\` - Track work
224
- - \`harmony_get_board\` - Get board state
225
- - \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
226
- `;
227
- files.push({
228
- path: join(cwd, "AGENTS.md"),
229
- content: agentsContent,
230
- type: "text",
231
- });
232
- // Codex prompt file
233
- const promptContent = `---
234
- name: hmy
235
- description: Start working on a Harmony card
236
- arguments:
237
- - name: card
238
- description: Card reference (#42, UUID, or name)
239
- required: true
240
- ---
241
-
242
- ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "{{card}}").replace("Your agent identifier", "codex").replace("Your agent name", "OpenAI Codex")}
243
- `;
244
- if (installMode === "global") {
245
- // Global mode: Write prompt to central location, symlink to ~/.codex/prompts/
246
- files.push({
247
- path: join(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
248
- content: promptContent,
249
- type: "text",
250
- });
251
- symlinks.push({
252
- target: join(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
253
- link: join(home, ".codex", "prompts", "hmy.md"),
254
- });
255
- }
256
- else {
257
- // Local mode: Write prompt directly to ~/.codex/prompts/
258
- files.push({
259
- path: join(home, ".codex", "prompts", "hmy.md"),
260
- content: promptContent,
261
- type: "text",
262
- });
263
- }
264
- // Codex config.toml (always global)
265
- const tomlContent = `
266
- # Harmony MCP Server
267
- [mcp_servers.harmony]
268
- command = "npx"
269
- args = ["-y", "@gethmy/mcp@latest", "serve"]
270
- `;
271
- files.push({
272
- path: join(home, ".codex", "config.toml"),
273
- content: tomlContent,
274
- type: "toml",
275
- tomlSection: "mcp_servers.harmony",
276
- });
277
- break;
278
- }
279
- case "cursor": {
280
- // Cursor MCP config (always project-level)
281
- files.push({
282
- path: join(cwd, ".cursor", "mcp.json"),
283
- content: JSON.stringify({
284
- mcpServers: {
285
- harmony: {
286
- command: "npx",
287
- args: ["-y", "@gethmy/mcp@latest", "serve"],
288
- },
289
- },
290
- }, null, 2),
291
- type: "json",
292
- });
293
- // Cursor rule file
294
- const ruleContent = `---
295
- description: Harmony card workflow rule
296
- globs:
297
- - "**/*"
298
- alwaysApply: false
299
- ---
300
-
301
- # Harmony Integration
302
-
303
- When the user asks you to work on a Harmony card (references like #42, card names, or UUIDs):
304
-
305
- ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "cursor").replace("Your agent name", "Cursor AI")}
306
- `;
307
- if (installMode === "global") {
308
- // Global mode: Write rule to central location, symlink to ~/.cursor/rules/
309
- files.push({
310
- path: join(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
311
- content: ruleContent,
312
- type: "text",
313
- });
314
- symlinks.push({
315
- target: join(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
316
- link: join(home, ".cursor", "rules", "harmony.mdc"),
317
- });
318
- }
319
- else {
320
- // Local mode: Write rule directly to project directory
321
- files.push({
322
- path: join(cwd, ".cursor", "rules", "harmony.mdc"),
323
- content: ruleContent,
324
- type: "text",
325
- });
326
- }
327
- break;
328
- }
329
- case "windsurf": {
330
- // Windsurf global MCP config (always global)
331
- files.push({
332
- path: join(home, ".codeium", "windsurf", "mcp_config.json"),
333
- content: JSON.stringify({
334
- mcpServers: {
335
- harmony: {
336
- command: "npx",
337
- args: ["-y", "@gethmy/mcp@latest", "serve"],
338
- disabled: false,
339
- alwaysAllow: [],
340
- },
341
- },
342
- }, null, 2),
343
- type: "json",
344
- });
345
- // Windsurf rule file
346
- const ruleContent = `---
347
- trigger: model_decision
348
- description: Activate when user asks to work on a Harmony card (references like #42, card names, or task management)
349
- ---
350
-
351
- # Harmony Card Workflow
352
-
353
- When working on a Harmony card:
354
-
355
- ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "windsurf").replace("Your agent name", "Windsurf AI")}
356
- `;
357
- if (installMode === "global") {
358
- // Global mode: Write rule to central location, symlink to ~/.codeium/windsurf/rules/
359
- files.push({
360
- path: join(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
361
- content: ruleContent,
362
- type: "text",
363
- });
364
- symlinks.push({
365
- target: join(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
366
- link: join(home, ".codeium", "windsurf", "rules", "harmony.md"),
367
- });
368
- }
369
- else {
370
- // Local mode: Write rule directly to project directory
371
- files.push({
372
- path: join(cwd, ".windsurf", "rules", "harmony.md"),
373
- content: ruleContent,
374
- type: "text",
375
- });
376
- }
377
- break;
378
- }
379
- }
380
- return { files, symlinks };
381
- }
382
- /**
383
- * Main setup wizard with smart detection
384
- */
385
- export async function runSetup(options = {}) {
386
- const cwd = process.cwd();
387
- const home = homedir();
388
- console.clear();
389
- console.log(messages.header());
390
- // Check existing configuration
391
- const existingConfig = loadConfig();
392
- const alreadyConfigured = isConfigured();
393
- const skillsStatus = areSkillsInstalled(cwd);
394
- const hasContext = hasProjectContext(cwd);
395
- // Track what we need to do
396
- let needsApiKey = !alreadyConfigured;
397
- let needsSkills = !skillsStatus.installed || options.force;
398
- let needsContext = !hasContext && !options.skipContext;
399
- // If workspace/project provided via flags, we'll set context
400
- if (options.workspaceId || options.projectId) {
401
- needsContext = true;
402
- }
403
- // Step 1: API Key (or create account)
404
- let apiKey = options.apiKey || existingConfig.apiKey;
405
- let userEmail = options.userEmail || existingConfig.userEmail || undefined;
406
- let selectedWorkspaceIdFromSignup;
407
- let selectedProjectIdFromSignup;
408
- let selectedWorkspaceNameFromSignup;
409
- let selectedProjectNameFromSignup;
410
- let createdNewAccount = false;
411
- if (needsApiKey || !apiKey || !apiKey.startsWith("hmy_")) {
412
- // Determine path: create account or enter API key
413
- let useNewAccount = options.newAccount === true;
414
- if (!useNewAccount && options.apiKey) {
415
- // API key passed via flag — skip the choice prompt
416
- useNewAccount = false;
417
- }
418
- else if (!useNewAccount && !options.apiKey) {
419
- const getStarted = await p.select({
420
- message: "How would you like to get started?",
421
- options: [
422
- {
423
- value: "create",
424
- label: "Create a free account",
425
- hint: "recommended for new users",
426
- },
427
- {
428
- value: "apikey",
429
- label: "I already have an API key",
430
- },
431
- ],
432
- });
433
- if (p.isCancel(getStarted)) {
434
- p.cancel("Setup cancelled");
435
- process.exit(0);
436
- }
437
- useNewAccount = getStarted === "create";
438
- }
439
- if (useNewAccount) {
440
- // --- Create account flow ---
441
- const fullName = options.name ||
442
- (await p.text({
443
- message: "Full name",
444
- placeholder: "Jane Smith",
445
- validate: (v) => {
446
- if (!v || v.trim().length === 0)
447
- return "Name is required";
448
- if (v.length > 100)
449
- return "Name must be 100 characters or less";
450
- return undefined;
451
- },
452
- }));
453
- if (p.isCancel(fullName)) {
454
- p.cancel("Setup cancelled");
455
- process.exit(0);
456
- }
457
- const email = options.userEmail ||
458
- (await p.text({
459
- message: "Email",
460
- placeholder: "you@example.com",
461
- validate: (v) => {
462
- if (!v)
463
- return "Email is required";
464
- if (v.length > 254)
465
- return "Email is too long";
466
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v))
467
- return "Invalid email format";
468
- return undefined;
469
- },
470
- }));
471
- if (p.isCancel(email)) {
472
- p.cancel("Setup cancelled");
473
- process.exit(0);
474
- }
475
- const password = (await p.password({
476
- message: "Password",
477
- validate: (v) => {
478
- if (!v)
479
- return "Password is required";
480
- if (v.length < 8)
481
- return "Password must be at least 8 characters";
482
- if (v.length > 128)
483
- return "Password must be 128 characters or less";
484
- return undefined;
485
- },
486
- }));
487
- if (p.isCancel(password)) {
488
- p.cancel("Setup cancelled");
489
- process.exit(0);
490
- }
491
- const spinner = p.spinner();
492
- spinner.start("Creating your account...");
493
- try {
494
- const result = await onboardNewUser({
495
- email: email,
496
- password: password,
497
- fullName: fullName,
498
- });
499
- spinner.stop(colors.success(`Account created for ${result.user.email}`));
500
- apiKey = result.apiKey.rawKey;
501
- userEmail = result.user.email;
502
- selectedWorkspaceIdFromSignup = result.workspace.id;
503
- selectedProjectIdFromSignup = result.project.id;
504
- selectedWorkspaceNameFromSignup = result.workspace.name;
505
- selectedProjectNameFromSignup = result.project.name;
506
- createdNewAccount = true;
507
- needsApiKey = true;
508
- // Save config immediately
509
- saveConfig({ apiKey, userEmail, apiUrl: API_URL });
510
- setActiveWorkspace(selectedWorkspaceIdFromSignup);
511
- setActiveProject(selectedProjectIdFromSignup);
512
- p.log.success("Workspace and board created");
513
- }
514
- catch (error) {
515
- spinner.stop(colors.error("Account creation failed"));
516
- const msg = error instanceof Error ? error.message : "Unknown error";
517
- if (msg.includes("already") || msg.includes("409")) {
518
- p.log.error("Account already exists. Sign in at app.gethmy.com to get your API key, or re-run setup and choose 'I already have an API key'.");
519
- }
520
- else {
521
- p.log.error(msg);
522
- p.log.info("Please try again or visit https://app.gethmy.com");
523
- }
524
- process.exit(1);
525
- }
526
- }
527
- else {
528
- // --- Existing API key flow ---
529
- const keyInput = await p.text({
530
- message: "Enter your Harmony API key",
531
- placeholder: "hmy_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
532
- validate: (value) => {
533
- if (!value)
534
- return "API key is required";
535
- if (!value.startsWith("hmy_"))
536
- return 'API key must start with "hmy_"';
537
- if (value.length < 20)
538
- return "API key is too short";
539
- return undefined;
540
- },
541
- });
542
- if (p.isCancel(keyInput)) {
543
- p.cancel("Setup cancelled");
544
- process.exit(0);
545
- }
546
- apiKey = keyInput;
547
- needsApiKey = true;
548
- }
549
- }
550
- else {
551
- p.log.success(`Using existing API key: ${apiKey.slice(0, 8)}...`);
552
- }
553
- // Validate API key (skip if we just created account — key is guaranteed valid)
554
- const spinner = p.spinner();
555
- if (!createdNewAccount) {
556
- spinner.start("Validating API key...");
557
- const validation = await validateApiKey(apiKey);
558
- if (!validation.valid) {
559
- spinner.stop(colors.error("API key validation failed"));
560
- p.log.error(validation.error || "Could not connect to Harmony API");
561
- p.log.info("Get an API key at: https://app.gethmy.com/user/keys");
562
- process.exit(1);
563
- }
564
- if (!userEmail) {
565
- userEmail = validation.email;
566
- }
567
- spinner.stop(colors.success(userEmail ? `Connected as ${userEmail}` : "API key validated"));
568
- }
569
- // Step 2: Check if skills are already installed
570
- let selectedAgents = [];
571
- let installMode = options.installMode || "global";
572
- if (skillsStatus.installed && !options.force) {
573
- p.log.success(`Skills already installed (${skillsStatus.location})`);
574
- const reinstall = await p.confirm({
575
- message: "Reinstall skills?",
576
- initialValue: false,
577
- });
578
- if (p.isCancel(reinstall)) {
579
- p.cancel("Setup cancelled");
580
- process.exit(0);
581
- }
582
- needsSkills = reinstall;
583
- }
584
- if (needsSkills) {
585
- // Detect and select agents
586
- const detectedAgents = detectAgents(cwd);
587
- if (options.agents && options.agents.length > 0) {
588
- selectedAgents = options.agents;
589
- }
590
- else {
591
- const agentOptions = detectedAgents.map((agent) => ({
592
- value: agent.id,
593
- label: agent.name,
594
- hint: agent.detected
595
- ? colors.success(`${agent.description} (detected)`)
596
- : colors.dim(`${agent.description}`),
597
- }));
598
- const agentSelection = await p.multiselect({
599
- message: "Select agents to configure",
600
- options: agentOptions,
601
- initialValues: detectedAgents
602
- .filter((a) => a.detected)
603
- .map((a) => a.id),
604
- required: true,
605
- });
606
- if (p.isCancel(agentSelection)) {
607
- p.cancel("Setup cancelled");
608
- process.exit(0);
609
- }
610
- selectedAgents = agentSelection;
611
- }
612
- if (selectedAgents.length === 0) {
613
- p.log.warning("No agents selected. Skipping skills installation.");
614
- needsSkills = false;
615
- }
616
- else if (!options.installMode) {
617
- // Select install mode
618
- const modeSelection = await p.select({
619
- message: "Where should Harmony skills be installed?",
620
- options: [
621
- {
622
- value: "global",
623
- label: "Global (shared) - Recommended",
624
- hint: "Skills in ~/.agents/skills/, available in all projects",
625
- },
626
- {
627
- value: "local",
628
- label: "Local (project directory)",
629
- hint: "Skills in .claude/skills/, committed to git",
630
- },
631
- ],
632
- initialValue: "global",
633
- });
634
- if (p.isCancel(modeSelection)) {
635
- p.cancel("Setup cancelled");
636
- process.exit(0);
637
- }
638
- installMode = modeSelection;
639
- }
640
- }
641
- // Step 3: Workspace and Project Selection
642
- let selectedWorkspaceId = selectedWorkspaceIdFromSignup || options.workspaceId;
643
- let selectedProjectId = selectedProjectIdFromSignup || options.projectId;
644
- let selectedWorkspaceName = selectedWorkspaceNameFromSignup;
645
- let selectedProjectName = selectedProjectNameFromSignup;
646
- // Skip context selection if we just created a new account
647
- if (createdNewAccount) {
648
- needsContext = false;
649
- }
650
- if (needsContext && !options.skipContext) {
651
- // Fetch workspaces
652
- spinner.start("Fetching workspaces...");
653
- let workspaces = [];
654
- try {
655
- workspaces = await fetchWorkspaces(apiKey);
656
- spinner.stop(colors.success(`Found ${workspaces.length} workspace(s)`));
657
- }
658
- catch (_error) {
659
- spinner.stop(colors.warning("Could not fetch workspaces"));
660
- p.log.warning("Skipping workspace/project selection. You can set this later.");
661
- needsContext = false;
662
- }
663
- if (needsContext && workspaces.length > 0) {
664
- // Select workspace
665
- if (!selectedWorkspaceId) {
666
- const workspaceOptions = workspaces.map((ws) => ({
667
- value: ws.id,
668
- label: ws.name,
669
- }));
670
- const workspaceSelection = await p.select({
671
- message: "Select workspace",
672
- options: workspaceOptions,
673
- });
674
- if (p.isCancel(workspaceSelection)) {
675
- p.cancel("Setup cancelled");
676
- process.exit(0);
677
- }
678
- selectedWorkspaceId = workspaceSelection;
679
- }
680
- selectedWorkspaceName = workspaces.find((w) => w.id === selectedWorkspaceId)?.name;
681
- // Fetch and select project
682
- spinner.start("Fetching projects...");
683
- let projects = [];
684
- try {
685
- projects = await fetchProjects(apiKey, selectedWorkspaceId);
686
- spinner.stop(colors.success(`Found ${projects.length} project(s)`));
687
- }
688
- catch (_error) {
689
- spinner.stop(colors.warning("Could not fetch projects"));
690
- p.log.warning("Skipping project selection. You can set this later.");
691
- }
692
- if (projects.length > 0 && !selectedProjectId) {
693
- const projectOptions = projects.map((proj) => ({
694
- value: proj.id,
695
- label: proj.name,
696
- hint: proj.description
697
- ? colors.dim(proj.description.slice(0, 50))
698
- : undefined,
699
- }));
700
- const projectSelection = await p.select({
701
- message: "Select project",
702
- options: projectOptions,
703
- });
704
- if (p.isCancel(projectSelection)) {
705
- p.cancel("Setup cancelled");
706
- process.exit(0);
707
- }
708
- selectedProjectId = projectSelection;
709
- selectedProjectName = projects.find((p) => p.id === selectedProjectId)?.name;
710
- }
711
- }
712
- }
713
- // Step 4: Collect all files and symlinks to create
714
- const allFiles = [];
715
- const allSymlinks = [];
716
- // Always save global config if we have an API key change
717
- if (needsApiKey || !alreadyConfigured) {
718
- allFiles.push({
719
- path: getConfigPath(),
720
- content: JSON.stringify({
721
- apiKey,
722
- apiUrl: API_URL,
723
- userEmail: userEmail || null,
724
- activeWorkspaceId: null,
725
- activeProjectId: null,
726
- }, null, 2),
727
- type: "text", // Use text to avoid merging
728
- });
729
- }
730
- // Project docs scaffold / verification
731
- if (!options.skipDocs) {
732
- const docsResult = await runDocsStep(cwd);
733
- if (!docsResult.skipped) {
734
- for (const file of docsResult.files) {
735
- allFiles.push(file);
736
- }
737
- }
738
- }
739
- // Agent-specific files
740
- if (needsSkills && selectedAgents.length > 0) {
741
- for (const agentId of selectedAgents) {
742
- const { files, symlinks } = getAgentFiles(agentId, cwd, installMode);
743
- allFiles.push(...files);
744
- allSymlinks.push(...symlinks);
745
- }
746
- }
747
- // Step 5: Show summary
748
- const detectedAgents = detectAgents(cwd);
749
- console.log("");
750
- p.log.step("Summary");
751
- console.log("");
752
- console.log(` ${colors.bold("API Key:")} ${apiKey.slice(0, 8)}...`);
753
- if (userEmail) {
754
- console.log(` ${colors.bold("Email:")} ${userEmail}`);
755
- }
756
- if (needsSkills && selectedAgents.length > 0) {
757
- console.log(` ${colors.bold("Agents:")} ${selectedAgents.map((a) => detectedAgents.find((d) => d.id === a)?.name).join(", ")}`);
758
- console.log(` ${colors.bold("Install:")} ${installMode === "global" ? "Global (~/.agents/skills/)" : "Local (project)"}`);
759
- }
760
- else {
761
- console.log(` ${colors.bold("Skills:")} Already installed (${skillsStatus.location || "none"})`);
762
- }
763
- if (selectedWorkspaceId) {
764
- console.log(` ${colors.bold("Workspace:")} ${selectedWorkspaceName || selectedWorkspaceId}`);
765
- }
766
- if (selectedProjectId) {
767
- console.log(` ${colors.bold("Project:")} ${selectedProjectName || selectedProjectId}`);
768
- }
769
- if (allFiles.length > 0) {
770
- const summary = getWriteSummary(allFiles, {
771
- force: options.force || needsSkills,
772
- });
773
- if (summary.toCreate.length > 0) {
774
- console.log("");
775
- console.log(` ${colors.success("Files to create:")}`);
776
- for (const path of summary.toCreate) {
777
- console.log(` ${colors.dim("\u2022")} ${path}`);
778
- }
779
- }
780
- if (allSymlinks.length > 0) {
781
- console.log("");
782
- console.log(` ${colors.info("Symlinks to create:")}`);
783
- for (const symlink of allSymlinks) {
784
- console.log(` ${colors.dim("\u{1F517}")} ${formatPath(symlink.link, home)} → ${formatPath(symlink.target, home)}`);
785
- }
786
- }
787
- if (summary.toUpdate.length > 0) {
788
- console.log("");
789
- console.log(` ${colors.info("Files to update:")}`);
790
- for (const path of summary.toUpdate) {
791
- console.log(` ${colors.dim("\u2022")} ${path}`);
792
- }
793
- }
794
- if (summary.toSkip.length > 0 && !options.force) {
795
- console.log("");
796
- console.log(` ${colors.dim("Files to skip (already exist):")}`);
797
- for (const path of summary.toSkip) {
798
- console.log(` ${colors.dim("\u2022")} ${path}`);
799
- }
800
- }
801
- }
802
- console.log("");
803
- // Step 6: Confirm and execute
804
- const shouldProceed = await p.confirm({
805
- message: "Proceed with setup?",
806
- initialValue: true,
807
- });
808
- if (p.isCancel(shouldProceed) || !shouldProceed) {
809
- p.cancel("Setup cancelled");
810
- process.exit(0);
811
- }
812
- console.log("");
813
- // Step 7: Write files
814
- // Force-write when user chose to reinstall skills — skill files are
815
- // package-generated content that should always match the installed version.
816
- if (allFiles.length > 0) {
817
- await writeFilesWithProgress(allFiles, {
818
- force: options.force || needsSkills,
819
- });
820
- }
821
- // Step 8: Create symlinks
822
- if (allSymlinks.length > 0) {
823
- for (const symlink of allSymlinks) {
824
- try {
825
- // Ensure parent directory exists
826
- const linkDir = dirname(symlink.link);
827
- if (!existsSync(linkDir)) {
828
- mkdirSync(linkDir, { recursive: true });
829
- }
830
- // Check if link already exists
831
- let linkExists = false;
832
- try {
833
- lstatSync(symlink.link);
834
- linkExists = true;
835
- }
836
- catch {
837
- // Link doesn't exist
838
- }
839
- if (linkExists) {
840
- if (options.force) {
841
- unlinkSync(symlink.link);
842
- }
843
- else {
844
- // Skip existing symlink
845
- continue;
846
- }
847
- }
848
- symlinkSync(symlink.target, symlink.link);
849
- }
850
- catch {
851
- p.log.warning(`Failed to create symlink: ${symlink.link}`);
852
- }
853
- }
854
- }
855
- // Step 8b: Register MCP server for Claude (project-local scope)
856
- const claudeDetected = detectAgents(cwd).some((a) => a.id === "claude" && a.detected);
857
- if (claudeDetected || selectedAgents.includes("claude")) {
858
- const mcpRegistered = await registerMcpServer();
859
- if (mcpRegistered) {
860
- console.log(` ${colors.success("\u2713")} ${colors.dim("MCP server registered via claude CLI")}`);
861
- }
862
- else {
863
- // Fallback: write directly to settings.json
864
- try {
865
- await writeMcpConfigFallback(home);
866
- console.log(` ${colors.success("\u2713")} ${colors.dim(formatPath(join(home, ".claude", "settings.json"), home))} ${colors.dim("(updated)")}`);
867
- }
868
- catch {
869
- p.log.warning("Could not register MCP server. Run manually: claude mcp add --transport stdio harmony -- npx -y @gethmy/mcp@latest serve");
870
- }
871
- }
872
- }
873
- // Step 9: Save local context
874
- if (selectedWorkspaceId || selectedProjectId) {
875
- const localConfig = {};
876
- if (selectedWorkspaceId)
877
- localConfig.workspaceId = selectedWorkspaceId;
878
- if (selectedProjectId)
879
- localConfig.projectId = selectedProjectId;
880
- saveLocalConfig(localConfig, cwd);
881
- console.log(` ${colors.success("\u2713")} ${colors.dim(formatPath(getLocalConfigPath(cwd), home))} ${colors.dim("(created)")}`);
882
- }
883
- // Step 10: Show completion message
884
- console.log("");
885
- p.outro(colors.success("Setup complete!"));
886
- if (createdNewAccount && selectedWorkspaceNameFromSignup) {
887
- // New account: show board URL and next steps
888
- const wsSlug = selectedWorkspaceNameFromSignup
889
- .toLowerCase()
890
- .replace(/[^a-z0-9]+/g, "-")
891
- .replace(/(^-|-$)/g, "");
892
- const projSlug = (selectedProjectNameFromSignup || "my-first-board")
893
- .toLowerCase()
894
- .replace(/[^a-z0-9]+/g, "-")
895
- .replace(/(^-|-$)/g, "");
896
- console.log("");
897
- console.log(` ${colors.bold("Your board:")} ${colors.highlight(`https://app.gethmy.com/${wsSlug}/${projSlug}`)}`);
898
- console.log("");
899
- console.log(` ${colors.bold("Next steps:")}`);
900
- console.log(` 1. Open Claude Code and say: ${colors.highlight('"Show me my board"')}`);
901
- console.log(` 2. Create a card: ${colors.highlight('"Create a card called Auth token refresh"')}`);
902
- console.log(` 3. Start the daemon: ${colors.highlight("npx @gethmy/agent")}`);
903
- console.log("");
904
- console.log(` ${colors.dim("Happy shipping!")}`);
905
- }
906
- else {
907
- // Existing user: show config paths and usage
908
- console.log("");
909
- console.log(` ${colors.bold("Configuration:")}`);
910
- console.log(` API key: ${formatPath(getConfigPath(), home)}`);
911
- if (needsSkills && selectedAgents.length > 0) {
912
- console.log(` Skills: ${installMode === "global" ? "~/.agents/skills/ (global)" : ".claude/skills/ (local)"}`);
913
- }
914
- if (selectedWorkspaceId || selectedProjectId) {
915
- console.log(` Context: ${formatPath(getLocalConfigPath(cwd), home)}`);
916
- }
917
- console.log("");
918
- console.log(` ${colors.bold("Usage:")}`);
919
- if (!needsSkills || selectedAgents.includes("claude")) {
920
- console.log(` ${colors.brand("Claude Code:")} ${colors.highlight("/hmy #42")} or ${colors.highlight("/hmy-plan")} ${colors.dim("(create or execute plans)")}`);
921
- }
922
- if (selectedAgents.includes("codex")) {
923
- console.log(` ${colors.brand("Codex:")} ${colors.highlight("/prompts:hmy #42")}`);
924
- }
925
- if (selectedAgents.includes("cursor") ||
926
- selectedAgents.includes("windsurf")) {
927
- console.log(` ${colors.brand("Cursor:")} MCP tools available automatically`);
928
- }
929
- console.log("");
930
- console.log(` ${colors.dim("Add to new project: npx @gethmy/mcp setup")}`);
931
- console.log(` ${colors.dim("Need help? Visit https://app.gethmy.com/docs/mcp")}`);
932
- }
933
- console.log("");
934
- }