@gethmy/mcp 2.5.6 → 2.6.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/dist/cli.js CHANGED
@@ -1479,6 +1479,51 @@ class HarmonyApiClient {
1479
1479
  async updateMemoryEntity(entityId, updates) {
1480
1480
  return this.request("PUT", `/memory/entities/${entityId}`, updates);
1481
1481
  }
1482
+ async harmonyRecall(options) {
1483
+ const fetchLimit = Math.max(options.topK ?? 3, 50);
1484
+ let entities = [];
1485
+ if (options.query) {
1486
+ const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
1487
+ project_id: options.projectId,
1488
+ type: options.type?.length === 1 ? options.type[0] : undefined,
1489
+ limit: fetchLimit
1490
+ });
1491
+ entities = search.entities ?? [];
1492
+ } else {
1493
+ const list = await this.listMemoryEntities({
1494
+ workspace_id: options.workspaceId,
1495
+ project_id: options.projectId,
1496
+ scope: options.scope,
1497
+ type: options.type?.length === 1 ? options.type[0] : undefined,
1498
+ tags: options.tags,
1499
+ min_confidence: options.minConfidence,
1500
+ limit: fetchLimit
1501
+ });
1502
+ entities = list.entities ?? [];
1503
+ }
1504
+ if (options.type && options.type.length > 1) {
1505
+ const allowed = new Set(options.type);
1506
+ entities = entities.filter((e) => allowed.has(e.type));
1507
+ }
1508
+ if (options.memory_tier) {
1509
+ entities = entities.filter((e) => e.memory_tier === options.memory_tier);
1510
+ }
1511
+ if (options.scope) {
1512
+ entities = entities.filter((e) => e.scope === options.scope);
1513
+ }
1514
+ if (options.tags?.length) {
1515
+ const wanted = new Set(options.tags);
1516
+ entities = entities.filter((e) => (e.tags ?? []).some((t) => wanted.has(t)));
1517
+ }
1518
+ if (typeof options.minConfidence === "number") {
1519
+ const threshold = options.minConfidence;
1520
+ entities = entities.filter((e) => typeof e.confidence === "number" && e.confidence >= threshold);
1521
+ }
1522
+ if (options.topK !== undefined) {
1523
+ entities = entities.slice(0, options.topK);
1524
+ }
1525
+ return { entities };
1526
+ }
1482
1527
  async deleteMemoryEntity(entityId) {
1483
1528
  return this.request("DELETE", `/memory/entities/${entityId}`);
1484
1529
  }
@@ -1785,11 +1830,8 @@ function resolveAgentIdentity(info) {
1785
1830
  var AUTO_START_TRIGGERS = new Set([
1786
1831
  "harmony_generate_prompt",
1787
1832
  "harmony_update_card",
1788
- "harmony_move_card",
1789
1833
  "harmony_create_subtask",
1790
- "harmony_toggle_subtask",
1791
- "harmony_add_label_to_card",
1792
- "harmony_remove_label_from_card"
1834
+ "harmony_toggle_subtask"
1793
1835
  ]);
1794
1836
  var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
1795
1837
  var CHECK_INTERVAL_MS = 60 * 1000;
@@ -4957,6 +4999,7 @@ async function refreshSkills() {
4957
4999
  }
4958
5000
 
4959
5001
  // src/tui/setup.ts
5002
+ import { createHash as createHash3 } from "node:crypto";
4960
5003
  import {
4961
5004
  existsSync as existsSync7,
4962
5005
  lstatSync,
@@ -5729,7 +5772,13 @@ async function runDocsStep(cwd) {
5729
5772
  }
5730
5773
 
5731
5774
  // src/tui/writer.ts
5732
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
5775
+ import {
5776
+ chmodSync,
5777
+ existsSync as existsSync6,
5778
+ mkdirSync as mkdirSync4,
5779
+ readFileSync as readFileSync5,
5780
+ writeFileSync as writeFileSync4
5781
+ } from "node:fs";
5733
5782
  import { homedir as homedir3 } from "node:os";
5734
5783
  import { dirname as dirname2 } from "node:path";
5735
5784
  import * as p2 from "@clack/prompts";
@@ -5745,8 +5794,12 @@ function writeFile(filePath, content, options = {}) {
5745
5794
  }
5746
5795
  try {
5747
5796
  ensureDir(dirname2(filePath));
5748
- const mode = filePath.includes(".harmony-mcp") ? 384 : 420;
5797
+ const defaultMode = filePath.includes(".harmony-mcp") ? 384 : 420;
5798
+ const mode = options.mode ?? defaultMode;
5749
5799
  writeFileSync4(filePath, content, { mode });
5800
+ if (options.mode !== undefined) {
5801
+ chmodSync(filePath, options.mode);
5802
+ }
5750
5803
  return { path: filePath, action: exists ? "update" : "create" };
5751
5804
  } catch (error) {
5752
5805
  return {
@@ -5857,7 +5910,10 @@ async function writeFilesWithProgress(files, options = {}) {
5857
5910
  } else if (file.type === "toml" && file.tomlSection) {
5858
5911
  result = appendToToml(file.path, file.tomlSection, file.content, options);
5859
5912
  } else {
5860
- result = writeFile(file.path, file.content, options);
5913
+ result = writeFile(file.path, file.content, {
5914
+ ...options,
5915
+ mode: file.mode
5916
+ });
5861
5917
  }
5862
5918
  results.push(result);
5863
5919
  await new Promise((resolve2) => setTimeout(resolve2, 50));
@@ -5993,6 +6049,22 @@ async function fetchProjects(apiKey, workspaceId) {
5993
6049
  const data = await response.json();
5994
6050
  return data.projects || [];
5995
6051
  }
6052
+ async function resolveProjectSlug(apiKey, slug) {
6053
+ const response = await fetch(`${API_URL}/v1/projects/resolve/${encodeURIComponent(slug)}`, {
6054
+ method: "GET",
6055
+ headers: {
6056
+ "Content-Type": "application/json",
6057
+ "X-API-Key": apiKey
6058
+ }
6059
+ });
6060
+ if (response.status === 404)
6061
+ return null;
6062
+ if (!response.ok) {
6063
+ throw new Error(`Failed to resolve project slug: ${response.status}`);
6064
+ }
6065
+ const data = await response.json();
6066
+ return { workspaceId: data.workspaceId, projectId: data.projectId };
6067
+ }
5996
6068
  async function getAgentFiles(agentId, cwd, installMode = "global") {
5997
6069
  const home = homedir4();
5998
6070
  const files = [];
@@ -6035,6 +6107,24 @@ async function getAgentFiles(agentId, cwd, installMode = "global") {
6035
6107
  throw new Error(`Failed to fetch ${skillFailures.length}/${installableNames.length} skill(s) from /v1/skills:
6036
6108
  ${summary}`);
6037
6109
  }
6110
+ try {
6111
+ const updateCheckFetched = await client3.fetchSkill("hmy-update-check");
6112
+ const actualHash = createHash3("sha256").update(updateCheckFetched.content).digest("hex");
6113
+ if (actualHash !== updateCheckFetched.sha256) {
6114
+ throw new Error(`hmy-update-check integrity check failed: expected ${updateCheckFetched.sha256}, got ${actualHash}`);
6115
+ }
6116
+ files.push({
6117
+ path: join5(home, ".hmy", "bin", "hmy-update-check"),
6118
+ content: updateCheckFetched.content,
6119
+ type: "text",
6120
+ mode: 493
6121
+ });
6122
+ files.push({
6123
+ path: join5(home, ".hmy", "VERSION"),
6124
+ content: versionInfo.version,
6125
+ type: "text"
6126
+ });
6127
+ } catch {}
6038
6128
  break;
6039
6129
  }
6040
6130
  case "codex": {
@@ -6227,7 +6317,7 @@ async function runSetup(options = {}) {
6227
6317
  let needsApiKey = !alreadyConfigured;
6228
6318
  let needsSkills = !skillsStatus.installed || options.force;
6229
6319
  let needsContext = !hasContext && !options.skipContext;
6230
- if (options.workspaceId || options.projectId) {
6320
+ if (options.workspaceId || options.projectId || options.projectSlug) {
6231
6321
  needsContext = true;
6232
6322
  }
6233
6323
  let apiKey = options.apiKey || existingConfig.apiKey;
@@ -6449,6 +6539,21 @@ async function runSetup(options = {}) {
6449
6539
  let selectedProjectId = selectedProjectIdFromSignup || options.projectId;
6450
6540
  let selectedWorkspaceName = selectedWorkspaceNameFromSignup;
6451
6541
  let selectedProjectName = selectedProjectNameFromSignup;
6542
+ if (options.projectSlug && apiKey && (!selectedWorkspaceId || !selectedProjectId)) {
6543
+ spinner3.start(`Resolving project slug "${options.projectSlug}"...`);
6544
+ try {
6545
+ const resolved = await resolveProjectSlug(apiKey, options.projectSlug);
6546
+ if (resolved) {
6547
+ selectedWorkspaceId = selectedWorkspaceId || resolved.workspaceId;
6548
+ selectedProjectId = selectedProjectId || resolved.projectId;
6549
+ spinner3.stop(colors.success(`Resolved "${options.projectSlug}"`));
6550
+ } else {
6551
+ spinner3.stop(colors.warning(`No project found for slug "${options.projectSlug}"`));
6552
+ }
6553
+ } catch (error) {
6554
+ spinner3.stop(colors.warning(`Could not resolve slug: ${error instanceof Error ? error.message : "unknown error"}`));
6555
+ }
6556
+ }
6452
6557
  if (createdNewAccount) {
6453
6558
  needsContext = false;
6454
6559
  }
@@ -6775,13 +6880,14 @@ program.command("reset").description("Remove stored configuration").action(() =>
6775
6880
  console.log(`
6776
6881
  To reconfigure, run: npx @gethmy/mcp setup`);
6777
6882
  });
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) => {
6883
+ 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
6884
  await runSetup({
6780
6885
  force: options.force,
6781
6886
  apiKey: options.apiKey,
6782
6887
  userEmail: options.email,
6783
6888
  agents: options.agents,
6784
6889
  installMode: options.global ? "global" : options.local ? "local" : undefined,
6890
+ projectSlug: slug,
6785
6891
  workspaceId: options.workspace,
6786
6892
  projectId: options.project,
6787
6893
  skipContext: options.skipContext,
package/dist/index.js CHANGED
@@ -1475,6 +1475,51 @@ class HarmonyApiClient {
1475
1475
  async updateMemoryEntity(entityId, updates) {
1476
1476
  return this.request("PUT", `/memory/entities/${entityId}`, updates);
1477
1477
  }
1478
+ async harmonyRecall(options) {
1479
+ const fetchLimit = Math.max(options.topK ?? 3, 50);
1480
+ let entities = [];
1481
+ if (options.query) {
1482
+ const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
1483
+ project_id: options.projectId,
1484
+ type: options.type?.length === 1 ? options.type[0] : undefined,
1485
+ limit: fetchLimit
1486
+ });
1487
+ entities = search.entities ?? [];
1488
+ } else {
1489
+ const list = await this.listMemoryEntities({
1490
+ workspace_id: options.workspaceId,
1491
+ project_id: options.projectId,
1492
+ scope: options.scope,
1493
+ type: options.type?.length === 1 ? options.type[0] : undefined,
1494
+ tags: options.tags,
1495
+ min_confidence: options.minConfidence,
1496
+ limit: fetchLimit
1497
+ });
1498
+ entities = list.entities ?? [];
1499
+ }
1500
+ if (options.type && options.type.length > 1) {
1501
+ const allowed = new Set(options.type);
1502
+ entities = entities.filter((e) => allowed.has(e.type));
1503
+ }
1504
+ if (options.memory_tier) {
1505
+ entities = entities.filter((e) => e.memory_tier === options.memory_tier);
1506
+ }
1507
+ if (options.scope) {
1508
+ entities = entities.filter((e) => e.scope === options.scope);
1509
+ }
1510
+ if (options.tags?.length) {
1511
+ const wanted = new Set(options.tags);
1512
+ entities = entities.filter((e) => (e.tags ?? []).some((t) => wanted.has(t)));
1513
+ }
1514
+ if (typeof options.minConfidence === "number") {
1515
+ const threshold = options.minConfidence;
1516
+ entities = entities.filter((e) => typeof e.confidence === "number" && e.confidence >= threshold);
1517
+ }
1518
+ if (options.topK !== undefined) {
1519
+ entities = entities.slice(0, options.topK);
1520
+ }
1521
+ return { entities };
1522
+ }
1478
1523
  async deleteMemoryEntity(entityId) {
1479
1524
  return this.request("DELETE", `/memory/entities/${entityId}`);
1480
1525
  }
@@ -1781,11 +1826,8 @@ function resolveAgentIdentity(info) {
1781
1826
  var AUTO_START_TRIGGERS = new Set([
1782
1827
  "harmony_generate_prompt",
1783
1828
  "harmony_update_card",
1784
- "harmony_move_card",
1785
1829
  "harmony_create_subtask",
1786
- "harmony_toggle_subtask",
1787
- "harmony_add_label_to_card",
1788
- "harmony_remove_label_from_card"
1830
+ "harmony_toggle_subtask"
1789
1831
  ]);
1790
1832
  var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
1791
1833
  var CHECK_INTERVAL_MS = 60 * 1000;
@@ -1082,6 +1082,51 @@ class HarmonyApiClient {
1082
1082
  async updateMemoryEntity(entityId, updates) {
1083
1083
  return this.request("PUT", `/memory/entities/${entityId}`, updates);
1084
1084
  }
1085
+ async harmonyRecall(options) {
1086
+ const fetchLimit = Math.max(options.topK ?? 3, 50);
1087
+ let entities = [];
1088
+ if (options.query) {
1089
+ const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
1090
+ project_id: options.projectId,
1091
+ type: options.type?.length === 1 ? options.type[0] : undefined,
1092
+ limit: fetchLimit
1093
+ });
1094
+ entities = search.entities ?? [];
1095
+ } else {
1096
+ const list = await this.listMemoryEntities({
1097
+ workspace_id: options.workspaceId,
1098
+ project_id: options.projectId,
1099
+ scope: options.scope,
1100
+ type: options.type?.length === 1 ? options.type[0] : undefined,
1101
+ tags: options.tags,
1102
+ min_confidence: options.minConfidence,
1103
+ limit: fetchLimit
1104
+ });
1105
+ entities = list.entities ?? [];
1106
+ }
1107
+ if (options.type && options.type.length > 1) {
1108
+ const allowed = new Set(options.type);
1109
+ entities = entities.filter((e) => allowed.has(e.type));
1110
+ }
1111
+ if (options.memory_tier) {
1112
+ entities = entities.filter((e) => e.memory_tier === options.memory_tier);
1113
+ }
1114
+ if (options.scope) {
1115
+ entities = entities.filter((e) => e.scope === options.scope);
1116
+ }
1117
+ if (options.tags?.length) {
1118
+ const wanted = new Set(options.tags);
1119
+ entities = entities.filter((e) => (e.tags ?? []).some((t) => wanted.has(t)));
1120
+ }
1121
+ if (typeof options.minConfidence === "number") {
1122
+ const threshold = options.minConfidence;
1123
+ entities = entities.filter((e) => typeof e.confidence === "number" && e.confidence >= threshold);
1124
+ }
1125
+ if (options.topK !== undefined) {
1126
+ entities = entities.slice(0, options.topK);
1127
+ }
1128
+ return { entities };
1129
+ }
1085
1130
  async deleteMemoryEntity(entityId) {
1086
1131
  return this.request("DELETE", `/memory/entities/${entityId}`);
1087
1132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.5.6",
3
+ "version": "2.6.0",
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/api-client.ts CHANGED
@@ -743,15 +743,101 @@ export class HarmonyApiClient {
743
743
  scope?: string;
744
744
  type?: string;
745
745
  memory_tier?: string;
746
- // AGP lifecycle fields. Backend may not yet whitelist these — extra keys
747
- // are dropped server-side, leaving the call as a no-op for those fields.
746
+ // Supersede semantics used by Phase 1.5 review-reject back-fill to
747
+ // tombstone the original implement episode without hard-deleting it.
748
+ // The backend sets superseded_at automatically when superseded_by lands.
748
749
  superseded_by?: string | null;
750
+ superseded_at?: string | null;
749
751
  version?: number;
750
752
  },
751
753
  ): Promise<{ entity: unknown; warnings?: string[] }> {
752
754
  return this.request("PUT", `/memory/entities/${entityId}`, updates);
753
755
  }
754
756
 
757
+ /**
758
+ * Retrieve memories filtered by type/tier/scope, optionally ranked by a
759
+ * free-text query. Wraps `searchMemoryEntities` (when a query is given)
760
+ * or `listMemoryEntities` and applies client-side filters that the REST
761
+ * surface doesn't natively expose (multi-type, memory_tier).
762
+ *
763
+ * Used by the agent daemon's read hook to surface similar past episodes
764
+ * before building a new task prompt (Phase 1.5).
765
+ */
766
+ async harmonyRecall(options: {
767
+ workspaceId: string;
768
+ projectId?: string;
769
+ query?: string;
770
+ type?: string[];
771
+ memory_tier?: string;
772
+ scope?: string;
773
+ tags?: string[];
774
+ minConfidence?: number;
775
+ topK?: number;
776
+ }): Promise<{ entities: unknown[] }> {
777
+ // Over-fetch beyond topK so client-side filters (multi-type, memory_tier,
778
+ // tags) have headroom — matches the MCP server's recall path (server.ts).
779
+ const fetchLimit = Math.max(options.topK ?? 3, 50);
780
+ let entities: Array<Record<string, unknown>> = [];
781
+
782
+ if (options.query) {
783
+ // searchMemoryEntities accepts a single type — refine client-side
784
+ // when the caller passed multiple.
785
+ const search = await this.searchMemoryEntities(
786
+ options.workspaceId,
787
+ options.query,
788
+ {
789
+ project_id: options.projectId,
790
+ type: options.type?.length === 1 ? options.type[0] : undefined,
791
+ limit: fetchLimit,
792
+ },
793
+ );
794
+ entities = (search.entities ?? []) as Array<Record<string, unknown>>;
795
+ } else {
796
+ const list = await this.listMemoryEntities({
797
+ workspace_id: options.workspaceId,
798
+ project_id: options.projectId,
799
+ scope: options.scope,
800
+ type: options.type?.length === 1 ? options.type[0] : undefined,
801
+ tags: options.tags,
802
+ min_confidence: options.minConfidence,
803
+ limit: fetchLimit,
804
+ });
805
+ entities = (list.entities ?? []) as Array<Record<string, unknown>>;
806
+ }
807
+
808
+ // Client-side filters: REST surface lacks multi-type and memory_tier.
809
+ if (options.type && options.type.length > 1) {
810
+ const allowed = new Set(options.type);
811
+ entities = entities.filter((e) => allowed.has(e.type as string));
812
+ }
813
+ if (options.memory_tier) {
814
+ entities = entities.filter((e) => e.memory_tier === options.memory_tier);
815
+ }
816
+ if (options.scope) {
817
+ entities = entities.filter((e) => e.scope === options.scope);
818
+ }
819
+ if (options.tags?.length) {
820
+ const wanted = new Set(options.tags);
821
+ entities = entities.filter((e) =>
822
+ ((e.tags as string[]) ?? []).some((t) => wanted.has(t)),
823
+ );
824
+ }
825
+ if (typeof options.minConfidence === "number") {
826
+ const threshold = options.minConfidence;
827
+ entities = entities.filter(
828
+ (e) =>
829
+ typeof e.confidence === "number" &&
830
+ (e.confidence as number) >= threshold,
831
+ );
832
+ }
833
+
834
+ if (options.topK !== undefined) {
835
+ entities = entities.slice(0, options.topK);
836
+ }
837
+
838
+ return { entities };
839
+ }
840
+
755
841
  async deleteMemoryEntity(entityId: string): Promise<{ success: boolean }> {
756
842
  return this.request("DELETE", `/memory/entities/${entityId}`);
757
843
  }
@@ -65,15 +65,19 @@ export function resolveAgentIdentity(info: ClientInfo | null): {
65
65
  return { agentIdentifier: key, agentName: displayName };
66
66
  }
67
67
 
68
- /** Tools that trigger auto-start of a session */
68
+ /**
69
+ * Tools that trigger auto-start of a session.
70
+ *
71
+ * Restricted to tools that signal real work on a card. Board-management ops
72
+ * (move, label add/remove) are excluded — they're routinely used for triage
73
+ * and would create false-positive sessions whose side effect (the auto-added
74
+ * `agent` label on the card) confuses both UI and humans.
75
+ */
69
76
  export const AUTO_START_TRIGGERS = new Set([
70
77
  "harmony_generate_prompt",
71
78
  "harmony_update_card",
72
- "harmony_move_card",
73
79
  "harmony_create_subtask",
74
80
  "harmony_toggle_subtask",
75
- "harmony_add_label_to_card",
76
- "harmony_remove_label_from_card",
77
81
  ]);
78
82
 
79
83
  export const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
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/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);