@gethmy/mcp 2.5.5 → 2.5.7

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 CHANGED
@@ -6,7 +6,7 @@ Enables AI coding agents (Claude Code, OpenAI Codex, Cursor) to interact with yo
6
6
  ## Features
7
7
 
8
8
  - **56 MCP Tools** for full board control, knowledge graph, and workflow plans
9
- - **4 Global Skills** — `/hmy`, `/hmy-plan`, `/hmy-cleanup`, `/hmy-standup`, served from the DB-backed [skill hub](../../docs/skills.md) with auto-update and admin-managed versioning
9
+ - **5 Global Skills** — `/hmy`, `/hmy-plan`, `/hmy-cleanup`, `/hmy-standup`, `/hmy-memory-prune`, served from the DB-backed [skill hub](../../docs/skills.md) with auto-update and admin-managed versioning
10
10
  - **Knowledge Graph Memory** — Phase 1 surface: hybrid retrieval (vector + lexical + RRF), session-scoped working memory, activity feed. See [docs/memory.md](../../docs/memory.md)
11
11
  - **GSD Workflow Plans** - plan/execute/verify/done lifecycle with auto card creation
12
12
  - **Card Linking** - create relationships between cards (blocks, relates_to, duplicates, is_part_of)
@@ -154,7 +154,7 @@ npx @gethmy/mcp serve # Start MCP server
154
154
 
155
155
  ## Skills
156
156
 
157
- Four global skills ship with the MCP server and are installed automatically by `npx @gethmy/mcp setup`. They live in the `skill_resource` Postgres table, are fetched via `GET /v1/skills/<name>`, and render-time composed with a shared auto-update preamble.
157
+ Five global skills ship with the MCP server and are installed automatically by `npx @gethmy/mcp setup`. They live in the `skill_resource` Postgres table, are fetched via `GET /v1/skills/<name>`, and render-time composed with a shared auto-update preamble.
158
158
 
159
159
  For the full skill hub architecture (storage, versioning, auto-update, admin management), see [docs/skills.md](../../docs/skills.md).
160
160
 
package/dist/cli.js CHANGED
@@ -4957,6 +4957,7 @@ async function refreshSkills() {
4957
4957
  }
4958
4958
 
4959
4959
  // src/tui/setup.ts
4960
+ import { createHash as createHash3 } from "node:crypto";
4960
4961
  import {
4961
4962
  existsSync as existsSync7,
4962
4963
  lstatSync,
@@ -5028,7 +5029,7 @@ import { isAbsolute, join as join4, resolve, sep as sep2 } from "node:path";
5028
5029
  import * as p from "@clack/prompts";
5029
5030
 
5030
5031
  // src/tui/theme.ts
5031
- import * as pc from "picocolors";
5032
+ import pc from "picocolors";
5032
5033
  var symbols = {
5033
5034
  harmony: "▲",
5034
5035
  check: "✓",
@@ -5729,7 +5730,13 @@ async function runDocsStep(cwd) {
5729
5730
  }
5730
5731
 
5731
5732
  // src/tui/writer.ts
5732
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
5733
+ import {
5734
+ chmodSync,
5735
+ existsSync as existsSync6,
5736
+ mkdirSync as mkdirSync4,
5737
+ readFileSync as readFileSync5,
5738
+ writeFileSync as writeFileSync4
5739
+ } from "node:fs";
5733
5740
  import { homedir as homedir3 } from "node:os";
5734
5741
  import { dirname as dirname2 } from "node:path";
5735
5742
  import * as p2 from "@clack/prompts";
@@ -5745,8 +5752,12 @@ function writeFile(filePath, content, options = {}) {
5745
5752
  }
5746
5753
  try {
5747
5754
  ensureDir(dirname2(filePath));
5748
- const mode = filePath.includes(".harmony-mcp") ? 384 : 420;
5755
+ const defaultMode = filePath.includes(".harmony-mcp") ? 384 : 420;
5756
+ const mode = options.mode ?? defaultMode;
5749
5757
  writeFileSync4(filePath, content, { mode });
5758
+ if (options.mode !== undefined) {
5759
+ chmodSync(filePath, options.mode);
5760
+ }
5750
5761
  return { path: filePath, action: exists ? "update" : "create" };
5751
5762
  } catch (error) {
5752
5763
  return {
@@ -5857,7 +5868,10 @@ async function writeFilesWithProgress(files, options = {}) {
5857
5868
  } else if (file.type === "toml" && file.tomlSection) {
5858
5869
  result = appendToToml(file.path, file.tomlSection, file.content, options);
5859
5870
  } else {
5860
- result = writeFile(file.path, file.content, options);
5871
+ result = writeFile(file.path, file.content, {
5872
+ ...options,
5873
+ mode: file.mode
5874
+ });
5861
5875
  }
5862
5876
  results.push(result);
5863
5877
  await new Promise((resolve2) => setTimeout(resolve2, 50));
@@ -5993,6 +6007,22 @@ async function fetchProjects(apiKey, workspaceId) {
5993
6007
  const data = await response.json();
5994
6008
  return data.projects || [];
5995
6009
  }
6010
+ async function resolveProjectSlug(apiKey, slug) {
6011
+ const response = await fetch(`${API_URL}/v1/projects/resolve/${encodeURIComponent(slug)}`, {
6012
+ method: "GET",
6013
+ headers: {
6014
+ "Content-Type": "application/json",
6015
+ "X-API-Key": apiKey
6016
+ }
6017
+ });
6018
+ if (response.status === 404)
6019
+ return null;
6020
+ if (!response.ok) {
6021
+ throw new Error(`Failed to resolve project slug: ${response.status}`);
6022
+ }
6023
+ const data = await response.json();
6024
+ return { workspaceId: data.workspaceId, projectId: data.projectId };
6025
+ }
5996
6026
  async function getAgentFiles(agentId, cwd, installMode = "global") {
5997
6027
  const home = homedir4();
5998
6028
  const files = [];
@@ -6035,6 +6065,24 @@ async function getAgentFiles(agentId, cwd, installMode = "global") {
6035
6065
  throw new Error(`Failed to fetch ${skillFailures.length}/${installableNames.length} skill(s) from /v1/skills:
6036
6066
  ${summary}`);
6037
6067
  }
6068
+ try {
6069
+ const updateCheckFetched = await client3.fetchSkill("hmy-update-check");
6070
+ const actualHash = createHash3("sha256").update(updateCheckFetched.content).digest("hex");
6071
+ if (actualHash !== updateCheckFetched.sha256) {
6072
+ throw new Error(`hmy-update-check integrity check failed: expected ${updateCheckFetched.sha256}, got ${actualHash}`);
6073
+ }
6074
+ files.push({
6075
+ path: join5(home, ".hmy", "bin", "hmy-update-check"),
6076
+ content: updateCheckFetched.content,
6077
+ type: "text",
6078
+ mode: 493
6079
+ });
6080
+ files.push({
6081
+ path: join5(home, ".hmy", "VERSION"),
6082
+ content: versionInfo.version,
6083
+ type: "text"
6084
+ });
6085
+ } catch {}
6038
6086
  break;
6039
6087
  }
6040
6088
  case "codex": {
@@ -6227,7 +6275,7 @@ async function runSetup(options = {}) {
6227
6275
  let needsApiKey = !alreadyConfigured;
6228
6276
  let needsSkills = !skillsStatus.installed || options.force;
6229
6277
  let needsContext = !hasContext && !options.skipContext;
6230
- if (options.workspaceId || options.projectId) {
6278
+ if (options.workspaceId || options.projectId || options.projectSlug) {
6231
6279
  needsContext = true;
6232
6280
  }
6233
6281
  let apiKey = options.apiKey || existingConfig.apiKey;
@@ -6449,6 +6497,21 @@ async function runSetup(options = {}) {
6449
6497
  let selectedProjectId = selectedProjectIdFromSignup || options.projectId;
6450
6498
  let selectedWorkspaceName = selectedWorkspaceNameFromSignup;
6451
6499
  let selectedProjectName = selectedProjectNameFromSignup;
6500
+ if (options.projectSlug && apiKey && (!selectedWorkspaceId || !selectedProjectId)) {
6501
+ spinner3.start(`Resolving project slug "${options.projectSlug}"...`);
6502
+ try {
6503
+ const resolved = await resolveProjectSlug(apiKey, options.projectSlug);
6504
+ if (resolved) {
6505
+ selectedWorkspaceId = selectedWorkspaceId || resolved.workspaceId;
6506
+ selectedProjectId = selectedProjectId || resolved.projectId;
6507
+ spinner3.stop(colors.success(`Resolved "${options.projectSlug}"`));
6508
+ } else {
6509
+ spinner3.stop(colors.warning(`No project found for slug "${options.projectSlug}"`));
6510
+ }
6511
+ } catch (error) {
6512
+ spinner3.stop(colors.warning(`Could not resolve slug: ${error instanceof Error ? error.message : "unknown error"}`));
6513
+ }
6514
+ }
6452
6515
  if (createdNewAccount) {
6453
6516
  needsContext = false;
6454
6517
  }
@@ -6775,13 +6838,14 @@ program.command("reset").description("Remove stored configuration").action(() =>
6775
6838
  console.log(`
6776
6839
  To reconfigure, run: npx @gethmy/mcp setup`);
6777
6840
  });
6778
- program.command("setup").description("Smart setup wizard for Harmony MCP (recommended)").option("-f, --force", "Overwrite existing configuration files").option("-k, --api-key <key>", "API key (skips prompt)").option("-e, --email <email>", "Your email for auto-assignment").option("-a, --agents <agents...>", "Agents to configure: claude, codex, cursor, windsurf").option("-l, --local", "Install skills locally in project directory").option("-g, --global", "Install skills globally (recommended)").option("-w, --workspace <id>", "Set workspace context").option("-p, --project <id>", "Set project context").option("--skip-context", "Skip workspace/project selection").option("--skip-docs", "Skip project docs scaffold/verification").option("--new", "Create a new account (skip the choice prompt)").option("-n, --name <name>", "Full name (for account creation)").action(async (options) => {
6841
+ program.command("setup").description("Smart setup wizard for Harmony MCP (recommended)").argument("[slug]", "Project slug — resolves to workspace + project in one step (e.g. harmony-6590761b)").option("-f, --force", "Overwrite existing configuration files").option("-k, --api-key <key>", "API key (skips prompt)").option("-e, --email <email>", "Your email for auto-assignment").option("-a, --agents <agents...>", "Agents to configure: claude, codex, cursor, windsurf").option("-l, --local", "Install skills locally in project directory").option("-g, --global", "Install skills globally (recommended)").option("-w, --workspace <id>", "Set workspace context (UUID)").option("-p, --project <id>", "Set project context (UUID)").option("--skip-context", "Skip workspace/project selection").option("--skip-docs", "Skip project docs scaffold/verification").option("--new", "Create a new account (skip the choice prompt)").option("-n, --name <name>", "Full name (for account creation)").action(async (slug, options) => {
6779
6842
  await runSetup({
6780
6843
  force: options.force,
6781
6844
  apiKey: options.apiKey,
6782
6845
  userEmail: options.email,
6783
6846
  agents: options.agents,
6784
6847
  installMode: options.global ? "global" : options.local ? "local" : undefined,
6848
+ projectSlug: slug,
6785
6849
  workspaceId: options.workspace,
6786
6850
  projectId: options.project,
6787
6851
  skipContext: options.skipContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.5.5",
3
+ "version": "2.5.7",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/cli.ts CHANGED
@@ -139,6 +139,10 @@ program
139
139
  program
140
140
  .command("setup")
141
141
  .description("Smart setup wizard for Harmony MCP (recommended)")
142
+ .argument(
143
+ "[slug]",
144
+ "Project slug — resolves to workspace + project in one step (e.g. harmony-6590761b)",
145
+ )
142
146
  .option("-f, --force", "Overwrite existing configuration files")
143
147
  .option("-k, --api-key <key>", "API key (skips prompt)")
144
148
  .option("-e, --email <email>", "Your email for auto-assignment")
@@ -148,13 +152,13 @@ program
148
152
  )
149
153
  .option("-l, --local", "Install skills locally in project directory")
150
154
  .option("-g, --global", "Install skills globally (recommended)")
151
- .option("-w, --workspace <id>", "Set workspace context")
152
- .option("-p, --project <id>", "Set project context")
155
+ .option("-w, --workspace <id>", "Set workspace context (UUID)")
156
+ .option("-p, --project <id>", "Set project context (UUID)")
153
157
  .option("--skip-context", "Skip workspace/project selection")
154
158
  .option("--skip-docs", "Skip project docs scaffold/verification")
155
159
  .option("--new", "Create a new account (skip the choice prompt)")
156
160
  .option("-n, --name <name>", "Full name (for account creation)")
157
- .action(async (options) => {
161
+ .action(async (slug, options) => {
158
162
  await runSetup({
159
163
  force: options.force,
160
164
  apiKey: options.apiKey,
@@ -165,6 +169,7 @@ program
165
169
  : options.local
166
170
  ? "local"
167
171
  : undefined,
172
+ projectSlug: slug,
168
173
  workspaceId: options.workspace,
169
174
  projectId: options.project,
170
175
  skipContext: options.skipContext,
package/src/tui/setup.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import {
2
3
  existsSync,
3
4
  lstatSync,
@@ -38,6 +39,7 @@ export interface SetupOptions {
38
39
  installMode?: InstallMode;
39
40
  workspaceId?: string;
40
41
  projectId?: string;
42
+ projectSlug?: string;
41
43
  skipContext?: boolean;
42
44
  skipDocs?: boolean;
43
45
  newAccount?: boolean;
@@ -211,11 +213,40 @@ async function fetchProjects(
211
213
  return data.projects || [];
212
214
  }
213
215
 
216
+ /**
217
+ * Resolve a project slug to {workspaceId, projectId}. Used by
218
+ * `npx @gethmy/mcp setup <slug>` so users don't have to copy raw UUIDs.
219
+ */
220
+ async function resolveProjectSlug(
221
+ apiKey: string,
222
+ slug: string,
223
+ ): Promise<{ workspaceId: string; projectId: string } | null> {
224
+ const response = await fetch(
225
+ `${API_URL}/v1/projects/resolve/${encodeURIComponent(slug)}`,
226
+ {
227
+ method: "GET",
228
+ headers: {
229
+ "Content-Type": "application/json",
230
+ "X-API-Key": apiKey,
231
+ },
232
+ },
233
+ );
234
+
235
+ if (response.status === 404) return null;
236
+ if (!response.ok) {
237
+ throw new Error(`Failed to resolve project slug: ${response.status}`);
238
+ }
239
+
240
+ const data = await response.json();
241
+ return { workspaceId: data.workspaceId, projectId: data.projectId };
242
+ }
243
+
214
244
  export interface FileToWrite {
215
245
  path: string;
216
246
  content: string;
217
247
  type: "text" | "json" | "toml";
218
248
  tomlSection?: string;
249
+ mode?: number;
219
250
  }
220
251
 
221
252
  export interface SymlinkToCreate {
@@ -288,6 +319,36 @@ async function getAgentFiles(
288
319
  );
289
320
  }
290
321
 
322
+ // Pre-populate ~/.hmy/VERSION and ~/.hmy/bin/hmy-update-check so the
323
+ // lazy bootstrap inside the skill preamble is bypassed on first run.
324
+ // Without this, the bootstrap writes "1.0.0" as a fallback whenever the
325
+ // version fetch times out (edge function cold start) — triggering a
326
+ // spurious "upgrade to v6" prompt the moment the skill is first invoked.
327
+ try {
328
+ const updateCheckFetched = await client.fetchSkill("hmy-update-check");
329
+ const actualHash = createHash("sha256")
330
+ .update(updateCheckFetched.content)
331
+ .digest("hex");
332
+ if (actualHash !== updateCheckFetched.sha256) {
333
+ throw new Error(
334
+ `hmy-update-check integrity check failed: expected ${updateCheckFetched.sha256}, got ${actualHash}`,
335
+ );
336
+ }
337
+ files.push({
338
+ path: join(home, ".hmy", "bin", "hmy-update-check"),
339
+ content: updateCheckFetched.content,
340
+ type: "text",
341
+ mode: 0o755,
342
+ });
343
+ files.push({
344
+ path: join(home, ".hmy", "VERSION"),
345
+ content: versionInfo.version,
346
+ type: "text",
347
+ });
348
+ } catch {
349
+ // Non-fatal — bootstrap will install both on first skill invocation.
350
+ }
351
+
291
352
  // Note: MCP server registration is handled separately via `claude mcp add` CLI
292
353
  // in runSetup() after file writing, with fallback to settings.json if CLI unavailable
293
354
  break;
@@ -524,8 +585,8 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
524
585
  let needsSkills = !skillsStatus.installed || options.force;
525
586
  let needsContext = !hasContext && !options.skipContext;
526
587
 
527
- // If workspace/project provided via flags, we'll set context
528
- if (options.workspaceId || options.projectId) {
588
+ // If workspace/project/slug provided via flags or argument, we'll set context
589
+ if (options.workspaceId || options.projectId || options.projectSlug) {
529
590
  needsContext = true;
530
591
  }
531
592
 
@@ -806,6 +867,35 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
806
867
  selectedWorkspaceNameFromSignup;
807
868
  let selectedProjectName: string | undefined = selectedProjectNameFromSignup;
808
869
 
870
+ // Resolve project slug shorthand (e.g. `npx @gethmy/mcp setup harmony-6590761b`).
871
+ // Slug wins over --workspace/--project flags only when those aren't already set
872
+ // from signup or explicit flags.
873
+ if (
874
+ options.projectSlug &&
875
+ apiKey &&
876
+ (!selectedWorkspaceId || !selectedProjectId)
877
+ ) {
878
+ spinner.start(`Resolving project slug "${options.projectSlug}"...`);
879
+ try {
880
+ const resolved = await resolveProjectSlug(apiKey, options.projectSlug);
881
+ if (resolved) {
882
+ selectedWorkspaceId = selectedWorkspaceId || resolved.workspaceId;
883
+ selectedProjectId = selectedProjectId || resolved.projectId;
884
+ spinner.stop(colors.success(`Resolved "${options.projectSlug}"`));
885
+ } else {
886
+ spinner.stop(
887
+ colors.warning(`No project found for slug "${options.projectSlug}"`),
888
+ );
889
+ }
890
+ } catch (error) {
891
+ spinner.stop(
892
+ colors.warning(
893
+ `Could not resolve slug: ${error instanceof Error ? error.message : "unknown error"}`,
894
+ ),
895
+ );
896
+ }
897
+ }
898
+
809
899
  // Skip context selection if we just created a new account
810
900
  if (createdNewAccount) {
811
901
  needsContext = false;
package/src/tui/theme.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as pc from "picocolors";
1
+ import pc from "picocolors";
2
2
 
3
3
  /**
4
4
  * Consistent theme for Harmony MCP TUI
package/src/tui/writer.ts CHANGED
@@ -1,4 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import {
2
+ chmodSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ writeFileSync,
7
+ } from "node:fs";
2
8
  import { homedir } from "node:os";
3
9
  import { dirname } from "node:path";
4
10
  import * as p from "@clack/prompts";
@@ -15,6 +21,7 @@ export interface FileResult {
15
21
  interface WriteOptions {
16
22
  force?: boolean;
17
23
  merge?: boolean;
24
+ mode?: number;
18
25
  }
19
26
 
20
27
  /**
@@ -42,9 +49,12 @@ export function writeFile(
42
49
 
43
50
  try {
44
51
  ensureDir(dirname(filePath));
45
- // Use restrictive permissions for config files
46
- const mode = filePath.includes(".harmony-mcp") ? 0o600 : 0o644;
52
+ const defaultMode = filePath.includes(".harmony-mcp") ? 0o600 : 0o644;
53
+ const mode = options.mode ?? defaultMode;
47
54
  writeFileSync(filePath, content, { mode });
55
+ if (options.mode !== undefined) {
56
+ chmodSync(filePath, options.mode);
57
+ }
48
58
  return { path: filePath, action: exists ? "update" : "create" };
49
59
  } catch (error) {
50
60
  return {
@@ -183,6 +193,7 @@ export async function writeFilesWithProgress(
183
193
  type: "text" | "json" | "toml";
184
194
  jsonKey?: string;
185
195
  tomlSection?: string;
196
+ mode?: number;
186
197
  }>,
187
198
  options: WriteOptions = {},
188
199
  ): Promise<FileResult[]> {
@@ -201,7 +212,10 @@ export async function writeFilesWithProgress(
201
212
  } else if (file.type === "toml" && file.tomlSection) {
202
213
  result = appendToToml(file.path, file.tomlSection, file.content, options);
203
214
  } else {
204
- result = writeFile(file.path, file.content, options);
215
+ result = writeFile(file.path, file.content, {
216
+ ...options,
217
+ mode: file.mode,
218
+ });
205
219
  }
206
220
 
207
221
  results.push(result);