@gethmy/mcp 2.23.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.23.0",
3
+ "version": "2.24.0",
4
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"
package/src/api-client.ts CHANGED
@@ -2148,6 +2148,9 @@ export class HarmonyApiClient {
2148
2148
  description?: string;
2149
2149
  steps?: unknown;
2150
2150
  triggerType?: string;
2151
+ /** Auto-bind rule (#892); validated server-side, passed through as-is. */
2152
+ autoBind?: unknown;
2153
+ catalogId?: string;
2151
2154
  }): Promise<{ playbook: unknown }> {
2152
2155
  return this.request("POST", "/playbooks", data);
2153
2156
  }
@@ -2160,6 +2163,9 @@ export class HarmonyApiClient {
2160
2163
  steps?: unknown;
2161
2164
  enabled?: boolean;
2162
2165
  state?: string;
2166
+ triggerType?: string;
2167
+ /** `null` clears the rule; absent leaves it untouched (#892). */
2168
+ autoBind?: unknown;
2163
2169
  },
2164
2170
  ): Promise<{ playbook: unknown }> {
2165
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,
@@ -85,7 +86,9 @@ program
85
86
  console.log("\nContext:");
86
87
 
87
88
  if (hasLocal) {
88
- 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()}`);
89
92
  console.log(
90
93
  ` Workspace: ${localConfig?.workspaceId || "(not set)"}`,
91
94
  );
@@ -103,16 +106,14 @@ program
103
106
  // Show effective (active) context
104
107
  const effectiveWorkspace = getActiveWorkspaceId();
105
108
  const effectiveProject = getActiveProjectId();
106
- const wsSource = localConfig?.workspaceId
107
- ? "local"
108
- : globalConfig.activeWorkspaceId
109
- ? "global"
110
- : "";
111
- const projSource = localConfig?.projectId
112
- ? "local"
113
- : globalConfig.activeProjectId
114
- ? "global"
115
- : "";
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 : "";
116
117
 
117
118
  console.log("\n Active (effective):");
118
119
  console.log(
@@ -121,6 +122,14 @@ program
121
122
  console.log(
122
123
  ` Project: ${effectiveProject || "(not set)"}${projSource ? ` ← ${projSource}` : ""}`,
123
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
+ }
124
133
  } else {
125
134
  console.log("Status: Not configured\n");
126
135
  console.log("Run: npx @gethmy/mcp setup");