@gethmy/mcp 2.2.1 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -7
- package/dist/cli.js +405 -143
- package/dist/http.js +6 -4
- package/dist/index.js +164 -85
- package/dist/lib/auto-session.js +1 -1
- package/dist/lib/cli.js +9 -0
- package/dist/lib/onboard.js +36 -0
- package/dist/lib/server.js +129 -73
- package/dist/lib/skills.js +1 -1
- package/dist/lib/tui/setup.js +212 -59
- package/dist/remote.js +7132 -10614
- package/package.json +2 -1
- package/src/auto-session.ts +1 -1
- package/src/cli.ts +9 -0
- package/src/onboard.ts +93 -0
- package/src/server.ts +153 -102
- package/src/skills.ts +1 -1
- package/src/tui/setup.ts +249 -67
package/dist/lib/server.js
CHANGED
|
@@ -4,13 +4,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { detectContradictions, extractLearnings, extractMidSessionLearnings, } from "./active-learning.js";
|
|
7
|
-
import { getClient,
|
|
7
|
+
import { getClient, resetClient, signupUser, } from "./api-client.js";
|
|
8
8
|
import { AUTO_START_TRIGGERS, destroyAutoSession, initAutoSession, markExplicit, shutdownAllSessions, trackActivity, untrack, } from "./auto-session.js";
|
|
9
9
|
import { getActiveProjectId, getActiveWorkspaceId, getApiUrl, getMemoryDir, getUserEmail, isConfigured, saveConfig, setActiveProject, setActiveWorkspace, } from "./config.js";
|
|
10
10
|
import { consolidateMemories } from "./consolidation.js";
|
|
11
11
|
import { assembleContext, cacheManifest, computeRelevanceScore, getCachedManifest, mapToContextEntity, recordContextFeedback, trackSessionAssembly, } from "./context-assembly.js";
|
|
12
12
|
import { autoExpandGraph } from "./graph-expansion.js";
|
|
13
13
|
import { runLifecycleMaintenance } from "./lifecycle-maintenance.js";
|
|
14
|
+
import { onboardNewUser } from "./onboard.js";
|
|
14
15
|
const memorySessions = new Map();
|
|
15
16
|
function initMemorySession(cardId, agentIdentifier, agentName) {
|
|
16
17
|
memorySessions.set(cardId, {
|
|
@@ -168,18 +169,22 @@ const TOOLS = {
|
|
|
168
169
|
},
|
|
169
170
|
},
|
|
170
171
|
harmony_move_card: {
|
|
171
|
-
description: "Move a card to a different column or position",
|
|
172
|
+
description: "Move a card to a different column or position. Provide either columnId (UUID) or columnName (e.g. 'Review', 'Done').",
|
|
172
173
|
inputSchema: {
|
|
173
174
|
type: "object",
|
|
174
175
|
properties: {
|
|
175
176
|
cardId: { type: "string", description: "Card ID to move" },
|
|
176
|
-
columnId: { type: "string", description: "Target column ID" },
|
|
177
|
+
columnId: { type: "string", description: "Target column ID (UUID)" },
|
|
178
|
+
columnName: {
|
|
179
|
+
type: "string",
|
|
180
|
+
description: "Target column name (e.g. 'To Do', 'In Progress', 'Review', 'Done'). Used if columnId is not provided.",
|
|
181
|
+
},
|
|
177
182
|
position: {
|
|
178
183
|
type: "number",
|
|
179
184
|
description: "Position in column (0-indexed)",
|
|
180
185
|
},
|
|
181
186
|
},
|
|
182
|
-
required: ["cardId"
|
|
187
|
+
required: ["cardId"],
|
|
183
188
|
},
|
|
184
189
|
},
|
|
185
190
|
harmony_archive_card: {
|
|
@@ -312,14 +317,21 @@ const TOOLS = {
|
|
|
312
317
|
},
|
|
313
318
|
},
|
|
314
319
|
harmony_add_label_to_card: {
|
|
315
|
-
description: "Add a label to a card",
|
|
320
|
+
description: "Add a label to a card. Provide labelId directly, or labelName to look up (or auto-create) the label by name.",
|
|
316
321
|
inputSchema: {
|
|
317
322
|
type: "object",
|
|
318
323
|
properties: {
|
|
319
324
|
cardId: { type: "string" },
|
|
320
|
-
labelId: {
|
|
325
|
+
labelId: {
|
|
326
|
+
type: "string",
|
|
327
|
+
description: "Label ID (optional if labelName provided)",
|
|
328
|
+
},
|
|
329
|
+
labelName: {
|
|
330
|
+
type: "string",
|
|
331
|
+
description: "Label name — will look up or create if not found",
|
|
332
|
+
},
|
|
321
333
|
},
|
|
322
|
-
required: ["cardId"
|
|
334
|
+
required: ["cardId"],
|
|
323
335
|
},
|
|
324
336
|
},
|
|
325
337
|
harmony_remove_label_from_card: {
|
|
@@ -1703,6 +1715,19 @@ export function registerHandlers(server, deps) {
|
|
|
1703
1715
|
throw new Error(`Unknown resource: ${uri}`);
|
|
1704
1716
|
});
|
|
1705
1717
|
}
|
|
1718
|
+
/** Resolve a column name to its ID. Prefers exact match, falls back to substring. */
|
|
1719
|
+
async function resolveColumnByName(client, projectId, columnName) {
|
|
1720
|
+
const board = await client.getBoard(projectId, { summary: true });
|
|
1721
|
+
const columns = board.columns;
|
|
1722
|
+
const lower = columnName.toLowerCase();
|
|
1723
|
+
const col = columns.find((c) => c.name.toLowerCase() === lower) ||
|
|
1724
|
+
columns.find((c) => c.name.toLowerCase().includes(lower));
|
|
1725
|
+
if (!col) {
|
|
1726
|
+
const available = columns.map((c) => c.name).join(", ");
|
|
1727
|
+
throw new Error(`Column "${columnName}" not found. Available columns: ${available}`);
|
|
1728
|
+
}
|
|
1729
|
+
return col;
|
|
1730
|
+
}
|
|
1706
1731
|
async function handleToolCall(name, args, deps) {
|
|
1707
1732
|
// Unauthenticated tools that don't require an API key
|
|
1708
1733
|
const unauthenticatedTools = ["harmony_signup", "harmony_onboard"];
|
|
@@ -1757,19 +1782,36 @@ async function handleToolCall(name, args, deps) {
|
|
|
1757
1782
|
}
|
|
1758
1783
|
case "harmony_move_card": {
|
|
1759
1784
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
1760
|
-
const columnId = z.string().uuid().parse(args.columnId);
|
|
1761
1785
|
const position = args.position !== undefined
|
|
1762
1786
|
? z.number().int().min(0).parse(args.position)
|
|
1763
1787
|
: undefined;
|
|
1788
|
+
// Resolve columnId — accept UUID directly or resolve from columnName
|
|
1789
|
+
let columnId;
|
|
1790
|
+
let resolvedProjectId;
|
|
1791
|
+
if (args.columnId) {
|
|
1792
|
+
columnId = z.string().uuid().parse(args.columnId);
|
|
1793
|
+
}
|
|
1794
|
+
else if (args.columnName) {
|
|
1795
|
+
const columnName = z.string().parse(args.columnName);
|
|
1796
|
+
const { card: cardForProject } = await client.getCard(cardId);
|
|
1797
|
+
resolvedProjectId = cardForProject
|
|
1798
|
+
?.project_id;
|
|
1799
|
+
if (!resolvedProjectId)
|
|
1800
|
+
throw new Error("Card has no project");
|
|
1801
|
+
const col = await resolveColumnByName(client, resolvedProjectId, columnName);
|
|
1802
|
+
columnId = col.id;
|
|
1803
|
+
}
|
|
1804
|
+
else {
|
|
1805
|
+
throw new Error("Either columnId or columnName is required");
|
|
1806
|
+
}
|
|
1764
1807
|
const result = await client.moveCard(cardId, columnId, position);
|
|
1765
1808
|
// Auto-end active agent session when moving to Review or Done
|
|
1766
1809
|
let sessionEnded = false;
|
|
1767
1810
|
try {
|
|
1768
1811
|
const { card } = result;
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
});
|
|
1812
|
+
const projectId = card?.project_id || resolvedProjectId;
|
|
1813
|
+
if (projectId) {
|
|
1814
|
+
const board = await client.getBoard(projectId, { summary: true });
|
|
1773
1815
|
const columns = board.columns;
|
|
1774
1816
|
const destCol = columns.find((c) => c.id === columnId);
|
|
1775
1817
|
const colName = destCol?.name?.toLowerCase() || "";
|
|
@@ -1870,7 +1912,38 @@ async function handleToolCall(name, args, deps) {
|
|
|
1870
1912
|
}
|
|
1871
1913
|
case "harmony_add_label_to_card": {
|
|
1872
1914
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
1873
|
-
|
|
1915
|
+
let labelId = args.labelId
|
|
1916
|
+
? z.string().uuid().parse(args.labelId)
|
|
1917
|
+
: undefined;
|
|
1918
|
+
const labelName = args.labelName
|
|
1919
|
+
? z.string().min(1).max(100).parse(args.labelName)
|
|
1920
|
+
: undefined;
|
|
1921
|
+
// If labelName provided without labelId, look up or create the label
|
|
1922
|
+
if (!labelId && labelName) {
|
|
1923
|
+
const { card } = await client.getCard(cardId);
|
|
1924
|
+
const projectId = card.project_id;
|
|
1925
|
+
if (!projectId) {
|
|
1926
|
+
throw new Error("Cannot resolve label by name: card has no project_id");
|
|
1927
|
+
}
|
|
1928
|
+
if (projectId) {
|
|
1929
|
+
const board = await client.getBoard(projectId, { summary: true });
|
|
1930
|
+
const labels = board.labels;
|
|
1931
|
+
const existing = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
|
|
1932
|
+
if (existing) {
|
|
1933
|
+
labelId = existing.id;
|
|
1934
|
+
}
|
|
1935
|
+
else {
|
|
1936
|
+
const created = await client.createLabel(projectId, {
|
|
1937
|
+
name: labelName,
|
|
1938
|
+
color: "#57b8a5",
|
|
1939
|
+
});
|
|
1940
|
+
labelId = created.label.id;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
if (!labelId) {
|
|
1945
|
+
throw new Error("Either labelId or labelName must be provided");
|
|
1946
|
+
}
|
|
1874
1947
|
await client.addLabelToCard(cardId, labelId);
|
|
1875
1948
|
return { success: true };
|
|
1876
1949
|
}
|
|
@@ -2015,10 +2088,18 @@ async function handleToolCall(name, args, deps) {
|
|
|
2015
2088
|
}
|
|
2016
2089
|
if (addLabels?.length) {
|
|
2017
2090
|
for (const labelName of addLabels) {
|
|
2018
|
-
|
|
2091
|
+
let label = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
|
|
2092
|
+
if (!label && projectId) {
|
|
2093
|
+
const created = await client.createLabel(projectId, {
|
|
2094
|
+
name: labelName,
|
|
2095
|
+
color: "#57b8a5",
|
|
2096
|
+
});
|
|
2097
|
+
label = created
|
|
2098
|
+
.label;
|
|
2099
|
+
}
|
|
2019
2100
|
if (label) {
|
|
2020
2101
|
await client.addLabelToCard(cardId, label.id);
|
|
2021
|
-
labelsAdded.push(label.name);
|
|
2102
|
+
labelsAdded.push(label.name ?? labelName);
|
|
2022
2103
|
}
|
|
2023
2104
|
}
|
|
2024
2105
|
}
|
|
@@ -2176,29 +2257,25 @@ async function handleToolCall(name, args, deps) {
|
|
|
2176
2257
|
// Final flush of any pending memory actions before ending the session
|
|
2177
2258
|
await flushMemoryActions(client, cardId);
|
|
2178
2259
|
cleanupMemorySession(cardId);
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2260
|
+
// End the session — tolerate failure (e.g., session already ended or not found)
|
|
2261
|
+
let result = { session: null };
|
|
2262
|
+
let sessionEndError = null;
|
|
2263
|
+
try {
|
|
2264
|
+
result = await client.endAgentSession(cardId, {
|
|
2265
|
+
status: sessionStatus,
|
|
2266
|
+
progressPercent: endProgressPercent,
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
catch (err) {
|
|
2270
|
+
sessionEndError =
|
|
2271
|
+
err instanceof Error ? err.message : "Failed to end session";
|
|
2272
|
+
}
|
|
2273
|
+
// Remove from auto-session tracking regardless
|
|
2184
2274
|
untrack(cardId);
|
|
2185
2275
|
let movedTo = null;
|
|
2186
|
-
const _learningsExtracted = 0;
|
|
2187
|
-
// Get card info for move and learning extraction
|
|
2188
|
-
let _cardTitle = "";
|
|
2189
|
-
let _cardLabels = [];
|
|
2190
|
-
let _cardDescription = "";
|
|
2191
|
-
let _cardSubtasks = [];
|
|
2192
2276
|
try {
|
|
2193
2277
|
const { card } = await client.getCard(cardId);
|
|
2194
2278
|
const typedCard = card;
|
|
2195
|
-
_cardTitle = typedCard.title || "";
|
|
2196
|
-
_cardLabels = (typedCard.labels || []).map((l) => l.name);
|
|
2197
|
-
_cardDescription = typedCard.description || "";
|
|
2198
|
-
_cardSubtasks = (typedCard.subtasks || []).map((s) => ({
|
|
2199
|
-
title: s.title,
|
|
2200
|
-
done: s.done,
|
|
2201
|
-
}));
|
|
2202
2279
|
const projectId = typedCard.project_id;
|
|
2203
2280
|
// Remove "agent" label when session is completed (not paused)
|
|
2204
2281
|
if (sessionStatus === "completed" && typedCard.labels?.length) {
|
|
@@ -2208,15 +2285,9 @@ async function handleToolCall(name, args, deps) {
|
|
|
2208
2285
|
}
|
|
2209
2286
|
}
|
|
2210
2287
|
if (moveToColumn && projectId) {
|
|
2211
|
-
const
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
const columns = board.columns;
|
|
2215
|
-
const col = columns.find((c) => c.name.toLowerCase().includes(moveToColumn.toLowerCase()));
|
|
2216
|
-
if (col) {
|
|
2217
|
-
await client.moveCard(cardId, col.id);
|
|
2218
|
-
movedTo = col.name;
|
|
2219
|
-
}
|
|
2288
|
+
const col = await resolveColumnByName(client, projectId, moveToColumn);
|
|
2289
|
+
await client.moveCard(cardId, col.id);
|
|
2290
|
+
movedTo = col.name;
|
|
2220
2291
|
}
|
|
2221
2292
|
}
|
|
2222
2293
|
catch {
|
|
@@ -2227,6 +2298,7 @@ async function handleToolCall(name, args, deps) {
|
|
|
2227
2298
|
const pipelineResult = await runEndSessionPipeline(client, deps, cardId, sessionStatus, endProgressPercent, sessionObj);
|
|
2228
2299
|
return {
|
|
2229
2300
|
success: true,
|
|
2301
|
+
...(sessionEndError && { sessionEndError }),
|
|
2230
2302
|
movedTo,
|
|
2231
2303
|
learningsExtracted: pipelineResult.learningsExtracted,
|
|
2232
2304
|
feedbackAdjusted: pipelineResult.feedbackAdjusted,
|
|
@@ -3072,44 +3144,28 @@ async function handleToolCall(name, args, deps) {
|
|
|
3072
3144
|
const projectName = args.projectName || "My First Project";
|
|
3073
3145
|
const template = args.template || "kanban";
|
|
3074
3146
|
const keyName = args.keyName || "mcp-agent";
|
|
3075
|
-
const
|
|
3076
|
-
// 1. Signup
|
|
3077
|
-
const signupResult = await signupUser(apiUrl, {
|
|
3147
|
+
const result = await onboardNewUser({
|
|
3078
3148
|
email,
|
|
3079
3149
|
password,
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
// 2. Create workspace
|
|
3084
|
-
const workspaceResult = await requestWithBearer(apiUrl, token, "POST", "/workspaces", {
|
|
3085
|
-
name: workspaceName,
|
|
3086
|
-
});
|
|
3087
|
-
// 3. Create project
|
|
3088
|
-
const projectResult = await requestWithBearer(apiUrl, token, "POST", "/projects", {
|
|
3089
|
-
workspaceId: workspaceResult.workspace.id,
|
|
3090
|
-
name: projectName,
|
|
3150
|
+
fullName,
|
|
3151
|
+
workspaceName,
|
|
3152
|
+
projectName,
|
|
3091
3153
|
template,
|
|
3154
|
+
keyName,
|
|
3155
|
+
apiUrl: deps.getApiUrl(),
|
|
3092
3156
|
});
|
|
3093
|
-
//
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
// 5. Save config
|
|
3098
|
-
deps.saveConfig({ apiKey: keyResult.rawKey });
|
|
3099
|
-
deps.setActiveWorkspace(workspaceResult.workspace.id);
|
|
3100
|
-
deps.setActiveProject(projectResult.project.id);
|
|
3101
|
-
// 6. Reset client so singleton picks up new key
|
|
3157
|
+
// Save config and reset client
|
|
3158
|
+
deps.saveConfig({ apiKey: result.apiKey.rawKey });
|
|
3159
|
+
deps.setActiveWorkspace(result.workspace.id);
|
|
3160
|
+
deps.setActiveProject(result.project.id);
|
|
3102
3161
|
deps.resetClient();
|
|
3103
3162
|
return {
|
|
3104
3163
|
success: true,
|
|
3105
|
-
user:
|
|
3106
|
-
workspace:
|
|
3107
|
-
project:
|
|
3108
|
-
columns:
|
|
3109
|
-
apiKey:
|
|
3110
|
-
rawKey: keyResult.rawKey,
|
|
3111
|
-
prefix: keyResult.apiKey.prefix,
|
|
3112
|
-
},
|
|
3164
|
+
user: result.user,
|
|
3165
|
+
workspace: result.workspace,
|
|
3166
|
+
project: result.project,
|
|
3167
|
+
columns: result.columns,
|
|
3168
|
+
apiKey: result.apiKey,
|
|
3113
3169
|
message: `Onboarding complete! Account created for ${email}. Workspace "${workspaceName}" and project "${projectName}" are ready. API key saved to config.`,
|
|
3114
3170
|
};
|
|
3115
3171
|
}
|
package/dist/lib/skills.js
CHANGED
|
@@ -4,7 +4,7 @@ import { areSkillsInstalled } from "./config.js";
|
|
|
4
4
|
export const SKILLS_VERSION = "3";
|
|
5
5
|
const VERSION_MARKER_PREFIX = "<!-- skills-version:";
|
|
6
6
|
/**
|
|
7
|
-
* Legacy workflow prompt used by Codex, Cursor
|
|
7
|
+
* Legacy workflow prompt used by Codex, Cursor agents.
|
|
8
8
|
* Claude Code skills use the newer SKILL_DEFINITIONS content instead.
|
|
9
9
|
*/
|
|
10
10
|
export const HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow
|
package/dist/lib/tui/setup.js
CHANGED
|
@@ -2,7 +2,8 @@ import { existsSync, lstatSync, mkdirSync, symlinkSync, unlinkSync, } from "node
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
|
-
import { areSkillsInstalled, getConfigPath, getLocalConfigPath, hasProjectContext, isConfigured, loadConfig, saveLocalConfig, } from "../config.js";
|
|
5
|
+
import { areSkillsInstalled, getConfigPath, getLocalConfigPath, hasProjectContext, isConfigured, loadConfig, saveConfig, saveLocalConfig, setActiveProject, setActiveWorkspace, } from "../config.js";
|
|
6
|
+
import { onboardNewUser } from "../onboard.js";
|
|
6
7
|
import { buildSkillFile, HARMONY_WORKFLOW_PROMPT } from "../skills.js";
|
|
7
8
|
import { detectAgents } from "./agents.js";
|
|
8
9
|
import { runDocsStep } from "./docs.js";
|
|
@@ -388,47 +389,172 @@ export async function runSetup(options = {}) {
|
|
|
388
389
|
if (options.workspaceId || options.projectId) {
|
|
389
390
|
needsContext = true;
|
|
390
391
|
}
|
|
391
|
-
// Step 1: API Key
|
|
392
|
+
// Step 1: API Key (or create account)
|
|
392
393
|
let apiKey = options.apiKey || existingConfig.apiKey;
|
|
393
394
|
let userEmail = options.userEmail || existingConfig.userEmail || undefined;
|
|
395
|
+
let selectedWorkspaceIdFromSignup;
|
|
396
|
+
let selectedProjectIdFromSignup;
|
|
397
|
+
let selectedWorkspaceNameFromSignup;
|
|
398
|
+
let selectedProjectNameFromSignup;
|
|
399
|
+
let createdNewAccount = false;
|
|
394
400
|
if (needsApiKey || !apiKey || !apiKey.startsWith("hmy_")) {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
401
|
+
// Determine path: create account or enter API key
|
|
402
|
+
let useNewAccount = options.newAccount === true;
|
|
403
|
+
if (!useNewAccount && options.apiKey) {
|
|
404
|
+
// API key passed via flag — skip the choice prompt
|
|
405
|
+
useNewAccount = false;
|
|
406
|
+
}
|
|
407
|
+
else if (!useNewAccount && !options.apiKey) {
|
|
408
|
+
const getStarted = await p.select({
|
|
409
|
+
message: "How would you like to get started?",
|
|
410
|
+
options: [
|
|
411
|
+
{
|
|
412
|
+
value: "create",
|
|
413
|
+
label: "Create a free account",
|
|
414
|
+
hint: "recommended for new users",
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
value: "apikey",
|
|
418
|
+
label: "I already have an API key",
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
});
|
|
422
|
+
if (p.isCancel(getStarted)) {
|
|
423
|
+
p.cancel("Setup cancelled");
|
|
424
|
+
process.exit(0);
|
|
425
|
+
}
|
|
426
|
+
useNewAccount = getStarted === "create";
|
|
427
|
+
}
|
|
428
|
+
if (useNewAccount) {
|
|
429
|
+
// --- Create account flow ---
|
|
430
|
+
const fullName = options.name ||
|
|
431
|
+
(await p.text({
|
|
432
|
+
message: "Full name",
|
|
433
|
+
placeholder: "Jane Smith",
|
|
434
|
+
validate: (v) => {
|
|
435
|
+
if (!v || v.trim().length === 0)
|
|
436
|
+
return "Name is required";
|
|
437
|
+
if (v.length > 100)
|
|
438
|
+
return "Name must be 100 characters or less";
|
|
439
|
+
return undefined;
|
|
440
|
+
},
|
|
441
|
+
}));
|
|
442
|
+
if (p.isCancel(fullName)) {
|
|
443
|
+
p.cancel("Setup cancelled");
|
|
444
|
+
process.exit(0);
|
|
445
|
+
}
|
|
446
|
+
const email = options.userEmail ||
|
|
447
|
+
(await p.text({
|
|
448
|
+
message: "Email",
|
|
449
|
+
placeholder: "you@example.com",
|
|
450
|
+
validate: (v) => {
|
|
451
|
+
if (!v)
|
|
452
|
+
return "Email is required";
|
|
453
|
+
if (v.length > 254)
|
|
454
|
+
return "Email is too long";
|
|
455
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v))
|
|
456
|
+
return "Invalid email format";
|
|
457
|
+
return undefined;
|
|
458
|
+
},
|
|
459
|
+
}));
|
|
460
|
+
if (p.isCancel(email)) {
|
|
461
|
+
p.cancel("Setup cancelled");
|
|
462
|
+
process.exit(0);
|
|
463
|
+
}
|
|
464
|
+
const password = (await p.password({
|
|
465
|
+
message: "Password",
|
|
466
|
+
validate: (v) => {
|
|
467
|
+
if (!v)
|
|
468
|
+
return "Password is required";
|
|
469
|
+
if (v.length < 8)
|
|
470
|
+
return "Password must be at least 8 characters";
|
|
471
|
+
if (v.length > 128)
|
|
472
|
+
return "Password must be 128 characters or less";
|
|
473
|
+
return undefined;
|
|
474
|
+
},
|
|
475
|
+
}));
|
|
476
|
+
if (p.isCancel(password)) {
|
|
477
|
+
p.cancel("Setup cancelled");
|
|
478
|
+
process.exit(0);
|
|
479
|
+
}
|
|
480
|
+
const spinner = p.spinner();
|
|
481
|
+
spinner.start("Creating your account...");
|
|
482
|
+
try {
|
|
483
|
+
const result = await onboardNewUser({
|
|
484
|
+
email: email,
|
|
485
|
+
password: password,
|
|
486
|
+
fullName: fullName,
|
|
487
|
+
});
|
|
488
|
+
spinner.stop(colors.success(`Account created for ${result.user.email}`));
|
|
489
|
+
apiKey = result.apiKey.rawKey;
|
|
490
|
+
userEmail = result.user.email;
|
|
491
|
+
selectedWorkspaceIdFromSignup = result.workspace.id;
|
|
492
|
+
selectedProjectIdFromSignup = result.project.id;
|
|
493
|
+
selectedWorkspaceNameFromSignup = result.workspace.name;
|
|
494
|
+
selectedProjectNameFromSignup = result.project.name;
|
|
495
|
+
createdNewAccount = true;
|
|
496
|
+
needsApiKey = true;
|
|
497
|
+
// Save config immediately
|
|
498
|
+
saveConfig({ apiKey, userEmail, apiUrl: API_URL });
|
|
499
|
+
setActiveWorkspace(selectedWorkspaceIdFromSignup);
|
|
500
|
+
setActiveProject(selectedProjectIdFromSignup);
|
|
501
|
+
p.log.success("Workspace and board created");
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
spinner.stop(colors.error("Account creation failed"));
|
|
505
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
506
|
+
if (msg.includes("already") || msg.includes("409")) {
|
|
507
|
+
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'.");
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
p.log.error(msg);
|
|
511
|
+
p.log.info("Please try again or visit https://app.gethmy.com");
|
|
512
|
+
}
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
// --- Existing API key flow ---
|
|
518
|
+
const keyInput = await p.text({
|
|
519
|
+
message: "Enter your Harmony API key",
|
|
520
|
+
placeholder: "hmy_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
521
|
+
validate: (value) => {
|
|
522
|
+
if (!value)
|
|
523
|
+
return "API key is required";
|
|
524
|
+
if (!value.startsWith("hmy_"))
|
|
525
|
+
return 'API key must start with "hmy_"';
|
|
526
|
+
if (value.length < 20)
|
|
527
|
+
return "API key is too short";
|
|
528
|
+
return undefined;
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
if (p.isCancel(keyInput)) {
|
|
532
|
+
p.cancel("Setup cancelled");
|
|
533
|
+
process.exit(0);
|
|
534
|
+
}
|
|
535
|
+
apiKey = keyInput;
|
|
536
|
+
needsApiKey = true;
|
|
411
537
|
}
|
|
412
|
-
apiKey = keyInput;
|
|
413
|
-
needsApiKey = true;
|
|
414
538
|
}
|
|
415
539
|
else {
|
|
416
540
|
p.log.success(`Using existing API key: ${apiKey.slice(0, 8)}...`);
|
|
417
541
|
}
|
|
418
|
-
// Validate API key
|
|
542
|
+
// Validate API key (skip if we just created account — key is guaranteed valid)
|
|
419
543
|
const spinner = p.spinner();
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
userEmail
|
|
430
|
-
|
|
431
|
-
|
|
544
|
+
if (!createdNewAccount) {
|
|
545
|
+
spinner.start("Validating API key...");
|
|
546
|
+
const validation = await validateApiKey(apiKey);
|
|
547
|
+
if (!validation.valid) {
|
|
548
|
+
spinner.stop(colors.error("API key validation failed"));
|
|
549
|
+
p.log.error(validation.error || "Could not connect to Harmony API");
|
|
550
|
+
p.log.info("Get an API key at: https://app.gethmy.com/user/keys");
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
if (!userEmail) {
|
|
554
|
+
userEmail = validation.email;
|
|
555
|
+
}
|
|
556
|
+
spinner.stop(colors.success(userEmail ? `Connected as ${userEmail}` : "API key validated"));
|
|
557
|
+
}
|
|
432
558
|
// Step 2: Check if skills are already installed
|
|
433
559
|
let selectedAgents = [];
|
|
434
560
|
let installMode = options.installMode || "global";
|
|
@@ -502,10 +628,14 @@ export async function runSetup(options = {}) {
|
|
|
502
628
|
}
|
|
503
629
|
}
|
|
504
630
|
// Step 3: Workspace and Project Selection
|
|
505
|
-
let selectedWorkspaceId = options.workspaceId;
|
|
506
|
-
let selectedProjectId = options.projectId;
|
|
507
|
-
let selectedWorkspaceName;
|
|
508
|
-
let selectedProjectName;
|
|
631
|
+
let selectedWorkspaceId = selectedWorkspaceIdFromSignup || options.workspaceId;
|
|
632
|
+
let selectedProjectId = selectedProjectIdFromSignup || options.projectId;
|
|
633
|
+
let selectedWorkspaceName = selectedWorkspaceNameFromSignup;
|
|
634
|
+
let selectedProjectName = selectedProjectNameFromSignup;
|
|
635
|
+
// Skip context selection if we just created a new account
|
|
636
|
+
if (createdNewAccount) {
|
|
637
|
+
needsContext = false;
|
|
638
|
+
}
|
|
509
639
|
if (needsContext && !options.skipContext) {
|
|
510
640
|
// Fetch workspaces
|
|
511
641
|
spinner.start("Fetching workspaces...");
|
|
@@ -742,29 +872,52 @@ export async function runSetup(options = {}) {
|
|
|
742
872
|
// Step 10: Show completion message
|
|
743
873
|
console.log("");
|
|
744
874
|
p.outro(colors.success("Setup complete!"));
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
console.log(
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
console.log(`
|
|
875
|
+
if (createdNewAccount && selectedWorkspaceNameFromSignup) {
|
|
876
|
+
// New account: show board URL and next steps
|
|
877
|
+
const wsSlug = selectedWorkspaceNameFromSignup
|
|
878
|
+
.toLowerCase()
|
|
879
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
880
|
+
.replace(/(^-|-$)/g, "");
|
|
881
|
+
const projSlug = (selectedProjectNameFromSignup || "my-first-board")
|
|
882
|
+
.toLowerCase()
|
|
883
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
884
|
+
.replace(/(^-|-$)/g, "");
|
|
885
|
+
console.log("");
|
|
886
|
+
console.log(` ${colors.bold("Your board:")} ${colors.highlight(`https://app.gethmy.com/${wsSlug}/${projSlug}`)}`);
|
|
887
|
+
console.log("");
|
|
888
|
+
console.log(` ${colors.bold("Next steps:")}`);
|
|
889
|
+
console.log(` 1. Open Claude Code and say: ${colors.highlight('"Show me my board"')}`);
|
|
890
|
+
console.log(` 2. Create a card: ${colors.highlight('"Create a card called Auth token refresh"')}`);
|
|
891
|
+
console.log(` 3. Start the daemon: ${colors.highlight("npx @gethmy/agent")}`);
|
|
892
|
+
console.log("");
|
|
893
|
+
console.log(` ${colors.dim("Happy shipping!")}`);
|
|
761
894
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
console.log(
|
|
895
|
+
else {
|
|
896
|
+
// Existing user: show config paths and usage
|
|
897
|
+
console.log("");
|
|
898
|
+
console.log(` ${colors.bold("Configuration:")}`);
|
|
899
|
+
console.log(` API key: ${formatPath(getConfigPath(), home)}`);
|
|
900
|
+
if (needsSkills && selectedAgents.length > 0) {
|
|
901
|
+
console.log(` Skills: ${installMode === "global" ? "~/.agents/skills/ (global)" : ".claude/skills/ (local)"}`);
|
|
902
|
+
}
|
|
903
|
+
if (selectedWorkspaceId || selectedProjectId) {
|
|
904
|
+
console.log(` Context: ${formatPath(getLocalConfigPath(cwd), home)}`);
|
|
905
|
+
}
|
|
906
|
+
console.log("");
|
|
907
|
+
console.log(` ${colors.bold("Usage:")}`);
|
|
908
|
+
if (!needsSkills || selectedAgents.includes("claude")) {
|
|
909
|
+
console.log(` ${colors.brand("Claude Code:")} ${colors.highlight("/hmy #42")} or ${colors.highlight("/hmy-plan")} ${colors.dim("(create or execute plans)")}`);
|
|
910
|
+
}
|
|
911
|
+
if (selectedAgents.includes("codex")) {
|
|
912
|
+
console.log(` ${colors.brand("Codex:")} ${colors.highlight("/prompts:hmy #42")}`);
|
|
913
|
+
}
|
|
914
|
+
if (selectedAgents.includes("cursor") ||
|
|
915
|
+
selectedAgents.includes("windsurf")) {
|
|
916
|
+
console.log(` ${colors.brand("Cursor:")} MCP tools available automatically`);
|
|
917
|
+
}
|
|
918
|
+
console.log("");
|
|
919
|
+
console.log(` ${colors.dim("Add to new project: npx @gethmy/mcp setup")}`);
|
|
920
|
+
console.log(` ${colors.dim("Need help? Visit https://app.gethmy.com/docs/mcp")}`);
|
|
765
921
|
}
|
|
766
922
|
console.log("");
|
|
767
|
-
console.log(` ${colors.dim("Add to new project: npx @gethmy/mcp setup")}`);
|
|
768
|
-
console.log(` ${colors.dim("Need help? Visit https://app.gethmy.com/docs/mcp")}`);
|
|
769
|
-
console.log("");
|
|
770
923
|
}
|