@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.
- package/README.md +3 -3
- package/dist/cli.js +623 -157
- package/dist/index.js +228 -97
- package/dist/lib/api-client.js +181 -16
- package/dist/lib/config.js +110 -14
- package/dist/lib/oauth-refresh.js +110 -14
- package/dist/run-hook-cli.js +54 -0
- package/package.json +2 -2
- package/src/api-client.ts +91 -1
- package/src/config.ts +262 -14
- package/src/prompt-builder.ts +1 -1
- package/src/server.ts +70 -4
- package/src/skills.ts +6 -80
- package/src/tui/agent-instructions.ts +335 -0
- package/src/tui/setup.ts +144 -63
- package/src/tui/writer.ts +118 -2
package/dist/lib/api-client.js
CHANGED
|
@@ -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. ` + `
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
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:
|
|
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`,
|
|
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}`);
|
package/dist/lib/config.js
CHANGED
|
@@ -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. ` + `
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
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. ` + `
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
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
|
package/dist/run-hook-cli.js
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "MCP server for Harmony, the shared surface for human
|
|
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
|
},
|