@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.
- package/README.md +201 -36
- package/dist/cli.js +20938 -20249
- package/dist/http.js +1957 -0
- package/dist/index.js +17833 -17888
- package/dist/lib/__tests__/active-learning.test.js +386 -0
- package/dist/lib/__tests__/agent-performance-profiles.test.js +325 -0
- package/dist/lib/__tests__/auto-session.test.js +661 -0
- package/dist/lib/__tests__/context-assembly.test.js +362 -0
- package/dist/lib/__tests__/graph-expansion.test.js +150 -0
- package/dist/lib/__tests__/integration-memory-crud.test.js +797 -0
- package/dist/lib/__tests__/integration-memory-system.test.js +281 -0
- package/dist/lib/__tests__/lifecycle-maintenance.test.js +207 -0
- package/dist/lib/__tests__/pattern-detection.test.js +295 -0
- package/dist/lib/__tests__/prompt-builder.test.js +418 -0
- package/dist/lib/active-learning.js +878 -0
- package/dist/lib/api-client.js +548 -0
- package/dist/lib/auto-session.js +173 -0
- package/dist/lib/cli.js +127 -0
- package/dist/lib/config.js +205 -0
- package/dist/lib/consolidation.js +243 -0
- package/dist/lib/context-assembly.js +606 -0
- package/dist/lib/graph-expansion.js +163 -0
- package/dist/lib/http.js +174 -0
- package/dist/lib/index.js +7 -0
- package/dist/lib/lifecycle-maintenance.js +88 -0
- package/dist/lib/prompt-builder.js +483 -0
- package/dist/lib/remote.js +166 -0
- package/dist/lib/server.js +3132 -0
- package/dist/lib/tui/agents.js +116 -0
- package/dist/lib/tui/docs.js +558 -0
- package/dist/lib/tui/setup.js +1068 -0
- package/dist/lib/tui/theme.js +95 -0
- package/dist/lib/tui/writer.js +200 -0
- package/dist/remote.js +34534 -0
- package/dist/server.js +31967 -0
- package/package.json +20 -7
- package/src/__tests__/active-learning.test.ts +483 -0
- package/src/__tests__/agent-performance-profiles.test.ts +468 -0
- package/src/__tests__/auto-session.test.ts +912 -0
- package/src/__tests__/context-assembly.test.ts +506 -0
- package/src/__tests__/graph-expansion.test.ts +285 -0
- package/src/__tests__/integration-memory-crud.test.ts +948 -0
- package/src/__tests__/integration-memory-system.test.ts +321 -0
- package/src/__tests__/lifecycle-maintenance.test.ts +238 -0
- package/src/__tests__/pattern-detection.test.ts +438 -0
- package/src/__tests__/prompt-builder.test.ts +505 -0
- package/src/active-learning.ts +1227 -0
- package/src/api-client.ts +963 -0
- package/src/auto-session.ts +218 -0
- package/src/cli.ts +166 -0
- package/src/config.ts +285 -0
- package/src/consolidation.ts +314 -0
- package/src/context-assembly.ts +842 -0
- package/src/graph-expansion.ts +234 -0
- package/src/http.ts +265 -0
- package/src/index.ts +8 -0
- package/src/lifecycle-maintenance.ts +120 -0
- package/src/prompt-builder.ts +681 -0
- package/src/remote.ts +227 -0
- package/src/server.ts +3858 -0
- package/src/tui/agents.ts +154 -0
- package/src/tui/docs.ts +650 -0
- package/src/tui/setup.ts +1281 -0
- package/src/tui/theme.ts +114 -0
- package/src/tui/writer.ts +260 -0
package/src/tui/setup.ts
ADDED
|
@@ -0,0 +1,1281 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
symlinkSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import * as p from "@clack/prompts";
|
|
11
|
+
import {
|
|
12
|
+
areSkillsInstalled,
|
|
13
|
+
getConfigPath,
|
|
14
|
+
getLocalConfigPath,
|
|
15
|
+
hasProjectContext,
|
|
16
|
+
isConfigured,
|
|
17
|
+
loadConfig,
|
|
18
|
+
saveLocalConfig,
|
|
19
|
+
} from "../config.js";
|
|
20
|
+
import { type AgentId, detectAgents } from "./agents.js";
|
|
21
|
+
import { runDocsStep } from "./docs.js";
|
|
22
|
+
import { colors, formatPath, messages } from "./theme.js";
|
|
23
|
+
import { getWriteSummary, writeFilesWithProgress } from "./writer.js";
|
|
24
|
+
|
|
25
|
+
export type InstallMode = "local" | "global";
|
|
26
|
+
|
|
27
|
+
export interface SetupOptions {
|
|
28
|
+
force?: boolean;
|
|
29
|
+
apiKey?: string;
|
|
30
|
+
userEmail?: string;
|
|
31
|
+
agents?: AgentId[];
|
|
32
|
+
installMode?: InstallMode;
|
|
33
|
+
workspaceId?: string;
|
|
34
|
+
projectId?: string;
|
|
35
|
+
skipContext?: boolean;
|
|
36
|
+
skipDocs?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Central skills directory for global installation
|
|
40
|
+
const GLOBAL_SKILLS_DIR = join(homedir(), ".agents", "skills");
|
|
41
|
+
|
|
42
|
+
// API base URL
|
|
43
|
+
const API_URL = "https://gethmy.com/api";
|
|
44
|
+
|
|
45
|
+
// Harmony workflow prompt - shared across agents
|
|
46
|
+
const HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
|
|
47
|
+
|
|
48
|
+
Start work on a Harmony card. Card reference: $ARGUMENTS
|
|
49
|
+
|
|
50
|
+
## 1. Find & Fetch Card
|
|
51
|
+
|
|
52
|
+
Parse the reference and fetch the card:
|
|
53
|
+
- \`#42\` or \`42\` → \`harmony_get_card_by_short_id\` with \`shortId: 42\`
|
|
54
|
+
- UUID → \`harmony_get_card\` with \`cardId\`
|
|
55
|
+
- Name/text → \`harmony_search_cards\` with \`query\`
|
|
56
|
+
|
|
57
|
+
## 2. Get Board State
|
|
58
|
+
|
|
59
|
+
Call \`harmony_get_board\` to get columns and labels. From the response:
|
|
60
|
+
- Find the "In Progress" (or "Progress") column ID
|
|
61
|
+
- Find the "agent" label ID
|
|
62
|
+
|
|
63
|
+
## 3. Setup Card for Work
|
|
64
|
+
|
|
65
|
+
Execute these in sequence:
|
|
66
|
+
1. \`harmony_move_card\` → Move to "In Progress" column
|
|
67
|
+
2. \`harmony_add_label_to_card\` → Add "agent" label
|
|
68
|
+
3. \`harmony_start_agent_session\`:
|
|
69
|
+
- \`cardId\`: Card UUID
|
|
70
|
+
- \`agentIdentifier\`: Your agent identifier
|
|
71
|
+
- \`agentName\`: Your agent name
|
|
72
|
+
- \`currentTask\`: "Analyzing card requirements"
|
|
73
|
+
|
|
74
|
+
## 4. Generate Work Prompt
|
|
75
|
+
|
|
76
|
+
Call \`harmony_generate_prompt\` with:
|
|
77
|
+
- \`cardId\` or \`shortId\` (+ \`projectId\` if using shortId)
|
|
78
|
+
- \`variant\`: Select based on task:
|
|
79
|
+
- \`"execute"\` (default) → Clear tasks, bug fixes, well-defined work
|
|
80
|
+
- \`"analysis"\` → Complex features, unclear requirements
|
|
81
|
+
- \`"draft"\` → Medium complexity, want feedback first
|
|
82
|
+
|
|
83
|
+
The generated prompt provides role framing, focus areas, subtasks, linked cards, and suggested outputs.
|
|
84
|
+
|
|
85
|
+
## 5. Display Card Summary
|
|
86
|
+
|
|
87
|
+
Show the user: Card title, short ID, role, priority, labels, due date, description, and subtasks.
|
|
88
|
+
|
|
89
|
+
## 6. Implement Solution
|
|
90
|
+
|
|
91
|
+
Work on the card following the generated prompt's guidance. Update progress at milestones:
|
|
92
|
+
- \`harmony_update_agent_progress\` with \`progressPercent\` (0-100), \`currentTask\`, \`status\`, \`blockers\`
|
|
93
|
+
|
|
94
|
+
**Progress checkpoints:** 20% (exploration), 50% (implementation), 80% (testing), 100% (done)
|
|
95
|
+
|
|
96
|
+
## 7. Complete Work
|
|
97
|
+
|
|
98
|
+
When finished:
|
|
99
|
+
1. \`harmony_end_agent_session\` with \`status: "completed"\`, \`progressPercent: 100\`
|
|
100
|
+
2. \`harmony_move_card\` to "Review" column
|
|
101
|
+
3. Summarize accomplishments
|
|
102
|
+
|
|
103
|
+
If pausing: \`harmony_end_agent_session\` with \`status: "paused"\`
|
|
104
|
+
|
|
105
|
+
## Key Tools Reference
|
|
106
|
+
|
|
107
|
+
**Cards:** \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\`, \`harmony_create_card\`, \`harmony_update_card\`, \`harmony_move_card\`, \`harmony_delete_card\`, \`harmony_assign_card\`
|
|
108
|
+
|
|
109
|
+
**Subtasks:** \`harmony_create_subtask\`, \`harmony_toggle_subtask\`, \`harmony_delete_subtask\`
|
|
110
|
+
|
|
111
|
+
**Labels:** \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\`, \`harmony_create_label\`
|
|
112
|
+
|
|
113
|
+
**Links:** \`harmony_add_link_to_card\`, \`harmony_remove_link_from_card\`, \`harmony_get_card_links\`
|
|
114
|
+
|
|
115
|
+
**Board:** \`harmony_get_board\`, \`harmony_list_projects\`, \`harmony_get_context\`, \`harmony_set_project_context\`
|
|
116
|
+
|
|
117
|
+
**Sessions:** \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\`, \`harmony_get_agent_session\`
|
|
118
|
+
|
|
119
|
+
**AI:** \`harmony_generate_prompt\`, \`harmony_process_command\`
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
// Harmony plan prompt - unified plan creation and execution workflow
|
|
123
|
+
const HARMONY_PLAN_PROMPT = `# Harmony Plan Workflow
|
|
124
|
+
|
|
125
|
+
Create a new plan or work on an existing one. Argument: $ARGUMENTS
|
|
126
|
+
|
|
127
|
+
## Step 1 — Detect Intent
|
|
128
|
+
|
|
129
|
+
Parse \`$ARGUMENTS\` to determine the workflow:
|
|
130
|
+
|
|
131
|
+
- **UUID** (contains dashes, 36 chars) → call \`harmony_get_plan\` with \`planId\` directly → **Step 2A**
|
|
132
|
+
- **\`#N\`** (short ID) → call \`harmony_get_card_by_short_id\` to get the card, then \`harmony_get_plan\` with \`cardId\` → **Step 2A**
|
|
133
|
+
- **Text** (anything else) → call \`harmony_list_plans\` with \`search\` set to the text
|
|
134
|
+
- If **one match** → use it → **Step 2A**
|
|
135
|
+
- If **multiple matches** → list them with title, status, phase, and updated date. Ask the user to pick one using \`AskUserQuestion\` → **Step 2A**
|
|
136
|
+
- If **no matches** → ask user: "No existing plans found for '$ARGUMENTS'. Would you like to create a new plan on this topic?" → **Step 2B**
|
|
137
|
+
- **Empty / vague topic** → **Step 2B** (create new plan)
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Step 2A — Work on Existing Plan
|
|
142
|
+
|
|
143
|
+
### 2A.1 — Analyze & Display
|
|
144
|
+
|
|
145
|
+
Once you have the plan ID, call \`harmony_get_plan\` to fetch the full plan with tasks. Show a structured summary:
|
|
146
|
+
|
|
147
|
+
\\\`\\\`\\\`
|
|
148
|
+
## [Plan Title]
|
|
149
|
+
**Status:** draft/active/archived | **Phase:** plan/execute/verify/done
|
|
150
|
+
**Tasks:** N total (X pending, Y in_progress, Z completed)
|
|
151
|
+
**URL:** https://gethmy.com/plans/{id}
|
|
152
|
+
\\\`\\\`\\\`
|
|
153
|
+
|
|
154
|
+
If plan is in execute phase and tasks already have linked cards, note which tasks have cards and which don't.
|
|
155
|
+
|
|
156
|
+
### 2A.2 — Recall Context
|
|
157
|
+
|
|
158
|
+
Call \`harmony_memory_search\` with the plan title to find related knowledge, patterns, or decisions that may inform execution.
|
|
159
|
+
|
|
160
|
+
### 2A.3 — Present Options
|
|
161
|
+
|
|
162
|
+
Use \`AskUserQuestion\` to offer execution choices. Adapt options based on plan state:
|
|
163
|
+
|
|
164
|
+
#### Default options (plan in \`plan\` phase):
|
|
165
|
+
|
|
166
|
+
**(A) Single card** — Create one card with plan summary + link to plan. Best for simple plans.
|
|
167
|
+
**(B) Multiple cards** — Create one card per task via \`harmony_advance_plan\`. Best for complex plans with independent tasks.
|
|
168
|
+
**(C) Analyze only** — Review the plan and design an implementation approach. Create cards later.
|
|
169
|
+
**(D) Skip** — Do nothing.
|
|
170
|
+
|
|
171
|
+
#### If plan has no tasks:
|
|
172
|
+
Only offer **(A) Single card**, **(C) Analyze only**, or **(D) Skip**.
|
|
173
|
+
|
|
174
|
+
#### If plan is already in \`execute\` phase with existing cards:
|
|
175
|
+
**(A) Work on existing cards** — List cards with short IDs, suggest \`/hmy #N\` to start
|
|
176
|
+
**(B) Create cards for remaining tasks** — Only create cards for tasks without linked cards
|
|
177
|
+
**(C) Analyze progress** — Review what's done vs remaining
|
|
178
|
+
**(D) Skip**
|
|
179
|
+
|
|
180
|
+
### 2A.4 — Execute Choice
|
|
181
|
+
|
|
182
|
+
#### Option A — Single card
|
|
183
|
+
1. Call \`harmony_create_card\` with:
|
|
184
|
+
- \`title\`: Plan title
|
|
185
|
+
- \`description\`: Brief 2-3 sentence summary of the plan + \`\\n\\n[View plan](https://gethmy.com/plans/{planId})\`
|
|
186
|
+
- \`priority\`: based on plan task priorities (use highest)
|
|
187
|
+
2. Call \`harmony_update_plan\` to set \`status: "active"\`, \`workflowPhase: "execute"\`
|
|
188
|
+
|
|
189
|
+
#### Option B — Multiple cards (advance plan)
|
|
190
|
+
1. Call \`harmony_advance_plan\` with:
|
|
191
|
+
- \`planId\`: the plan ID
|
|
192
|
+
- \`phase\`: \`"execute"\`
|
|
193
|
+
- \`summary\`: Brief summary of the plan scope
|
|
194
|
+
2. Display the created cards with their short IDs
|
|
195
|
+
|
|
196
|
+
#### Option C — Analyze only
|
|
197
|
+
1. Present a structured implementation analysis:
|
|
198
|
+
- Suggested order of tasks
|
|
199
|
+
- Dependencies between tasks
|
|
200
|
+
- Key technical considerations from memory search
|
|
201
|
+
- Estimated complexity
|
|
202
|
+
2. Suggest creating cards when ready
|
|
203
|
+
|
|
204
|
+
#### Option D — Skip
|
|
205
|
+
Acknowledge and stop.
|
|
206
|
+
|
|
207
|
+
### 2A.5 — Summary
|
|
208
|
+
|
|
209
|
+
After execution, show:
|
|
210
|
+
- Created card(s) with short IDs
|
|
211
|
+
- Current plan phase
|
|
212
|
+
- Suggest next step: \`/hmy #N\` to start working on a card
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Step 2B — Create New Plan
|
|
217
|
+
|
|
218
|
+
### 2B.1 — Context Gathering
|
|
219
|
+
|
|
220
|
+
Before interviewing, gather existing context:
|
|
221
|
+
|
|
222
|
+
1. Call \`harmony_get_board\` for board state (columns, cards, labels)
|
|
223
|
+
2. Call \`harmony_recall\` with relevant tags to find existing knowledge on the topic
|
|
224
|
+
3. Call \`harmony_memory_search\` with the topic to find related patterns/decisions/lessons
|
|
225
|
+
|
|
226
|
+
Note what you learn — reference it during the interview to ask smarter questions.
|
|
227
|
+
|
|
228
|
+
### 2B.2 — Structured Interview (3-5 questions)
|
|
229
|
+
|
|
230
|
+
Interview the user with **3-5 focused questions** using AskUserQuestion. Adapt based on complexity.
|
|
231
|
+
|
|
232
|
+
#### Core Questions
|
|
233
|
+
1. **Problem & Audience**: What problem are we solving? Who benefits?
|
|
234
|
+
2. **Scope**: What's in v1, what's deferred?
|
|
235
|
+
3. **Technical Constraints**: Any technical preferences, constraints, or existing patterns to follow?
|
|
236
|
+
|
|
237
|
+
#### Adaptive Follow-ups (if needed)
|
|
238
|
+
- Key user flows or interactions?
|
|
239
|
+
- Integration points with existing systems?
|
|
240
|
+
- Performance, security, or accessibility requirements?
|
|
241
|
+
|
|
242
|
+
#### Interview Rules
|
|
243
|
+
- Reference what you found in Step 2B.1 (board state, memories) to ask informed questions
|
|
244
|
+
- Skip questions that aren't relevant — simple features need fewer questions
|
|
245
|
+
- Tell the user when you have enough information to draft
|
|
246
|
+
|
|
247
|
+
### 2B.3 — Plan Document
|
|
248
|
+
|
|
249
|
+
Create a structured markdown document:
|
|
250
|
+
|
|
251
|
+
\\\`\\\`\\\`markdown
|
|
252
|
+
# [Title]
|
|
253
|
+
|
|
254
|
+
## Problem
|
|
255
|
+
[What problem we're solving and why]
|
|
256
|
+
|
|
257
|
+
## Scope
|
|
258
|
+
|
|
259
|
+
### In Scope
|
|
260
|
+
- [Feature/capability 1]
|
|
261
|
+
- [Feature/capability 2]
|
|
262
|
+
|
|
263
|
+
### Out of Scope
|
|
264
|
+
- [Deferred items]
|
|
265
|
+
|
|
266
|
+
## Approach
|
|
267
|
+
[Technical design, architecture decisions, key patterns]
|
|
268
|
+
|
|
269
|
+
### Key Decisions
|
|
270
|
+
| Decision | Choice | Rationale |
|
|
271
|
+
|----------|--------|-----------|
|
|
272
|
+
| ... | ... | ... |
|
|
273
|
+
|
|
274
|
+
## Tasks
|
|
275
|
+
1. **[Task title]** — priority: high
|
|
276
|
+
[Brief description]
|
|
277
|
+
|
|
278
|
+
2. **[Task title]** — priority: medium
|
|
279
|
+
[Brief description]
|
|
280
|
+
|
|
281
|
+
[Continue for all tasks...]
|
|
282
|
+
|
|
283
|
+
## Risks & Mitigations
|
|
284
|
+
| Risk | Mitigation |
|
|
285
|
+
|------|------------|
|
|
286
|
+
| ... | ... |
|
|
287
|
+
|
|
288
|
+
## Success Criteria
|
|
289
|
+
- [ ] [Measurable criterion]
|
|
290
|
+
\\\`\\\`\\\`
|
|
291
|
+
|
|
292
|
+
### 2B.4 — Create Plan
|
|
293
|
+
|
|
294
|
+
Call \`harmony_create_plan\` with:
|
|
295
|
+
- \`title\`: The plan title
|
|
296
|
+
- \`content\`: Full markdown document from Step 2B.3
|
|
297
|
+
- \`source\`: \`"agent"\`
|
|
298
|
+
- \`workflowPhase\`: \`"plan"\`
|
|
299
|
+
- \`tasks\`: Array of tasks from the Tasks section, each with:
|
|
300
|
+
- \`content\`: Task title + brief description
|
|
301
|
+
- \`priority\`: \`"high"\`, \`"medium"\`, or \`"low"\`
|
|
302
|
+
- \`status\`: \`"pending"\`
|
|
303
|
+
|
|
304
|
+
### 2B.5 — User Approval
|
|
305
|
+
|
|
306
|
+
Show the user:
|
|
307
|
+
- Plan URL: \`https://gethmy.com/plans/{id}\`
|
|
308
|
+
- Number of tasks created
|
|
309
|
+
- Brief summary
|
|
310
|
+
|
|
311
|
+
Then ask: **"Ready to execute? I'll create board cards from these tasks and start the execution phase."**
|
|
312
|
+
|
|
313
|
+
Options:
|
|
314
|
+
1. **Yes, advance to Execute** — proceed to Step 2B.6
|
|
315
|
+
2. **Let me review first** — end here, they can advance later from the UI
|
|
316
|
+
3. **Make changes** — iterate on the plan
|
|
317
|
+
|
|
318
|
+
### 2B.6 — Advance to Execute
|
|
319
|
+
|
|
320
|
+
Call \`harmony_advance_plan\` with:
|
|
321
|
+
- \`planId\`: The plan ID from Step 2B.4
|
|
322
|
+
- \`phase\`: \`"execute"\`
|
|
323
|
+
- \`summary\`: A 2-3 sentence summary of the key decisions made during planning
|
|
324
|
+
|
|
325
|
+
Report the result:
|
|
326
|
+
- "Created N cards in 'To Do'. Use \`/hmy #<id>\` to start working on any card."
|
|
327
|
+
- List the created cards with their short IDs
|
|
328
|
+
|
|
329
|
+
## Key Tools Reference
|
|
330
|
+
|
|
331
|
+
**Discovery:** \`harmony_list_plans\`, \`harmony_get_plan\`, \`harmony_get_card_by_short_id\`
|
|
332
|
+
**Context:** \`harmony_get_board\`, \`harmony_recall\`, \`harmony_memory_search\`
|
|
333
|
+
**Planning:** \`harmony_create_plan\`, \`harmony_advance_plan\`, \`harmony_update_plan\`
|
|
334
|
+
**Execution:** \`harmony_create_card\`, \`harmony_update_card\`
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Register MCP server using Claude CLI
|
|
339
|
+
* Returns true if successful, false if CLI unavailable or failed
|
|
340
|
+
*/
|
|
341
|
+
async function registerMcpServer(): Promise<boolean> {
|
|
342
|
+
try {
|
|
343
|
+
const { execSync } = await import("node:child_process");
|
|
344
|
+
// Use the official CLI command to register the MCP server (npx for no global install)
|
|
345
|
+
execSync(
|
|
346
|
+
"claude mcp add --transport stdio harmony -- npx -y @gethmy/mcp@latest serve",
|
|
347
|
+
{
|
|
348
|
+
stdio: "pipe",
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
return true;
|
|
352
|
+
} catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Write MCP server config directly to settings.json (fallback method)
|
|
359
|
+
*/
|
|
360
|
+
async function writeMcpConfigFallback(home: string): Promise<void> {
|
|
361
|
+
const { readFileSync, writeFileSync, mkdirSync, existsSync } = await import(
|
|
362
|
+
"node:fs"
|
|
363
|
+
);
|
|
364
|
+
const settingsPath = join(home, ".claude", "settings.json");
|
|
365
|
+
const settingsDir = dirname(settingsPath);
|
|
366
|
+
|
|
367
|
+
// Ensure directory exists
|
|
368
|
+
if (!existsSync(settingsDir)) {
|
|
369
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Read existing settings or start fresh
|
|
373
|
+
let settings: Record<string, unknown> = {};
|
|
374
|
+
if (existsSync(settingsPath)) {
|
|
375
|
+
try {
|
|
376
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
377
|
+
} catch {
|
|
378
|
+
// Invalid JSON, start fresh
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Merge mcpServers config
|
|
383
|
+
const mcpServers = (settings.mcpServers as Record<string, unknown>) || {};
|
|
384
|
+
mcpServers.harmony = {
|
|
385
|
+
command: "npx",
|
|
386
|
+
args: ["-y", "@gethmy/mcp@latest", "serve"],
|
|
387
|
+
};
|
|
388
|
+
settings.mcpServers = mcpServers;
|
|
389
|
+
|
|
390
|
+
// Write back
|
|
391
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
interface Workspace {
|
|
395
|
+
id: string;
|
|
396
|
+
name: string;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
interface Project {
|
|
400
|
+
id: string;
|
|
401
|
+
name: string;
|
|
402
|
+
description?: string;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Validate API key by testing connectivity
|
|
407
|
+
*/
|
|
408
|
+
async function validateApiKey(
|
|
409
|
+
apiKey: string,
|
|
410
|
+
apiUrl = API_URL,
|
|
411
|
+
): Promise<{ valid: boolean; email?: string; error?: string }> {
|
|
412
|
+
try {
|
|
413
|
+
const response = await fetch(`${apiUrl}/v1/workspaces`, {
|
|
414
|
+
method: "GET",
|
|
415
|
+
headers: {
|
|
416
|
+
"Content-Type": "application/json",
|
|
417
|
+
"X-API-Key": apiKey,
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (!response.ok) {
|
|
422
|
+
const data = await response.json().catch(() => ({}));
|
|
423
|
+
return {
|
|
424
|
+
valid: false,
|
|
425
|
+
error: data.error || `API returned ${response.status}`,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Try to get user info from /me endpoint if available
|
|
430
|
+
const meResponse = await fetch(`${apiUrl}/v1/me`, {
|
|
431
|
+
method: "GET",
|
|
432
|
+
headers: {
|
|
433
|
+
"Content-Type": "application/json",
|
|
434
|
+
"X-API-Key": apiKey,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
if (meResponse.ok) {
|
|
439
|
+
const meData = await meResponse.json();
|
|
440
|
+
return { valid: true, email: meData.user?.email };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return { valid: true };
|
|
444
|
+
} catch (error) {
|
|
445
|
+
return {
|
|
446
|
+
valid: false,
|
|
447
|
+
error: error instanceof Error ? error.message : "Connection failed",
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Fetch workspaces from API
|
|
454
|
+
*/
|
|
455
|
+
async function fetchWorkspaces(apiKey: string): Promise<Workspace[]> {
|
|
456
|
+
const response = await fetch(`${API_URL}/v1/workspaces`, {
|
|
457
|
+
method: "GET",
|
|
458
|
+
headers: {
|
|
459
|
+
"Content-Type": "application/json",
|
|
460
|
+
"X-API-Key": apiKey,
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
if (!response.ok) {
|
|
465
|
+
throw new Error(`Failed to fetch workspaces: ${response.status}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const data = await response.json();
|
|
469
|
+
return data.workspaces || [];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Fetch projects from API
|
|
474
|
+
*/
|
|
475
|
+
async function fetchProjects(
|
|
476
|
+
apiKey: string,
|
|
477
|
+
workspaceId: string,
|
|
478
|
+
): Promise<Project[]> {
|
|
479
|
+
const response = await fetch(
|
|
480
|
+
`${API_URL}/v1/workspaces/${workspaceId}/projects`,
|
|
481
|
+
{
|
|
482
|
+
method: "GET",
|
|
483
|
+
headers: {
|
|
484
|
+
"Content-Type": "application/json",
|
|
485
|
+
"X-API-Key": apiKey,
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
if (!response.ok) {
|
|
491
|
+
throw new Error(`Failed to fetch projects: ${response.status}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const data = await response.json();
|
|
495
|
+
return data.projects || [];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export interface FileToWrite {
|
|
499
|
+
path: string;
|
|
500
|
+
content: string;
|
|
501
|
+
type: "text" | "json" | "toml";
|
|
502
|
+
tomlSection?: string;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export interface SymlinkToCreate {
|
|
506
|
+
target: string;
|
|
507
|
+
link: string;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Generate agent configuration files
|
|
512
|
+
*/
|
|
513
|
+
function getAgentFiles(
|
|
514
|
+
agentId: AgentId,
|
|
515
|
+
cwd: string,
|
|
516
|
+
installMode: InstallMode = "global",
|
|
517
|
+
): { files: FileToWrite[]; symlinks: SymlinkToCreate[] } {
|
|
518
|
+
const home = homedir();
|
|
519
|
+
const files: FileToWrite[] = [];
|
|
520
|
+
const symlinks: SymlinkToCreate[] = [];
|
|
521
|
+
|
|
522
|
+
switch (agentId) {
|
|
523
|
+
case "claude": {
|
|
524
|
+
// Claude Code skill file
|
|
525
|
+
const skillContent = `---
|
|
526
|
+
name: hmy
|
|
527
|
+
description: Start working on a Harmony card. Use when given a card reference like #42, UUID, or card name to implement.
|
|
528
|
+
argument-hint: <card-reference>
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
${HARMONY_WORKFLOW_PROMPT.replace("Your agent identifier", "claude-code").replace("Your agent name", "Claude Code")}
|
|
532
|
+
`;
|
|
533
|
+
|
|
534
|
+
// hmy-plan skill file (unified plan creation + execution)
|
|
535
|
+
const planSkillContent = `---
|
|
536
|
+
name: hmy-plan
|
|
537
|
+
description: Create a new plan or work on an existing one. Use when asked to plan a feature, execute a plan, review a plan, or given a plan reference.
|
|
538
|
+
argument-hint: [plan name, ID, or topic to plan]
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
${HARMONY_PLAN_PROMPT.replace("$ARGUMENTS", "$ARGUMENTS")}
|
|
542
|
+
`;
|
|
543
|
+
|
|
544
|
+
if (installMode === "global") {
|
|
545
|
+
// Global mode: Write skills to ~/.agents/skills/, symlink directories to ~/.claude/skills/
|
|
546
|
+
files.push({
|
|
547
|
+
path: join(GLOBAL_SKILLS_DIR, "hmy", "SKILL.md"),
|
|
548
|
+
content: skillContent,
|
|
549
|
+
type: "text",
|
|
550
|
+
});
|
|
551
|
+
symlinks.push({
|
|
552
|
+
target: join(GLOBAL_SKILLS_DIR, "hmy"),
|
|
553
|
+
link: join(home, ".claude", "skills", "hmy"),
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
files.push({
|
|
557
|
+
path: join(GLOBAL_SKILLS_DIR, "hmy-plan", "SKILL.md"),
|
|
558
|
+
content: planSkillContent,
|
|
559
|
+
type: "text",
|
|
560
|
+
});
|
|
561
|
+
symlinks.push({
|
|
562
|
+
target: join(GLOBAL_SKILLS_DIR, "hmy-plan"),
|
|
563
|
+
link: join(home, ".claude", "skills", "hmy-plan"),
|
|
564
|
+
});
|
|
565
|
+
} else {
|
|
566
|
+
// Local mode: Write skills directly to project directory
|
|
567
|
+
files.push({
|
|
568
|
+
path: join(cwd, ".claude", "skills", "hmy", "SKILL.md"),
|
|
569
|
+
content: skillContent,
|
|
570
|
+
type: "text",
|
|
571
|
+
});
|
|
572
|
+
files.push({
|
|
573
|
+
path: join(cwd, ".claude", "skills", "hmy-plan", "SKILL.md"),
|
|
574
|
+
content: planSkillContent,
|
|
575
|
+
type: "text",
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Note: MCP server registration is handled separately via `claude mcp add` CLI
|
|
580
|
+
// in runSetup() after file writing, with fallback to settings.json if CLI unavailable
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
case "codex": {
|
|
585
|
+
// AGENTS.md in project root (always local - project context)
|
|
586
|
+
const agentsContent = `# Harmony Integration
|
|
587
|
+
|
|
588
|
+
This project uses Harmony for task management. When working on tasks:
|
|
589
|
+
|
|
590
|
+
## Starting Work on a Card
|
|
591
|
+
|
|
592
|
+
When given a card reference (e.g., #42 or a card name), follow this workflow:
|
|
593
|
+
|
|
594
|
+
1. Use \`harmony_get_card_by_short_id\` or \`harmony_search_cards\` to find the card
|
|
595
|
+
2. Move the card to "In Progress" using \`harmony_move_card\`
|
|
596
|
+
3. Add the "agent" label using \`harmony_add_label_to_card\`
|
|
597
|
+
4. Start a session with \`harmony_start_agent_session\` (agentIdentifier: "codex", agentName: "OpenAI Codex")
|
|
598
|
+
5. Show the card details to the user
|
|
599
|
+
6. Use \`harmony_generate_prompt\` to get guidance, then implement the solution
|
|
600
|
+
7. Update progress periodically with \`harmony_update_agent_progress\`
|
|
601
|
+
8. When done, call \`harmony_end_agent_session\` and move to "Review"
|
|
602
|
+
|
|
603
|
+
## Available Harmony Tools
|
|
604
|
+
|
|
605
|
+
- \`harmony_get_card\`, \`harmony_get_card_by_short_id\`, \`harmony_search_cards\` - Find cards
|
|
606
|
+
- \`harmony_move_card\` - Move cards between columns
|
|
607
|
+
- \`harmony_add_label_to_card\`, \`harmony_remove_label_from_card\` - Manage labels
|
|
608
|
+
- \`harmony_start_agent_session\`, \`harmony_update_agent_progress\`, \`harmony_end_agent_session\` - Track work
|
|
609
|
+
- \`harmony_get_board\` - Get board state
|
|
610
|
+
- \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
|
|
611
|
+
`;
|
|
612
|
+
files.push({
|
|
613
|
+
path: join(cwd, "AGENTS.md"),
|
|
614
|
+
content: agentsContent,
|
|
615
|
+
type: "text",
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// Codex prompt file
|
|
619
|
+
const promptContent = `---
|
|
620
|
+
name: hmy
|
|
621
|
+
description: Start working on a Harmony card
|
|
622
|
+
arguments:
|
|
623
|
+
- name: card
|
|
624
|
+
description: Card reference (#42, UUID, or name)
|
|
625
|
+
required: true
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "{{card}}").replace("Your agent identifier", "codex").replace("Your agent name", "OpenAI Codex")}
|
|
629
|
+
`;
|
|
630
|
+
|
|
631
|
+
if (installMode === "global") {
|
|
632
|
+
// Global mode: Write prompt to central location, symlink to ~/.codex/prompts/
|
|
633
|
+
files.push({
|
|
634
|
+
path: join(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
|
|
635
|
+
content: promptContent,
|
|
636
|
+
type: "text",
|
|
637
|
+
});
|
|
638
|
+
symlinks.push({
|
|
639
|
+
target: join(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
|
|
640
|
+
link: join(home, ".codex", "prompts", "hmy.md"),
|
|
641
|
+
});
|
|
642
|
+
} else {
|
|
643
|
+
// Local mode: Write prompt directly to ~/.codex/prompts/
|
|
644
|
+
files.push({
|
|
645
|
+
path: join(home, ".codex", "prompts", "hmy.md"),
|
|
646
|
+
content: promptContent,
|
|
647
|
+
type: "text",
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Codex config.toml (always global)
|
|
652
|
+
const tomlContent = `
|
|
653
|
+
# Harmony MCP Server
|
|
654
|
+
[mcp_servers.harmony]
|
|
655
|
+
command = "npx"
|
|
656
|
+
args = ["-y", "@gethmy/mcp@latest", "serve"]
|
|
657
|
+
`;
|
|
658
|
+
files.push({
|
|
659
|
+
path: join(home, ".codex", "config.toml"),
|
|
660
|
+
content: tomlContent,
|
|
661
|
+
type: "toml",
|
|
662
|
+
tomlSection: "mcp_servers.harmony",
|
|
663
|
+
});
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
case "cursor": {
|
|
668
|
+
// Cursor MCP config (always project-level)
|
|
669
|
+
files.push({
|
|
670
|
+
path: join(cwd, ".cursor", "mcp.json"),
|
|
671
|
+
content: JSON.stringify(
|
|
672
|
+
{
|
|
673
|
+
mcpServers: {
|
|
674
|
+
harmony: {
|
|
675
|
+
command: "npx",
|
|
676
|
+
args: ["-y", "@gethmy/mcp@latest", "serve"],
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
null,
|
|
681
|
+
2,
|
|
682
|
+
),
|
|
683
|
+
type: "json",
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// Cursor rule file
|
|
687
|
+
const ruleContent = `---
|
|
688
|
+
description: Harmony card workflow rule
|
|
689
|
+
globs:
|
|
690
|
+
- "**/*"
|
|
691
|
+
alwaysApply: false
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
# Harmony Integration
|
|
695
|
+
|
|
696
|
+
When the user asks you to work on a Harmony card (references like #42, card names, or UUIDs):
|
|
697
|
+
|
|
698
|
+
${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "cursor").replace("Your agent name", "Cursor AI")}
|
|
699
|
+
`;
|
|
700
|
+
|
|
701
|
+
if (installMode === "global") {
|
|
702
|
+
// Global mode: Write rule to central location, symlink to ~/.cursor/rules/
|
|
703
|
+
files.push({
|
|
704
|
+
path: join(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
|
|
705
|
+
content: ruleContent,
|
|
706
|
+
type: "text",
|
|
707
|
+
});
|
|
708
|
+
symlinks.push({
|
|
709
|
+
target: join(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
|
|
710
|
+
link: join(home, ".cursor", "rules", "harmony.mdc"),
|
|
711
|
+
});
|
|
712
|
+
} else {
|
|
713
|
+
// Local mode: Write rule directly to project directory
|
|
714
|
+
files.push({
|
|
715
|
+
path: join(cwd, ".cursor", "rules", "harmony.mdc"),
|
|
716
|
+
content: ruleContent,
|
|
717
|
+
type: "text",
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
case "windsurf": {
|
|
724
|
+
// Windsurf global MCP config (always global)
|
|
725
|
+
files.push({
|
|
726
|
+
path: join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
727
|
+
content: JSON.stringify(
|
|
728
|
+
{
|
|
729
|
+
mcpServers: {
|
|
730
|
+
harmony: {
|
|
731
|
+
command: "npx",
|
|
732
|
+
args: ["-y", "@gethmy/mcp@latest", "serve"],
|
|
733
|
+
disabled: false,
|
|
734
|
+
alwaysAllow: [],
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
null,
|
|
739
|
+
2,
|
|
740
|
+
),
|
|
741
|
+
type: "json",
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// Windsurf rule file
|
|
745
|
+
const ruleContent = `---
|
|
746
|
+
trigger: model_decision
|
|
747
|
+
description: Activate when user asks to work on a Harmony card (references like #42, card names, or task management)
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
# Harmony Card Workflow
|
|
751
|
+
|
|
752
|
+
When working on a Harmony card:
|
|
753
|
+
|
|
754
|
+
${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Your agent identifier", "windsurf").replace("Your agent name", "Windsurf AI")}
|
|
755
|
+
`;
|
|
756
|
+
|
|
757
|
+
if (installMode === "global") {
|
|
758
|
+
// Global mode: Write rule to central location, symlink to ~/.codeium/windsurf/rules/
|
|
759
|
+
files.push({
|
|
760
|
+
path: join(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
|
|
761
|
+
content: ruleContent,
|
|
762
|
+
type: "text",
|
|
763
|
+
});
|
|
764
|
+
symlinks.push({
|
|
765
|
+
target: join(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
|
|
766
|
+
link: join(home, ".codeium", "windsurf", "rules", "harmony.md"),
|
|
767
|
+
});
|
|
768
|
+
} else {
|
|
769
|
+
// Local mode: Write rule directly to project directory
|
|
770
|
+
files.push({
|
|
771
|
+
path: join(cwd, ".windsurf", "rules", "harmony.md"),
|
|
772
|
+
content: ruleContent,
|
|
773
|
+
type: "text",
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return { files, symlinks };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Main setup wizard with smart detection
|
|
785
|
+
*/
|
|
786
|
+
export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
787
|
+
const cwd = process.cwd();
|
|
788
|
+
const home = homedir();
|
|
789
|
+
|
|
790
|
+
console.clear();
|
|
791
|
+
console.log(messages.header());
|
|
792
|
+
|
|
793
|
+
// Check existing configuration
|
|
794
|
+
const existingConfig = loadConfig();
|
|
795
|
+
const alreadyConfigured = isConfigured();
|
|
796
|
+
const skillsStatus = areSkillsInstalled(cwd);
|
|
797
|
+
const hasContext = hasProjectContext(cwd);
|
|
798
|
+
|
|
799
|
+
// Track what we need to do
|
|
800
|
+
let needsApiKey = !alreadyConfigured;
|
|
801
|
+
let needsSkills = !skillsStatus.installed || options.force;
|
|
802
|
+
let needsContext = !hasContext && !options.skipContext;
|
|
803
|
+
|
|
804
|
+
// If workspace/project provided via flags, we'll set context
|
|
805
|
+
if (options.workspaceId || options.projectId) {
|
|
806
|
+
needsContext = true;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Step 1: API Key
|
|
810
|
+
let apiKey = options.apiKey || existingConfig.apiKey;
|
|
811
|
+
let userEmail: string | undefined =
|
|
812
|
+
options.userEmail || existingConfig.userEmail || undefined;
|
|
813
|
+
|
|
814
|
+
if (needsApiKey || !apiKey || !apiKey.startsWith("hmy_")) {
|
|
815
|
+
const keyInput = await p.text({
|
|
816
|
+
message: "Enter your Harmony API key",
|
|
817
|
+
placeholder: "hmy_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
818
|
+
validate: (value) => {
|
|
819
|
+
if (!value) return "API key is required";
|
|
820
|
+
if (!value.startsWith("hmy_")) return 'API key must start with "hmy_"';
|
|
821
|
+
if (value.length < 20) return "API key is too short";
|
|
822
|
+
return undefined;
|
|
823
|
+
},
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
if (p.isCancel(keyInput)) {
|
|
827
|
+
p.cancel("Setup cancelled");
|
|
828
|
+
process.exit(0);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
apiKey = keyInput as string;
|
|
832
|
+
needsApiKey = true;
|
|
833
|
+
} else {
|
|
834
|
+
p.log.success(`Using existing API key: ${apiKey.slice(0, 8)}...`);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Validate API key
|
|
838
|
+
const spinner = p.spinner();
|
|
839
|
+
spinner.start("Validating API key...");
|
|
840
|
+
|
|
841
|
+
const validation = await validateApiKey(apiKey);
|
|
842
|
+
|
|
843
|
+
if (!validation.valid) {
|
|
844
|
+
spinner.stop(colors.error("API key validation failed"));
|
|
845
|
+
p.log.error(validation.error || "Could not connect to Harmony API");
|
|
846
|
+
p.log.info("Get an API key at: https://gethmy.com/user/keys");
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (!userEmail) {
|
|
851
|
+
userEmail = validation.email;
|
|
852
|
+
}
|
|
853
|
+
spinner.stop(
|
|
854
|
+
colors.success(
|
|
855
|
+
userEmail ? `Connected as ${userEmail}` : "API key validated",
|
|
856
|
+
),
|
|
857
|
+
);
|
|
858
|
+
|
|
859
|
+
// Step 2: Check if skills are already installed
|
|
860
|
+
let selectedAgents: AgentId[] = [];
|
|
861
|
+
let installMode: InstallMode = options.installMode || "global";
|
|
862
|
+
|
|
863
|
+
if (skillsStatus.installed && !options.force) {
|
|
864
|
+
p.log.success(`Skills already installed (${skillsStatus.location})`);
|
|
865
|
+
|
|
866
|
+
const reinstall = await p.confirm({
|
|
867
|
+
message: "Reinstall skills?",
|
|
868
|
+
initialValue: false,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
if (p.isCancel(reinstall)) {
|
|
872
|
+
p.cancel("Setup cancelled");
|
|
873
|
+
process.exit(0);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
needsSkills = reinstall as boolean;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (needsSkills) {
|
|
880
|
+
// Detect and select agents
|
|
881
|
+
const detectedAgents = detectAgents(cwd);
|
|
882
|
+
|
|
883
|
+
if (options.agents && options.agents.length > 0) {
|
|
884
|
+
selectedAgents = options.agents;
|
|
885
|
+
} else {
|
|
886
|
+
const agentOptions = detectedAgents.map((agent) => ({
|
|
887
|
+
value: agent.id,
|
|
888
|
+
label: agent.name,
|
|
889
|
+
hint: agent.detected
|
|
890
|
+
? colors.success(`${agent.description} (detected)`)
|
|
891
|
+
: colors.dim(`${agent.description}`),
|
|
892
|
+
}));
|
|
893
|
+
|
|
894
|
+
const agentSelection = await p.multiselect({
|
|
895
|
+
message: "Select agents to configure",
|
|
896
|
+
options: agentOptions,
|
|
897
|
+
initialValues: detectedAgents
|
|
898
|
+
.filter((a) => a.detected)
|
|
899
|
+
.map((a) => a.id),
|
|
900
|
+
required: true,
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
if (p.isCancel(agentSelection)) {
|
|
904
|
+
p.cancel("Setup cancelled");
|
|
905
|
+
process.exit(0);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
selectedAgents = agentSelection as AgentId[];
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (selectedAgents.length === 0) {
|
|
912
|
+
p.log.warning("No agents selected. Skipping skills installation.");
|
|
913
|
+
needsSkills = false;
|
|
914
|
+
} else if (!options.installMode) {
|
|
915
|
+
// Select install mode
|
|
916
|
+
const modeSelection = await p.select({
|
|
917
|
+
message: "Where should Harmony skills be installed?",
|
|
918
|
+
options: [
|
|
919
|
+
{
|
|
920
|
+
value: "global",
|
|
921
|
+
label: "Global (shared) - Recommended",
|
|
922
|
+
hint: "Skills in ~/.agents/skills/, available in all projects",
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
value: "local",
|
|
926
|
+
label: "Local (project directory)",
|
|
927
|
+
hint: "Skills in .claude/skills/, committed to git",
|
|
928
|
+
},
|
|
929
|
+
],
|
|
930
|
+
initialValue: "global",
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
if (p.isCancel(modeSelection)) {
|
|
934
|
+
p.cancel("Setup cancelled");
|
|
935
|
+
process.exit(0);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
installMode = modeSelection as InstallMode;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Step 3: Workspace and Project Selection
|
|
943
|
+
let selectedWorkspaceId = options.workspaceId;
|
|
944
|
+
let selectedProjectId = options.projectId;
|
|
945
|
+
let selectedWorkspaceName: string | undefined;
|
|
946
|
+
let selectedProjectName: string | undefined;
|
|
947
|
+
|
|
948
|
+
if (needsContext && !options.skipContext) {
|
|
949
|
+
// Fetch workspaces
|
|
950
|
+
spinner.start("Fetching workspaces...");
|
|
951
|
+
let workspaces: Workspace[] = [];
|
|
952
|
+
|
|
953
|
+
try {
|
|
954
|
+
workspaces = await fetchWorkspaces(apiKey);
|
|
955
|
+
spinner.stop(colors.success(`Found ${workspaces.length} workspace(s)`));
|
|
956
|
+
} catch (_error) {
|
|
957
|
+
spinner.stop(colors.warning("Could not fetch workspaces"));
|
|
958
|
+
p.log.warning(
|
|
959
|
+
"Skipping workspace/project selection. You can set this later.",
|
|
960
|
+
);
|
|
961
|
+
needsContext = false;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (needsContext && workspaces.length > 0) {
|
|
965
|
+
// Select workspace
|
|
966
|
+
if (!selectedWorkspaceId) {
|
|
967
|
+
const workspaceOptions = workspaces.map((ws) => ({
|
|
968
|
+
value: ws.id,
|
|
969
|
+
label: ws.name,
|
|
970
|
+
}));
|
|
971
|
+
|
|
972
|
+
const workspaceSelection = await p.select({
|
|
973
|
+
message: "Select workspace",
|
|
974
|
+
options: workspaceOptions,
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
if (p.isCancel(workspaceSelection)) {
|
|
978
|
+
p.cancel("Setup cancelled");
|
|
979
|
+
process.exit(0);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
selectedWorkspaceId = workspaceSelection as string;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
selectedWorkspaceName = workspaces.find(
|
|
986
|
+
(w) => w.id === selectedWorkspaceId,
|
|
987
|
+
)?.name;
|
|
988
|
+
|
|
989
|
+
// Fetch and select project
|
|
990
|
+
spinner.start("Fetching projects...");
|
|
991
|
+
let projects: Project[] = [];
|
|
992
|
+
|
|
993
|
+
try {
|
|
994
|
+
projects = await fetchProjects(apiKey, selectedWorkspaceId);
|
|
995
|
+
spinner.stop(colors.success(`Found ${projects.length} project(s)`));
|
|
996
|
+
} catch (_error) {
|
|
997
|
+
spinner.stop(colors.warning("Could not fetch projects"));
|
|
998
|
+
p.log.warning("Skipping project selection. You can set this later.");
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (projects.length > 0 && !selectedProjectId) {
|
|
1002
|
+
const projectOptions = projects.map((proj) => ({
|
|
1003
|
+
value: proj.id,
|
|
1004
|
+
label: proj.name,
|
|
1005
|
+
hint: proj.description
|
|
1006
|
+
? colors.dim(proj.description.slice(0, 50))
|
|
1007
|
+
: undefined,
|
|
1008
|
+
}));
|
|
1009
|
+
|
|
1010
|
+
const projectSelection = await p.select({
|
|
1011
|
+
message: "Select project",
|
|
1012
|
+
options: projectOptions,
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
if (p.isCancel(projectSelection)) {
|
|
1016
|
+
p.cancel("Setup cancelled");
|
|
1017
|
+
process.exit(0);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
selectedProjectId = projectSelection as string;
|
|
1021
|
+
selectedProjectName = projects.find(
|
|
1022
|
+
(p) => p.id === selectedProjectId,
|
|
1023
|
+
)?.name;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Step 4: Collect all files and symlinks to create
|
|
1029
|
+
const allFiles: FileToWrite[] = [];
|
|
1030
|
+
const allSymlinks: SymlinkToCreate[] = [];
|
|
1031
|
+
|
|
1032
|
+
// Always save global config if we have an API key change
|
|
1033
|
+
if (needsApiKey || !alreadyConfigured) {
|
|
1034
|
+
allFiles.push({
|
|
1035
|
+
path: getConfigPath(),
|
|
1036
|
+
content: JSON.stringify(
|
|
1037
|
+
{
|
|
1038
|
+
apiKey,
|
|
1039
|
+
apiUrl: API_URL,
|
|
1040
|
+
userEmail: userEmail || null,
|
|
1041
|
+
activeWorkspaceId: null,
|
|
1042
|
+
activeProjectId: null,
|
|
1043
|
+
},
|
|
1044
|
+
null,
|
|
1045
|
+
2,
|
|
1046
|
+
),
|
|
1047
|
+
type: "text", // Use text to avoid merging
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Project docs scaffold / verification
|
|
1052
|
+
if (!options.skipDocs) {
|
|
1053
|
+
const docsResult = await runDocsStep(cwd);
|
|
1054
|
+
if (!docsResult.skipped) {
|
|
1055
|
+
for (const file of docsResult.files) {
|
|
1056
|
+
allFiles.push(file);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Agent-specific files
|
|
1062
|
+
if (needsSkills && selectedAgents.length > 0) {
|
|
1063
|
+
for (const agentId of selectedAgents) {
|
|
1064
|
+
const { files, symlinks } = getAgentFiles(agentId, cwd, installMode);
|
|
1065
|
+
allFiles.push(...files);
|
|
1066
|
+
allSymlinks.push(...symlinks);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Step 5: Show summary
|
|
1071
|
+
const detectedAgents = detectAgents(cwd);
|
|
1072
|
+
|
|
1073
|
+
console.log("");
|
|
1074
|
+
p.log.step("Summary");
|
|
1075
|
+
console.log("");
|
|
1076
|
+
|
|
1077
|
+
console.log(` ${colors.bold("API Key:")} ${apiKey.slice(0, 8)}...`);
|
|
1078
|
+
if (userEmail) {
|
|
1079
|
+
console.log(` ${colors.bold("Email:")} ${userEmail}`);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (needsSkills && selectedAgents.length > 0) {
|
|
1083
|
+
console.log(
|
|
1084
|
+
` ${colors.bold("Agents:")} ${selectedAgents.map((a) => detectedAgents.find((d) => d.id === a)?.name).join(", ")}`,
|
|
1085
|
+
);
|
|
1086
|
+
console.log(
|
|
1087
|
+
` ${colors.bold("Install:")} ${installMode === "global" ? "Global (~/.agents/skills/)" : "Local (project)"}`,
|
|
1088
|
+
);
|
|
1089
|
+
} else {
|
|
1090
|
+
console.log(
|
|
1091
|
+
` ${colors.bold("Skills:")} Already installed (${skillsStatus.location || "none"})`,
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (selectedWorkspaceId) {
|
|
1096
|
+
console.log(
|
|
1097
|
+
` ${colors.bold("Workspace:")} ${selectedWorkspaceName || selectedWorkspaceId}`,
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
if (selectedProjectId) {
|
|
1101
|
+
console.log(
|
|
1102
|
+
` ${colors.bold("Project:")} ${selectedProjectName || selectedProjectId}`,
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (allFiles.length > 0) {
|
|
1107
|
+
const summary = getWriteSummary(allFiles, { force: options.force });
|
|
1108
|
+
|
|
1109
|
+
if (summary.toCreate.length > 0) {
|
|
1110
|
+
console.log("");
|
|
1111
|
+
console.log(` ${colors.success("Files to create:")}`);
|
|
1112
|
+
for (const path of summary.toCreate) {
|
|
1113
|
+
console.log(` ${colors.dim("\u2022")} ${path}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (allSymlinks.length > 0) {
|
|
1118
|
+
console.log("");
|
|
1119
|
+
console.log(` ${colors.info("Symlinks to create:")}`);
|
|
1120
|
+
for (const symlink of allSymlinks) {
|
|
1121
|
+
console.log(
|
|
1122
|
+
` ${colors.dim("\u{1F517}")} ${formatPath(symlink.link, home)} → ${formatPath(symlink.target, home)}`,
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (summary.toUpdate.length > 0) {
|
|
1128
|
+
console.log("");
|
|
1129
|
+
console.log(` ${colors.info("Files to update:")}`);
|
|
1130
|
+
for (const path of summary.toUpdate) {
|
|
1131
|
+
console.log(` ${colors.dim("\u2022")} ${path}`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (summary.toSkip.length > 0 && !options.force) {
|
|
1136
|
+
console.log("");
|
|
1137
|
+
console.log(` ${colors.dim("Files to skip (already exist):")}`);
|
|
1138
|
+
for (const path of summary.toSkip) {
|
|
1139
|
+
console.log(` ${colors.dim("\u2022")} ${path}`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
console.log("");
|
|
1145
|
+
|
|
1146
|
+
// Step 6: Confirm and execute
|
|
1147
|
+
const shouldProceed = await p.confirm({
|
|
1148
|
+
message: "Proceed with setup?",
|
|
1149
|
+
initialValue: true,
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
if (p.isCancel(shouldProceed) || !shouldProceed) {
|
|
1153
|
+
p.cancel("Setup cancelled");
|
|
1154
|
+
process.exit(0);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
console.log("");
|
|
1158
|
+
|
|
1159
|
+
// Step 7: Write files
|
|
1160
|
+
if (allFiles.length > 0) {
|
|
1161
|
+
await writeFilesWithProgress(allFiles, { force: options.force });
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Step 8: Create symlinks
|
|
1165
|
+
if (allSymlinks.length > 0) {
|
|
1166
|
+
for (const symlink of allSymlinks) {
|
|
1167
|
+
try {
|
|
1168
|
+
// Ensure parent directory exists
|
|
1169
|
+
const linkDir = dirname(symlink.link);
|
|
1170
|
+
if (!existsSync(linkDir)) {
|
|
1171
|
+
mkdirSync(linkDir, { recursive: true });
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Check if link already exists
|
|
1175
|
+
let linkExists = false;
|
|
1176
|
+
try {
|
|
1177
|
+
lstatSync(symlink.link);
|
|
1178
|
+
linkExists = true;
|
|
1179
|
+
} catch {
|
|
1180
|
+
// Link doesn't exist
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (linkExists) {
|
|
1184
|
+
if (options.force) {
|
|
1185
|
+
unlinkSync(symlink.link);
|
|
1186
|
+
} else {
|
|
1187
|
+
// Skip existing symlink
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
symlinkSync(symlink.target, symlink.link);
|
|
1193
|
+
} catch {
|
|
1194
|
+
p.log.warning(`Failed to create symlink: ${symlink.link}`);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Step 8b: Register MCP server for Claude (project-local scope)
|
|
1200
|
+
const claudeDetected = detectAgents(cwd).some(
|
|
1201
|
+
(a) => a.id === "claude" && a.detected,
|
|
1202
|
+
);
|
|
1203
|
+
if (claudeDetected || selectedAgents.includes("claude")) {
|
|
1204
|
+
const mcpRegistered = await registerMcpServer();
|
|
1205
|
+
if (mcpRegistered) {
|
|
1206
|
+
console.log(
|
|
1207
|
+
` ${colors.success("\u2713")} ${colors.dim("MCP server registered via claude CLI")}`,
|
|
1208
|
+
);
|
|
1209
|
+
} else {
|
|
1210
|
+
// Fallback: write directly to settings.json
|
|
1211
|
+
try {
|
|
1212
|
+
await writeMcpConfigFallback(home);
|
|
1213
|
+
console.log(
|
|
1214
|
+
` ${colors.success("\u2713")} ${colors.dim(formatPath(join(home, ".claude", "settings.json"), home))} ${colors.dim("(updated)")}`,
|
|
1215
|
+
);
|
|
1216
|
+
} catch {
|
|
1217
|
+
p.log.warning(
|
|
1218
|
+
"Could not register MCP server. Run manually: claude mcp add --transport stdio harmony -- npx -y @gethmy/mcp@latest serve",
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Step 9: Save local context
|
|
1225
|
+
if (selectedWorkspaceId || selectedProjectId) {
|
|
1226
|
+
const localConfig: { workspaceId?: string; projectId?: string } = {};
|
|
1227
|
+
if (selectedWorkspaceId) localConfig.workspaceId = selectedWorkspaceId;
|
|
1228
|
+
if (selectedProjectId) localConfig.projectId = selectedProjectId;
|
|
1229
|
+
saveLocalConfig(localConfig, cwd);
|
|
1230
|
+
console.log(
|
|
1231
|
+
` ${colors.success("\u2713")} ${colors.dim(formatPath(getLocalConfigPath(cwd), home))} ${colors.dim("(created)")}`,
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Step 10: Show completion message
|
|
1236
|
+
console.log("");
|
|
1237
|
+
p.outro(colors.success("Setup complete!"));
|
|
1238
|
+
|
|
1239
|
+
console.log("");
|
|
1240
|
+
console.log(` ${colors.bold("Configuration:")}`);
|
|
1241
|
+
console.log(` API key: ${formatPath(getConfigPath(), home)}`);
|
|
1242
|
+
if (needsSkills && selectedAgents.length > 0) {
|
|
1243
|
+
console.log(
|
|
1244
|
+
` Skills: ${installMode === "global" ? "~/.agents/skills/ (global)" : ".claude/skills/ (local)"}`,
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
if (selectedWorkspaceId || selectedProjectId) {
|
|
1248
|
+
console.log(` Context: ${formatPath(getLocalConfigPath(cwd), home)}`);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
console.log("");
|
|
1252
|
+
console.log(` ${colors.bold("Usage:")}`);
|
|
1253
|
+
|
|
1254
|
+
if (!needsSkills || selectedAgents.includes("claude")) {
|
|
1255
|
+
console.log(
|
|
1256
|
+
` ${colors.brand("Claude Code:")} ${colors.highlight("/hmy #42")} or ${colors.highlight("/hmy-plan")} ${colors.dim("(create or execute plans)")}`,
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (selectedAgents.includes("codex")) {
|
|
1261
|
+
console.log(
|
|
1262
|
+
` ${colors.brand("Codex:")} ${colors.highlight("/prompts:hmy #42")}`,
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
if (
|
|
1267
|
+
selectedAgents.includes("cursor") ||
|
|
1268
|
+
selectedAgents.includes("windsurf")
|
|
1269
|
+
) {
|
|
1270
|
+
console.log(
|
|
1271
|
+
` ${colors.brand("Cursor/Windsurf:")} MCP tools available automatically`,
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
console.log("");
|
|
1276
|
+
console.log(` ${colors.dim("Add to new project: npx @gethmy/mcp setup")}`);
|
|
1277
|
+
console.log(
|
|
1278
|
+
` ${colors.dim("Need help? Visit https://gethmy.com/docs/mcp")}`,
|
|
1279
|
+
);
|
|
1280
|
+
console.log("");
|
|
1281
|
+
}
|