@gethmy/mcp 2.22.0 → 2.24.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.
@@ -17,7 +17,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  // src/config.ts
18
18
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
19
  import { homedir } from "node:os";
20
- import { join } from "node:path";
20
+ import { dirname, join, parse, resolve } from "node:path";
21
21
  function getConfigDir() {
22
22
  return join(homedir(), ".harmony-mcp");
23
23
  }
@@ -27,6 +27,22 @@ function getConfigPath() {
27
27
  function getLocalConfigPath(cwd) {
28
28
  return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
29
29
  }
30
+ function findLocalConfigPath(cwd) {
31
+ const home = resolve(homedir());
32
+ let dir = resolve(cwd || process.cwd());
33
+ const { root } = parse(dir);
34
+ for (;; ) {
35
+ if (dir !== home && dir !== root) {
36
+ const candidate = join(dir, LOCAL_CONFIG_FILENAME);
37
+ if (existsSync(candidate))
38
+ return candidate;
39
+ }
40
+ const parent = dirname(dir);
41
+ if (parent === dir)
42
+ return null;
43
+ dir = parent;
44
+ }
45
+ }
30
46
  function emptyConfig() {
31
47
  return {
32
48
  apiKey: null,
@@ -78,8 +94,8 @@ function saveConfig(config) {
78
94
  });
79
95
  }
80
96
  function loadLocalConfig(cwd) {
81
- const localConfigPath = getLocalConfigPath(cwd);
82
- if (!existsSync(localConfigPath)) {
97
+ const localConfigPath = findLocalConfigPath(cwd);
98
+ if (localConfigPath === null) {
83
99
  return null;
84
100
  }
85
101
  try {
@@ -94,7 +110,7 @@ function loadLocalConfig(cwd) {
94
110
  }
95
111
  }
96
112
  function saveLocalConfig(config, cwd) {
97
- const localConfigPath = getLocalConfigPath(cwd);
113
+ const localConfigPath = findLocalConfigPath(cwd) ?? getLocalConfigPath(cwd);
98
114
  const existingConfig = loadLocalConfig(cwd) || {
99
115
  workspaceId: null,
100
116
  projectId: null
@@ -108,7 +124,7 @@ function saveLocalConfig(config, cwd) {
108
124
  writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
109
125
  }
110
126
  function hasLocalConfig(cwd) {
111
- return existsSync(getLocalConfigPath(cwd));
127
+ return findLocalConfigPath(cwd) !== null;
112
128
  }
113
129
  function getActiveCredential() {
114
130
  const config = loadConfig();
@@ -133,33 +149,69 @@ function getUserEmail() {
133
149
  function setUserEmail(email) {
134
150
  saveConfig({ userEmail: email });
135
151
  }
136
- function setActiveWorkspace(workspaceId, options) {
137
- if (options?.local) {
138
- saveLocalConfig({ workspaceId }, options.cwd);
139
- } else {
140
- saveConfig({ activeWorkspaceId: workspaceId });
152
+ function setActiveContext(context, options) {
153
+ if (options?.global) {
154
+ saveConfig({
155
+ activeWorkspaceId: context.workspaceId,
156
+ activeProjectId: context.projectId
157
+ });
158
+ return;
141
159
  }
142
- }
143
- function setActiveProject(projectId, options) {
144
- if (options?.local) {
145
- saveLocalConfig({ projectId }, options.cwd);
160
+ const localPath = findLocalConfigPath(options?.cwd);
161
+ if (options?.local || localPath !== null) {
162
+ saveLocalConfig({ workspaceId: context.workspaceId, projectId: context.projectId }, options?.cwd);
146
163
  } else {
147
- saveConfig({ activeProjectId: projectId });
164
+ saveConfig({
165
+ activeWorkspaceId: context.workspaceId,
166
+ activeProjectId: context.projectId
167
+ });
148
168
  }
149
169
  }
150
- function getActiveWorkspaceId(cwd) {
170
+ function setActiveWorkspace(workspaceId, options) {
171
+ const currentWorkspaceId = getActiveWorkspaceId(options?.cwd);
172
+ const keepProject = currentWorkspaceId === workspaceId;
173
+ setActiveContext({
174
+ workspaceId,
175
+ projectId: keepProject ? getActiveProjectId(options?.cwd) : null
176
+ }, options);
177
+ }
178
+ function readActiveContext(cwd) {
151
179
  const localConfig = loadLocalConfig(cwd);
152
- if (localConfig?.workspaceId) {
153
- return localConfig.workspaceId;
180
+ if (localConfig) {
181
+ return {
182
+ workspaceId: localConfig.workspaceId ?? null,
183
+ projectId: localConfig.projectId ?? null
184
+ };
154
185
  }
155
- return loadConfig().activeWorkspaceId;
186
+ const globalConfig = loadConfig();
187
+ return {
188
+ workspaceId: globalConfig.activeWorkspaceId,
189
+ projectId: globalConfig.activeProjectId
190
+ };
191
+ }
192
+ function getActiveWorkspaceId(cwd) {
193
+ return readActiveContext(cwd).workspaceId;
156
194
  }
157
195
  function getActiveProjectId(cwd) {
158
- const localConfig = loadLocalConfig(cwd);
159
- if (localConfig?.projectId) {
160
- return localConfig.projectId;
196
+ return readActiveContext(cwd).projectId;
197
+ }
198
+ function getActiveContext(cwd) {
199
+ return describeActiveContext({
200
+ projectId: getActiveProjectId(cwd),
201
+ workspaceId: getActiveWorkspaceId(cwd)
202
+ });
203
+ }
204
+ function describeActiveContext(context) {
205
+ const { projectId, workspaceId } = context;
206
+ if (projectId && !workspaceId) {
207
+ return {
208
+ projectId,
209
+ workspaceId,
210
+ consistent: false,
211
+ note: `An active project (${projectId}) is set with no active workspace, so ` + "workspace-scoped tools cannot resolve one from it. Re-set it with " + "harmony_set_project_context, or pass workspaceId explicitly."
212
+ };
161
213
  }
162
- return loadConfig().activeProjectId;
214
+ return { projectId, workspaceId, consistent: true, note: null };
163
215
  }
164
216
  function isConfigured() {
165
217
  const config = loadConfig();
@@ -214,7 +266,7 @@ init_config();
214
266
  export {
215
267
  setUserEmail,
216
268
  setActiveWorkspace,
217
- setActiveProject,
269
+ setActiveContext,
218
270
  saveLocalConfig,
219
271
  saveConfig,
220
272
  loadLocalConfig,
@@ -232,5 +284,8 @@ export {
232
284
  getActiveWorkspaceId,
233
285
  getActiveProjectId,
234
286
  getActiveCredential,
287
+ getActiveContext,
288
+ findLocalConfigPath,
289
+ describeActiveContext,
235
290
  areSkillsInstalled
236
291
  };
@@ -17,7 +17,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  // src/config.ts
18
18
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
19
  import { homedir } from "node:os";
20
- import { join } from "node:path";
20
+ import { dirname, join, parse, resolve } from "node:path";
21
21
  function getConfigDir() {
22
22
  return join(homedir(), ".harmony-mcp");
23
23
  }
@@ -27,6 +27,22 @@ function getConfigPath() {
27
27
  function getLocalConfigPath(cwd) {
28
28
  return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
29
29
  }
30
+ function findLocalConfigPath(cwd) {
31
+ const home = resolve(homedir());
32
+ let dir = resolve(cwd || process.cwd());
33
+ const { root } = parse(dir);
34
+ for (;; ) {
35
+ if (dir !== home && dir !== root) {
36
+ const candidate = join(dir, LOCAL_CONFIG_FILENAME);
37
+ if (existsSync(candidate))
38
+ return candidate;
39
+ }
40
+ const parent = dirname(dir);
41
+ if (parent === dir)
42
+ return null;
43
+ dir = parent;
44
+ }
45
+ }
30
46
  function emptyConfig() {
31
47
  return {
32
48
  apiKey: null,
@@ -78,8 +94,8 @@ function saveConfig(config) {
78
94
  });
79
95
  }
80
96
  function loadLocalConfig(cwd) {
81
- const localConfigPath = getLocalConfigPath(cwd);
82
- if (!existsSync(localConfigPath)) {
97
+ const localConfigPath = findLocalConfigPath(cwd);
98
+ if (localConfigPath === null) {
83
99
  return null;
84
100
  }
85
101
  try {
@@ -94,7 +110,7 @@ function loadLocalConfig(cwd) {
94
110
  }
95
111
  }
96
112
  function saveLocalConfig(config, cwd) {
97
- const localConfigPath = getLocalConfigPath(cwd);
113
+ const localConfigPath = findLocalConfigPath(cwd) ?? getLocalConfigPath(cwd);
98
114
  const existingConfig = loadLocalConfig(cwd) || {
99
115
  workspaceId: null,
100
116
  projectId: null
@@ -108,7 +124,7 @@ function saveLocalConfig(config, cwd) {
108
124
  writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
109
125
  }
110
126
  function hasLocalConfig(cwd) {
111
- return existsSync(getLocalConfigPath(cwd));
127
+ return findLocalConfigPath(cwd) !== null;
112
128
  }
113
129
  function getActiveCredential() {
114
130
  const config = loadConfig();
@@ -133,33 +149,69 @@ function getUserEmail() {
133
149
  function setUserEmail(email) {
134
150
  saveConfig({ userEmail: email });
135
151
  }
136
- function setActiveWorkspace(workspaceId, options) {
137
- if (options?.local) {
138
- saveLocalConfig({ workspaceId }, options.cwd);
139
- } else {
140
- saveConfig({ activeWorkspaceId: workspaceId });
152
+ function setActiveContext(context, options) {
153
+ if (options?.global) {
154
+ saveConfig({
155
+ activeWorkspaceId: context.workspaceId,
156
+ activeProjectId: context.projectId
157
+ });
158
+ return;
141
159
  }
142
- }
143
- function setActiveProject(projectId, options) {
144
- if (options?.local) {
145
- saveLocalConfig({ projectId }, options.cwd);
160
+ const localPath = findLocalConfigPath(options?.cwd);
161
+ if (options?.local || localPath !== null) {
162
+ saveLocalConfig({ workspaceId: context.workspaceId, projectId: context.projectId }, options?.cwd);
146
163
  } else {
147
- saveConfig({ activeProjectId: projectId });
164
+ saveConfig({
165
+ activeWorkspaceId: context.workspaceId,
166
+ activeProjectId: context.projectId
167
+ });
148
168
  }
149
169
  }
150
- function getActiveWorkspaceId(cwd) {
170
+ function setActiveWorkspace(workspaceId, options) {
171
+ const currentWorkspaceId = getActiveWorkspaceId(options?.cwd);
172
+ const keepProject = currentWorkspaceId === workspaceId;
173
+ setActiveContext({
174
+ workspaceId,
175
+ projectId: keepProject ? getActiveProjectId(options?.cwd) : null
176
+ }, options);
177
+ }
178
+ function readActiveContext(cwd) {
151
179
  const localConfig = loadLocalConfig(cwd);
152
- if (localConfig?.workspaceId) {
153
- return localConfig.workspaceId;
180
+ if (localConfig) {
181
+ return {
182
+ workspaceId: localConfig.workspaceId ?? null,
183
+ projectId: localConfig.projectId ?? null
184
+ };
154
185
  }
155
- return loadConfig().activeWorkspaceId;
186
+ const globalConfig = loadConfig();
187
+ return {
188
+ workspaceId: globalConfig.activeWorkspaceId,
189
+ projectId: globalConfig.activeProjectId
190
+ };
191
+ }
192
+ function getActiveWorkspaceId(cwd) {
193
+ return readActiveContext(cwd).workspaceId;
156
194
  }
157
195
  function getActiveProjectId(cwd) {
158
- const localConfig = loadLocalConfig(cwd);
159
- if (localConfig?.projectId) {
160
- return localConfig.projectId;
196
+ return readActiveContext(cwd).projectId;
197
+ }
198
+ function getActiveContext(cwd) {
199
+ return describeActiveContext({
200
+ projectId: getActiveProjectId(cwd),
201
+ workspaceId: getActiveWorkspaceId(cwd)
202
+ });
203
+ }
204
+ function describeActiveContext(context) {
205
+ const { projectId, workspaceId } = context;
206
+ if (projectId && !workspaceId) {
207
+ return {
208
+ projectId,
209
+ workspaceId,
210
+ consistent: false,
211
+ note: `An active project (${projectId}) is set with no active workspace, so ` + "workspace-scoped tools cannot resolve one from it. Re-set it with " + "harmony_set_project_context, or pass workspaceId explicitly."
212
+ };
161
213
  }
162
- return loadConfig().activeProjectId;
214
+ return { projectId, workspaceId, consistent: true, note: null };
163
215
  }
164
216
  function isConfigured() {
165
217
  const config = loadConfig();
@@ -238,7 +290,7 @@ function lockPath() {
238
290
  return join2(getConfigDir(), LOCK_FILENAME);
239
291
  }
240
292
  function sleep(ms) {
241
- return new Promise((resolve) => setTimeout(resolve, ms));
293
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
242
294
  }
243
295
  async function withRefreshLock(fn) {
244
296
  const path = lockPath();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.22.0",
4
- "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
3
+ "version": "2.24.0",
4
+ "description": "MCP server for Harmony, the shared surface for human–agent teams agents claim cards, report progress, and move work on your board.",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
package/src/api-client.ts CHANGED
@@ -1088,6 +1088,15 @@ export class HarmonyApiClient {
1088
1088
  * NULL until the terminal cost frame reported the observed model.
1089
1089
  */
1090
1090
  modelName?: string;
1091
+ /**
1092
+ * Who is calling (Task 5, "the drivers"): `daemon` (the local agent
1093
+ * daemon), `interactive` (a human-driven `/hmy` session), or `script`
1094
+ * (e.g. the e2e harness). Names the holder in a 409 conflict message so
1095
+ * a second caller knows who to wait on. Optional and nullable
1096
+ * server-side — omitting it is unchanged (pre-existing callers keep
1097
+ * working); the MCP tool dispatch defaults to `interactive`.
1098
+ */
1099
+ driver?: "daemon" | "interactive" | "script";
1091
1100
  },
1092
1101
  ): Promise<{
1093
1102
  session: unknown;
@@ -2139,6 +2148,9 @@ export class HarmonyApiClient {
2139
2148
  description?: string;
2140
2149
  steps?: unknown;
2141
2150
  triggerType?: string;
2151
+ /** Auto-bind rule (#892); validated server-side, passed through as-is. */
2152
+ autoBind?: unknown;
2153
+ catalogId?: string;
2142
2154
  }): Promise<{ playbook: unknown }> {
2143
2155
  return this.request("POST", "/playbooks", data);
2144
2156
  }
@@ -2151,6 +2163,9 @@ export class HarmonyApiClient {
2151
2163
  steps?: unknown;
2152
2164
  enabled?: boolean;
2153
2165
  state?: string;
2166
+ triggerType?: string;
2167
+ /** `null` clears the rule; absent leaves it untouched (#892). */
2168
+ autoBind?: unknown;
2154
2169
  },
2155
2170
  ): Promise<{ playbook: unknown }> {
2156
2171
  return this.request("PATCH", `/playbooks/${playbookId}`, updates);
package/src/cli.ts CHANGED
@@ -3,10 +3,11 @@ import { createRequire } from "node:module";
3
3
  import { program } from "commander";
4
4
  import {
5
5
  areSkillsInstalled,
6
+ findLocalConfigPath,
7
+ getActiveContext,
6
8
  getActiveProjectId,
7
9
  getActiveWorkspaceId,
8
10
  getConfigPath,
9
- getLocalConfigPath,
10
11
  hasLocalConfig,
11
12
  isConfigured,
12
13
  loadConfig,
@@ -22,7 +23,9 @@ const { version } = require("../package.json");
22
23
 
23
24
  program
24
25
  .name("@gethmy/mcp")
25
- .description("MCP server for Harmony Kanban board")
26
+ .description(
27
+ "MCP server for Harmony — the shared surface for human–agent teams",
28
+ )
26
29
  .version(version);
27
30
 
28
31
  program
@@ -83,7 +86,9 @@ program
83
86
  console.log("\nContext:");
84
87
 
85
88
  if (hasLocal) {
86
- console.log(` Local config: ${getLocalConfigPath()}`);
89
+ // The file that is actually READ, which since #893 may be an ancestor's
90
+ // — printing the cwd path would name a file that does not exist.
91
+ console.log(` Local config: ${findLocalConfigPath()}`);
87
92
  console.log(
88
93
  ` Workspace: ${localConfig?.workspaceId || "(not set)"}`,
89
94
  );
@@ -101,16 +106,14 @@ program
101
106
  // Show effective (active) context
102
107
  const effectiveWorkspace = getActiveWorkspaceId();
103
108
  const effectiveProject = getActiveProjectId();
104
- const wsSource = localConfig?.workspaceId
105
- ? "local"
106
- : globalConfig.activeWorkspaceId
107
- ? "global"
108
- : "";
109
- const projSource = localConfig?.projectId
110
- ? "local"
111
- : globalConfig.activeProjectId
112
- ? "global"
113
- : "";
109
+ // The local file wins as a WHOLE file (#893), so the source is the file,
110
+ // not the field: with a local config in scope BOTH ids come from it, and
111
+ // a key it omits reads as unset rather than falling through to global.
112
+ // Reporting "global" for such a field would describe a fallback that no
113
+ // longer happens.
114
+ const contextSource = hasLocal ? "local" : "global";
115
+ const wsSource = effectiveWorkspace ? contextSource : "";
116
+ const projSource = effectiveProject ? contextSource : "";
114
117
 
115
118
  console.log("\n Active (effective):");
116
119
  console.log(
@@ -119,6 +122,14 @@ program
119
122
  console.log(
120
123
  ` Project: ${effectiveProject || "(not set)"}${projSource ? ` ← ${projSource}` : ""}`,
121
124
  );
125
+
126
+ // Say it out loud when the pair does not hold together (#893). A project
127
+ // with no workspace leaves every workspace-scoped tool guessing, and this
128
+ // status output is where a person looks when one of them misbehaves.
129
+ const report = getActiveContext();
130
+ if (!report.consistent && report.note) {
131
+ console.log(`\n ⚠ ${report.note}`);
132
+ }
122
133
  } else {
123
134
  console.log("Status: Not configured\n");
124
135
  console.log("Run: npx @gethmy/mcp setup");
@@ -146,7 +157,7 @@ program
146
157
 
147
158
  program
148
159
  .command("setup")
149
- .description("Smart setup wizard for Harmony MCP (recommended)")
160
+ .description("Setup wizard for Harmony MCP (recommended)")
150
161
  .argument(
151
162
  "[slug]",
152
163
  "Project slug — resolves to workspace + project in one step (e.g. harmony-6590761b)",