@gethmy/mcp 3.7.0 → 3.9.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.
@@ -15,12 +15,14 @@ var __export = (target, all) => {
15
15
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
16
16
 
17
17
  // src/config.ts
18
+ import { execFileSync } from "node:child_process";
18
19
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
- import { homedir } from "node:os";
20
+ import { homedir, tmpdir } from "node:os";
20
21
  import { dirname, join, parse, resolve } from "node:path";
21
22
  function resetLegacyNoticesForTest() {
22
23
  warnedLegacyConfigDir = false;
23
24
  warnedLegacyLocalPin = false;
25
+ warnedUntrackedLocalPin = false;
24
26
  }
25
27
  function noteLegacyConfigDir(path) {
26
28
  if (warnedLegacyConfigDir)
@@ -32,11 +34,28 @@ function noteLegacyLocalPin(path) {
32
34
  if (warnedLegacyLocalPin)
33
35
  return;
34
36
  warnedLegacyLocalPin = true;
35
- console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} the fallback that finds it is temporary.`);
37
+ console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Run \`harmony-agent doctor --fix\` in this repo to write ` + `${LOCAL_CONFIG_FILENAME}, or rename the file yourself. ` + `The fallback that finds it is temporary.`);
36
38
  }
37
39
  function noteLocalPinRename(from, to) {
38
40
  console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
39
41
  }
42
+ function noteUntrackedLocalPin(path) {
43
+ if (warnedUntrackedLocalPin)
44
+ return;
45
+ warnedUntrackedLocalPin = true;
46
+ console.error(`Harmony: ${path} is ignored by git, so it will not travel with a branch — ` + `a fresh clone, and every worktree the agent daemon cuts, will not have it. ` + `Commit it if you want it to describe this repo everywhere.`);
47
+ }
48
+ function isGitIgnored(path) {
49
+ try {
50
+ execFileSync("git", ["check-ignore", "--quiet", path], {
51
+ cwd: dirname(path),
52
+ stdio: "ignore"
53
+ });
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
40
59
  function getHmyRootDir() {
41
60
  return join(homedir(), CONFIG_DIR_NAME);
42
61
  }
@@ -151,19 +170,96 @@ function saveLocalConfig(config, cwd) {
151
170
  if (foundPath !== null && foundPath !== localConfigPath) {
152
171
  noteLocalPinRename(foundPath, localConfigPath);
153
172
  }
154
- const existingConfig = loadLocalConfig(cwd) || {
155
- workspaceId: null,
156
- projectId: null
157
- };
158
- const newConfig = { ...existingConfig, ...config };
159
- const cleanConfig = {};
160
- if (newConfig.workspaceId)
161
- cleanConfig.workspaceId = newConfig.workspaceId;
162
- if (newConfig.projectId)
163
- cleanConfig.projectId = newConfig.projectId;
164
- writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
173
+ const existing = readRawLocalConfig(foundPath ?? localConfigPath);
174
+ const merged = { ...existing };
175
+ if ("workspaceId" in config) {
176
+ if (config.workspaceId)
177
+ merged.workspaceId = config.workspaceId;
178
+ else
179
+ delete merged.workspaceId;
180
+ }
181
+ if ("projectId" in config) {
182
+ if (config.projectId)
183
+ merged.projectId = config.projectId;
184
+ else
185
+ delete merged.projectId;
186
+ }
187
+ writeFileSync(localConfigPath, `${JSON.stringify(merged, null, localIndent(localConfigPath))}
188
+ `);
189
+ if (isGitIgnored(localConfigPath))
190
+ noteUntrackedLocalPin(localConfigPath);
165
191
  return localConfigPath;
166
192
  }
193
+ function readRawLocalConfig(path) {
194
+ let text;
195
+ try {
196
+ text = readFileSync(path, "utf-8");
197
+ } catch {
198
+ return {};
199
+ }
200
+ try {
201
+ const parsed = JSON.parse(text);
202
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
203
+ return parsed;
204
+ }
205
+ } catch {}
206
+ noteUnparsableLocalPin(path, text);
207
+ return {};
208
+ }
209
+ function noteUnparsableLocalPin(path, contents) {
210
+ let backup = null;
211
+ try {
212
+ backup = join(tmpdir(), `hmy-pin-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json.bak`);
213
+ writeFileSync(backup, contents);
214
+ } catch {
215
+ backup = null;
216
+ }
217
+ console.error(`Harmony: ${path} could not be parsed as JSON, so the pin write REPLACED it. ` + (backup ? `The previous contents are in ${backup}.` : "The previous contents could not be backed up and are gone.") + ` If it carried a "commands" block, re-add it.`);
218
+ }
219
+ function localIndent(configPath) {
220
+ const root = dirname(configPath);
221
+ for (const name of ["biome.json", "biome.jsonc"]) {
222
+ const parsed = readJsonish(join(root, name));
223
+ if (!parsed || typeof parsed !== "object")
224
+ continue;
225
+ const formatter = parsed.formatter;
226
+ if (formatter?.indentStyle === "space") {
227
+ const width = formatter.indentWidth;
228
+ return typeof width === "number" && width > 0 ? width : 2;
229
+ }
230
+ return "\t";
231
+ }
232
+ const editorconfig = readTextSafely(join(root, ".editorconfig"));
233
+ if (editorconfig) {
234
+ if (/^\s*indent_style\s*=\s*tab\s*$/im.test(editorconfig))
235
+ return "\t";
236
+ const size = editorconfig.match(/^\s*indent_size\s*=\s*(\d+)\s*$/im);
237
+ if (size) {
238
+ const width = Number(size[1]);
239
+ if (width > 0)
240
+ return width;
241
+ }
242
+ }
243
+ return 2;
244
+ }
245
+ function readJsonish(path) {
246
+ const text = readTextSafely(path);
247
+ if (text === null)
248
+ return null;
249
+ try {
250
+ const stripped = text.replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1");
251
+ return JSON.parse(stripped);
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+ function readTextSafely(path) {
257
+ try {
258
+ return readFileSync(path, "utf-8");
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
167
263
  function hasLocalConfig(cwd) {
168
264
  return findLocalConfigPath(cwd) !== null;
169
265
  }
@@ -300,7 +396,7 @@ function getMemoryDir() {
300
396
  return config.memoryDir;
301
397
  return join(homedir(), ".harmony", "memory");
302
398
  }
303
- var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false;
399
+ var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false, warnedUntrackedLocalPin = false;
304
400
  var init_config = () => {};
305
401
 
306
402
  // src/oauth-login.ts
@@ -864,7 +960,7 @@ var init_prompt_builder = __esm(() => {
864
960
  };
865
961
  VARIANT_INSTRUCTIONS = {
866
962
  analysis: `ANALYSIS MODE: Analyze this task thoroughly. Identify requirements, constraints, edge cases, and potential challenges. Do NOT implement anything yet - focus on understanding and planning.`,
867
- draft: `DRAFT MODE: Create a detailed implementation plan with code structure, key decisions, and approach. Include pseudocode or skeleton code where helpful. This is for review before full implementation.`,
963
+ draft: `DRAFT MODE: Draft the approach for review before implementing. Cover the key decisions with their reasons, the data model and the API contracts, and success criteria a test can check. A short signature or schema sketch is fine wherever an interpretation gap would otherwise remain; function bodies, control flow and test code are not - an implementer transcribes a plan faithfully, defects included.`,
868
964
  execute: `EXECUTE MODE: Implement this task completely. Write production-ready code following best practices. Include necessary tests and documentation.`
869
965
  };
870
966
  });
@@ -1014,6 +1110,60 @@ var REVIEW_DISALLOWED_TOOLS = [
1014
1110
  "mcp__harmony__harmony_delete_subtask",
1015
1111
  "mcp__harmony__harmony_toggle_subtask"
1016
1112
  ];
1113
+ // ../harmony-shared/dist/runEventSanitize.js
1114
+ var REPLACEMENT = "�";
1115
+ function sanitizeRunEventString(value) {
1116
+ let out = "";
1117
+ for (let i = 0;i < value.length; i++) {
1118
+ const code = value.charCodeAt(i);
1119
+ if (code === 0)
1120
+ continue;
1121
+ if (code >= 55296 && code <= 56319) {
1122
+ const next = value.charCodeAt(i + 1);
1123
+ if (next >= 56320 && next <= 57343) {
1124
+ out += value[i] + value[i + 1];
1125
+ i++;
1126
+ continue;
1127
+ }
1128
+ out += REPLACEMENT;
1129
+ continue;
1130
+ }
1131
+ if (code >= 56320 && code <= 57343) {
1132
+ out += REPLACEMENT;
1133
+ continue;
1134
+ }
1135
+ out += value[i];
1136
+ }
1137
+ return out;
1138
+ }
1139
+ function sanitizeRunEventPayload(payload) {
1140
+ return walk(payload, new Map);
1141
+ }
1142
+ function sanitizeRunEventDraft(draft) {
1143
+ return { ...draft, payload: sanitizeRunEventPayload(draft.payload) };
1144
+ }
1145
+ function walk(value, seen) {
1146
+ if (typeof value === "string")
1147
+ return sanitizeRunEventString(value);
1148
+ if (value === null || typeof value !== "object")
1149
+ return value;
1150
+ const already = seen.get(value);
1151
+ if (already !== undefined)
1152
+ return already;
1153
+ if (Array.isArray(value)) {
1154
+ const out2 = [];
1155
+ seen.set(value, out2);
1156
+ for (const entry of value)
1157
+ out2.push(walk(entry, seen));
1158
+ return out2;
1159
+ }
1160
+ const out = {};
1161
+ seen.set(value, out);
1162
+ for (const [key, entry] of Object.entries(value)) {
1163
+ out[sanitizeRunEventString(key)] = walk(entry, seen);
1164
+ }
1165
+ return out;
1166
+ }
1017
1167
  // ../harmony-shared/dist/runRedaction.js
1018
1168
  var REDACTION_MARK = "«redacted»";
1019
1169
  var CONFIG_SCOPED_SEGMENTS = new Set([
@@ -1395,6 +1545,15 @@ class HarmonyApiClient {
1395
1545
  async registerWorkspaceAgent(workspaceId, data) {
1396
1546
  return this.request("POST", `/workspaces/${workspaceId}/agents`, data);
1397
1547
  }
1548
+ async reportAgentConfig(workspaceId, agentId, config) {
1549
+ return this.request("POST", `/workspaces/${workspaceId}/agents/${agentId}/reported-config`, { config });
1550
+ }
1551
+ async getWorkspaceModelConfig(workspaceId) {
1552
+ return this.request("GET", `/workspaces/${workspaceId}/model-config`);
1553
+ }
1554
+ async getModelCatalog() {
1555
+ return this.request("GET", "/model-catalog");
1556
+ }
1398
1557
  async listProjects(workspaceId) {
1399
1558
  return this.request("GET", `/workspaces/${workspaceId}/projects`);
1400
1559
  }
@@ -1543,6 +1702,9 @@ class HarmonyApiClient {
1543
1702
  title
1544
1703
  });
1545
1704
  }
1705
+ async removeExternalLink(cardId, linkId) {
1706
+ return this.request("DELETE", `/cards/${cardId}/external-links/${linkId}`);
1707
+ }
1546
1708
  async uploadArtifact(data) {
1547
1709
  return this.request("POST", "/artifacts", data);
1548
1710
  }
@@ -1635,7 +1797,10 @@ class HarmonyApiClient {
1635
1797
  return this.request("DELETE", `/cards/${cardId}/agent-context`, data);
1636
1798
  }
1637
1799
  async appendAgentRunEvents(cardId, data) {
1638
- return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
1800
+ return this.request("POST", `/cards/${cardId}/agent-run-events`, {
1801
+ ...data,
1802
+ events: data.events.map((event) => sanitizeRunEventDraft(event))
1803
+ });
1639
1804
  }
1640
1805
  async getPendingUserMessages(cardId, sessionId, sinceSeq) {
1641
1806
  return this.request("GET", `/cards/${cardId}/agent-messages?sessionId=${sessionId}&sinceSeq=${sinceSeq}`);
@@ -15,12 +15,14 @@ var __export = (target, all) => {
15
15
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
16
16
 
17
17
  // src/config.ts
18
+ import { execFileSync } from "node:child_process";
18
19
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
- import { homedir } from "node:os";
20
+ import { homedir, tmpdir } from "node:os";
20
21
  import { dirname, join, parse, resolve } from "node:path";
21
22
  function resetLegacyNoticesForTest() {
22
23
  warnedLegacyConfigDir = false;
23
24
  warnedLegacyLocalPin = false;
25
+ warnedUntrackedLocalPin = false;
24
26
  }
25
27
  function noteLegacyConfigDir(path) {
26
28
  if (warnedLegacyConfigDir)
@@ -32,11 +34,28 @@ function noteLegacyLocalPin(path) {
32
34
  if (warnedLegacyLocalPin)
33
35
  return;
34
36
  warnedLegacyLocalPin = true;
35
- console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} the fallback that finds it is temporary.`);
37
+ console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Run \`harmony-agent doctor --fix\` in this repo to write ` + `${LOCAL_CONFIG_FILENAME}, or rename the file yourself. ` + `The fallback that finds it is temporary.`);
36
38
  }
37
39
  function noteLocalPinRename(from, to) {
38
40
  console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
39
41
  }
42
+ function noteUntrackedLocalPin(path) {
43
+ if (warnedUntrackedLocalPin)
44
+ return;
45
+ warnedUntrackedLocalPin = true;
46
+ console.error(`Harmony: ${path} is ignored by git, so it will not travel with a branch — ` + `a fresh clone, and every worktree the agent daemon cuts, will not have it. ` + `Commit it if you want it to describe this repo everywhere.`);
47
+ }
48
+ function isGitIgnored(path) {
49
+ try {
50
+ execFileSync("git", ["check-ignore", "--quiet", path], {
51
+ cwd: dirname(path),
52
+ stdio: "ignore"
53
+ });
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
40
59
  function getHmyRootDir() {
41
60
  return join(homedir(), CONFIG_DIR_NAME);
42
61
  }
@@ -151,19 +170,96 @@ function saveLocalConfig(config, cwd) {
151
170
  if (foundPath !== null && foundPath !== localConfigPath) {
152
171
  noteLocalPinRename(foundPath, localConfigPath);
153
172
  }
154
- const existingConfig = loadLocalConfig(cwd) || {
155
- workspaceId: null,
156
- projectId: null
157
- };
158
- const newConfig = { ...existingConfig, ...config };
159
- const cleanConfig = {};
160
- if (newConfig.workspaceId)
161
- cleanConfig.workspaceId = newConfig.workspaceId;
162
- if (newConfig.projectId)
163
- cleanConfig.projectId = newConfig.projectId;
164
- writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
173
+ const existing = readRawLocalConfig(foundPath ?? localConfigPath);
174
+ const merged = { ...existing };
175
+ if ("workspaceId" in config) {
176
+ if (config.workspaceId)
177
+ merged.workspaceId = config.workspaceId;
178
+ else
179
+ delete merged.workspaceId;
180
+ }
181
+ if ("projectId" in config) {
182
+ if (config.projectId)
183
+ merged.projectId = config.projectId;
184
+ else
185
+ delete merged.projectId;
186
+ }
187
+ writeFileSync(localConfigPath, `${JSON.stringify(merged, null, localIndent(localConfigPath))}
188
+ `);
189
+ if (isGitIgnored(localConfigPath))
190
+ noteUntrackedLocalPin(localConfigPath);
165
191
  return localConfigPath;
166
192
  }
193
+ function readRawLocalConfig(path) {
194
+ let text;
195
+ try {
196
+ text = readFileSync(path, "utf-8");
197
+ } catch {
198
+ return {};
199
+ }
200
+ try {
201
+ const parsed = JSON.parse(text);
202
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
203
+ return parsed;
204
+ }
205
+ } catch {}
206
+ noteUnparsableLocalPin(path, text);
207
+ return {};
208
+ }
209
+ function noteUnparsableLocalPin(path, contents) {
210
+ let backup = null;
211
+ try {
212
+ backup = join(tmpdir(), `hmy-pin-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json.bak`);
213
+ writeFileSync(backup, contents);
214
+ } catch {
215
+ backup = null;
216
+ }
217
+ console.error(`Harmony: ${path} could not be parsed as JSON, so the pin write REPLACED it. ` + (backup ? `The previous contents are in ${backup}.` : "The previous contents could not be backed up and are gone.") + ` If it carried a "commands" block, re-add it.`);
218
+ }
219
+ function localIndent(configPath) {
220
+ const root = dirname(configPath);
221
+ for (const name of ["biome.json", "biome.jsonc"]) {
222
+ const parsed = readJsonish(join(root, name));
223
+ if (!parsed || typeof parsed !== "object")
224
+ continue;
225
+ const formatter = parsed.formatter;
226
+ if (formatter?.indentStyle === "space") {
227
+ const width = formatter.indentWidth;
228
+ return typeof width === "number" && width > 0 ? width : 2;
229
+ }
230
+ return "\t";
231
+ }
232
+ const editorconfig = readTextSafely(join(root, ".editorconfig"));
233
+ if (editorconfig) {
234
+ if (/^\s*indent_style\s*=\s*tab\s*$/im.test(editorconfig))
235
+ return "\t";
236
+ const size = editorconfig.match(/^\s*indent_size\s*=\s*(\d+)\s*$/im);
237
+ if (size) {
238
+ const width = Number(size[1]);
239
+ if (width > 0)
240
+ return width;
241
+ }
242
+ }
243
+ return 2;
244
+ }
245
+ function readJsonish(path) {
246
+ const text = readTextSafely(path);
247
+ if (text === null)
248
+ return null;
249
+ try {
250
+ const stripped = text.replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1");
251
+ return JSON.parse(stripped);
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+ function readTextSafely(path) {
257
+ try {
258
+ return readFileSync(path, "utf-8");
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
167
263
  function hasLocalConfig(cwd) {
168
264
  return findLocalConfigPath(cwd) !== null;
169
265
  }
@@ -300,7 +396,7 @@ function getMemoryDir() {
300
396
  return config.memoryDir;
301
397
  return join(homedir(), ".harmony", "memory");
302
398
  }
303
- var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false;
399
+ var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false, warnedUntrackedLocalPin = false;
304
400
  var init_config = () => {};
305
401
  init_config();
306
402
 
@@ -15,12 +15,14 @@ var __export = (target, all) => {
15
15
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
16
16
 
17
17
  // src/config.ts
18
+ import { execFileSync } from "node:child_process";
18
19
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
- import { homedir } from "node:os";
20
+ import { homedir, tmpdir } from "node:os";
20
21
  import { dirname, join, parse, resolve } from "node:path";
21
22
  function resetLegacyNoticesForTest() {
22
23
  warnedLegacyConfigDir = false;
23
24
  warnedLegacyLocalPin = false;
25
+ warnedUntrackedLocalPin = false;
24
26
  }
25
27
  function noteLegacyConfigDir(path) {
26
28
  if (warnedLegacyConfigDir)
@@ -32,11 +34,28 @@ function noteLegacyLocalPin(path) {
32
34
  if (warnedLegacyLocalPin)
33
35
  return;
34
36
  warnedLegacyLocalPin = true;
35
- console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} the fallback that finds it is temporary.`);
37
+ console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Run \`harmony-agent doctor --fix\` in this repo to write ` + `${LOCAL_CONFIG_FILENAME}, or rename the file yourself. ` + `The fallback that finds it is temporary.`);
36
38
  }
37
39
  function noteLocalPinRename(from, to) {
38
40
  console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
39
41
  }
42
+ function noteUntrackedLocalPin(path) {
43
+ if (warnedUntrackedLocalPin)
44
+ return;
45
+ warnedUntrackedLocalPin = true;
46
+ console.error(`Harmony: ${path} is ignored by git, so it will not travel with a branch — ` + `a fresh clone, and every worktree the agent daemon cuts, will not have it. ` + `Commit it if you want it to describe this repo everywhere.`);
47
+ }
48
+ function isGitIgnored(path) {
49
+ try {
50
+ execFileSync("git", ["check-ignore", "--quiet", path], {
51
+ cwd: dirname(path),
52
+ stdio: "ignore"
53
+ });
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
40
59
  function getHmyRootDir() {
41
60
  return join(homedir(), CONFIG_DIR_NAME);
42
61
  }
@@ -151,19 +170,96 @@ function saveLocalConfig(config, cwd) {
151
170
  if (foundPath !== null && foundPath !== localConfigPath) {
152
171
  noteLocalPinRename(foundPath, localConfigPath);
153
172
  }
154
- const existingConfig = loadLocalConfig(cwd) || {
155
- workspaceId: null,
156
- projectId: null
157
- };
158
- const newConfig = { ...existingConfig, ...config };
159
- const cleanConfig = {};
160
- if (newConfig.workspaceId)
161
- cleanConfig.workspaceId = newConfig.workspaceId;
162
- if (newConfig.projectId)
163
- cleanConfig.projectId = newConfig.projectId;
164
- writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
173
+ const existing = readRawLocalConfig(foundPath ?? localConfigPath);
174
+ const merged = { ...existing };
175
+ if ("workspaceId" in config) {
176
+ if (config.workspaceId)
177
+ merged.workspaceId = config.workspaceId;
178
+ else
179
+ delete merged.workspaceId;
180
+ }
181
+ if ("projectId" in config) {
182
+ if (config.projectId)
183
+ merged.projectId = config.projectId;
184
+ else
185
+ delete merged.projectId;
186
+ }
187
+ writeFileSync(localConfigPath, `${JSON.stringify(merged, null, localIndent(localConfigPath))}
188
+ `);
189
+ if (isGitIgnored(localConfigPath))
190
+ noteUntrackedLocalPin(localConfigPath);
165
191
  return localConfigPath;
166
192
  }
193
+ function readRawLocalConfig(path) {
194
+ let text;
195
+ try {
196
+ text = readFileSync(path, "utf-8");
197
+ } catch {
198
+ return {};
199
+ }
200
+ try {
201
+ const parsed = JSON.parse(text);
202
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
203
+ return parsed;
204
+ }
205
+ } catch {}
206
+ noteUnparsableLocalPin(path, text);
207
+ return {};
208
+ }
209
+ function noteUnparsableLocalPin(path, contents) {
210
+ let backup = null;
211
+ try {
212
+ backup = join(tmpdir(), `hmy-pin-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json.bak`);
213
+ writeFileSync(backup, contents);
214
+ } catch {
215
+ backup = null;
216
+ }
217
+ console.error(`Harmony: ${path} could not be parsed as JSON, so the pin write REPLACED it. ` + (backup ? `The previous contents are in ${backup}.` : "The previous contents could not be backed up and are gone.") + ` If it carried a "commands" block, re-add it.`);
218
+ }
219
+ function localIndent(configPath) {
220
+ const root = dirname(configPath);
221
+ for (const name of ["biome.json", "biome.jsonc"]) {
222
+ const parsed = readJsonish(join(root, name));
223
+ if (!parsed || typeof parsed !== "object")
224
+ continue;
225
+ const formatter = parsed.formatter;
226
+ if (formatter?.indentStyle === "space") {
227
+ const width = formatter.indentWidth;
228
+ return typeof width === "number" && width > 0 ? width : 2;
229
+ }
230
+ return "\t";
231
+ }
232
+ const editorconfig = readTextSafely(join(root, ".editorconfig"));
233
+ if (editorconfig) {
234
+ if (/^\s*indent_style\s*=\s*tab\s*$/im.test(editorconfig))
235
+ return "\t";
236
+ const size = editorconfig.match(/^\s*indent_size\s*=\s*(\d+)\s*$/im);
237
+ if (size) {
238
+ const width = Number(size[1]);
239
+ if (width > 0)
240
+ return width;
241
+ }
242
+ }
243
+ return 2;
244
+ }
245
+ function readJsonish(path) {
246
+ const text = readTextSafely(path);
247
+ if (text === null)
248
+ return null;
249
+ try {
250
+ const stripped = text.replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1");
251
+ return JSON.parse(stripped);
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+ function readTextSafely(path) {
257
+ try {
258
+ return readFileSync(path, "utf-8");
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
167
263
  function hasLocalConfig(cwd) {
168
264
  return findLocalConfigPath(cwd) !== null;
169
265
  }
@@ -300,7 +396,7 @@ function getMemoryDir() {
300
396
  return config.memoryDir;
301
397
  return join(homedir(), ".harmony", "memory");
302
398
  }
303
- var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false;
399
+ var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false, warnedUntrackedLocalPin = false;
304
400
  var init_config = () => {};
305
401
 
306
402
  // src/oauth-login.ts
@@ -588,6 +588,60 @@ var REVIEW_DISALLOWED_TOOLS = [
588
588
  "mcp__harmony__harmony_delete_subtask",
589
589
  "mcp__harmony__harmony_toggle_subtask"
590
590
  ];
591
+ // ../harmony-shared/dist/runEventSanitize.js
592
+ var REPLACEMENT = "�";
593
+ function sanitizeRunEventString(value) {
594
+ let out = "";
595
+ for (let i = 0;i < value.length; i++) {
596
+ const code = value.charCodeAt(i);
597
+ if (code === 0)
598
+ continue;
599
+ if (code >= 55296 && code <= 56319) {
600
+ const next = value.charCodeAt(i + 1);
601
+ if (next >= 56320 && next <= 57343) {
602
+ out += value[i] + value[i + 1];
603
+ i++;
604
+ continue;
605
+ }
606
+ out += REPLACEMENT;
607
+ continue;
608
+ }
609
+ if (code >= 56320 && code <= 57343) {
610
+ out += REPLACEMENT;
611
+ continue;
612
+ }
613
+ out += value[i];
614
+ }
615
+ return out;
616
+ }
617
+ function sanitizeRunEventPayload(payload) {
618
+ return walk(payload, new Map);
619
+ }
620
+ function sanitizeRunEventDraft(draft) {
621
+ return { ...draft, payload: sanitizeRunEventPayload(draft.payload) };
622
+ }
623
+ function walk(value, seen) {
624
+ if (typeof value === "string")
625
+ return sanitizeRunEventString(value);
626
+ if (value === null || typeof value !== "object")
627
+ return value;
628
+ const already = seen.get(value);
629
+ if (already !== undefined)
630
+ return already;
631
+ if (Array.isArray(value)) {
632
+ const out2 = [];
633
+ seen.set(value, out2);
634
+ for (const entry of value)
635
+ out2.push(walk(entry, seen));
636
+ return out2;
637
+ }
638
+ const out = {};
639
+ seen.set(value, out);
640
+ for (const [key, entry] of Object.entries(value)) {
641
+ out[sanitizeRunEventString(key)] = walk(entry, seen);
642
+ }
643
+ return out;
644
+ }
591
645
  // ../harmony-shared/dist/runRedaction.js
592
646
  var MAX_INPUT_CHARS = 2000;
593
647
  var MAX_OUTPUT_CHARS = 4000;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "3.7.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.",
3
+ "version": "3.9.0",
4
+ "description": "MCP server for Harmony, the shared surface for human\u2013agent teams \u2014 agents claim cards, report progress, and move work on your board.",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },