@gethmy/mcp 2.13.4 → 2.14.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 +5 -2
- package/dist/cli.js +271 -272
- package/dist/index.js +271 -272
- package/dist/lib/api-client.js +143 -140
- package/dist/lib/oauth-refresh.js +350 -0
- package/package.json +6 -2
- package/src/api-client.ts +13 -12
- package/src/server.ts +387 -320
- package/src/skills.ts +2 -2
package/dist/lib/api-client.js
CHANGED
|
@@ -210,6 +210,140 @@ function getMemoryDir() {
|
|
|
210
210
|
var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
211
211
|
var init_config = () => {};
|
|
212
212
|
|
|
213
|
+
// src/oauth-login.ts
|
|
214
|
+
function oauthBaseFromApiUrl(apiUrl) {
|
|
215
|
+
return `${new URL(apiUrl).origin}/oauth`;
|
|
216
|
+
}
|
|
217
|
+
var MCP_RESOURCE_URL, LOGIN_TIMEOUT_MS;
|
|
218
|
+
var init_oauth_login = __esm(() => {
|
|
219
|
+
MCP_RESOURCE_URL = process.env.HARMONY_MCP_RESOURCE || "https://mcp.gethmy.com";
|
|
220
|
+
LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// src/oauth-refresh.ts
|
|
224
|
+
var exports_oauth_refresh = {};
|
|
225
|
+
__export(exports_oauth_refresh, {
|
|
226
|
+
refreshOAuthToken: () => refreshOAuthToken
|
|
227
|
+
});
|
|
228
|
+
import {
|
|
229
|
+
closeSync,
|
|
230
|
+
openSync,
|
|
231
|
+
renameSync,
|
|
232
|
+
rmSync,
|
|
233
|
+
statSync,
|
|
234
|
+
writeSync
|
|
235
|
+
} from "node:fs";
|
|
236
|
+
import { join as join2 } from "node:path";
|
|
237
|
+
function lockPath() {
|
|
238
|
+
return join2(getConfigDir(), LOCK_FILENAME);
|
|
239
|
+
}
|
|
240
|
+
function sleep(ms) {
|
|
241
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
242
|
+
}
|
|
243
|
+
async function withRefreshLock(fn) {
|
|
244
|
+
const path = lockPath();
|
|
245
|
+
const deadline = Date.now() + LOCK_ACQUIRE_TIMEOUT_MS;
|
|
246
|
+
let held = false;
|
|
247
|
+
while (Date.now() < deadline) {
|
|
248
|
+
try {
|
|
249
|
+
const fd = openSync(path, "wx");
|
|
250
|
+
writeSync(fd, String(process.pid));
|
|
251
|
+
closeSync(fd);
|
|
252
|
+
held = true;
|
|
253
|
+
break;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (err.code !== "EEXIST") {
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const age = Date.now() - statSync(path).mtimeMs;
|
|
260
|
+
if (age > LOCK_STALE_MS) {
|
|
261
|
+
const claim = `${path}.stale.${process.pid}`;
|
|
262
|
+
try {
|
|
263
|
+
renameSync(path, claim);
|
|
264
|
+
rmSync(claim, { force: true });
|
|
265
|
+
} catch {}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
await sleep(LOCK_RETRY_MS);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
return await fn();
|
|
276
|
+
} finally {
|
|
277
|
+
if (held) {
|
|
278
|
+
try {
|
|
279
|
+
rmSync(path, { force: true });
|
|
280
|
+
} catch {}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function refreshOAuthToken() {
|
|
285
|
+
if (inFlight)
|
|
286
|
+
return inFlight;
|
|
287
|
+
inFlight = doRefresh().finally(() => {
|
|
288
|
+
inFlight = null;
|
|
289
|
+
});
|
|
290
|
+
return inFlight;
|
|
291
|
+
}
|
|
292
|
+
async function doRefresh() {
|
|
293
|
+
const before = loadConfig();
|
|
294
|
+
if (!before.oauthRefreshToken || !before.oauthClientId)
|
|
295
|
+
return null;
|
|
296
|
+
return withRefreshLock(async () => {
|
|
297
|
+
const config = loadConfig();
|
|
298
|
+
if (config.oauthRefreshToken !== before.oauthRefreshToken && config.oauthAccessToken) {
|
|
299
|
+
return config.oauthAccessToken;
|
|
300
|
+
}
|
|
301
|
+
if (!config.oauthRefreshToken || !config.oauthClientId)
|
|
302
|
+
return null;
|
|
303
|
+
const base = oauthBaseFromApiUrl(config.apiUrl);
|
|
304
|
+
let res;
|
|
305
|
+
try {
|
|
306
|
+
res = await fetch(`${base}/token`, {
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
309
|
+
body: new URLSearchParams({
|
|
310
|
+
grant_type: "refresh_token",
|
|
311
|
+
refresh_token: config.oauthRefreshToken,
|
|
312
|
+
client_id: config.oauthClientId
|
|
313
|
+
}).toString()
|
|
314
|
+
});
|
|
315
|
+
} catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
if (!res.ok) {
|
|
319
|
+
const errorCode = await res.json().then((b) => b?.error ?? null).catch(() => null);
|
|
320
|
+
if (errorCode === "invalid_grant") {
|
|
321
|
+
saveConfig({
|
|
322
|
+
oauthAccessToken: null,
|
|
323
|
+
oauthRefreshToken: null,
|
|
324
|
+
oauthExpiresAt: null,
|
|
325
|
+
oauthClientId: null
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const body = await res.json().catch(() => null);
|
|
331
|
+
if (!body?.access_token || !body.refresh_token)
|
|
332
|
+
return null;
|
|
333
|
+
saveConfig({
|
|
334
|
+
oauthAccessToken: body.access_token,
|
|
335
|
+
oauthRefreshToken: body.refresh_token,
|
|
336
|
+
oauthExpiresAt: Date.now() + (body.expires_in ?? 0) * 1000
|
|
337
|
+
});
|
|
338
|
+
return body.access_token;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
var inFlight = null, LOCK_FILENAME = "refresh.lock", LOCK_STALE_MS = 30000, LOCK_ACQUIRE_TIMEOUT_MS = 35000, LOCK_RETRY_MS = 100;
|
|
342
|
+
var init_oauth_refresh = __esm(() => {
|
|
343
|
+
init_config();
|
|
344
|
+
init_oauth_login();
|
|
345
|
+
});
|
|
346
|
+
|
|
213
347
|
// src/prompt-builder.ts
|
|
214
348
|
var exports_prompt_builder = {};
|
|
215
349
|
__export(exports_prompt_builder, {
|
|
@@ -699,140 +833,6 @@ var init_prompt_builder = __esm(() => {
|
|
|
699
833
|
execute: `EXECUTE MODE: Implement this task completely. Write production-ready code following best practices. Include necessary tests and documentation.`
|
|
700
834
|
};
|
|
701
835
|
});
|
|
702
|
-
|
|
703
|
-
// src/oauth-login.ts
|
|
704
|
-
function oauthBaseFromApiUrl(apiUrl) {
|
|
705
|
-
return `${new URL(apiUrl).origin}/oauth`;
|
|
706
|
-
}
|
|
707
|
-
var MCP_RESOURCE_URL, LOGIN_TIMEOUT_MS;
|
|
708
|
-
var init_oauth_login = __esm(() => {
|
|
709
|
-
MCP_RESOURCE_URL = process.env.HARMONY_MCP_RESOURCE || "https://mcp.gethmy.com";
|
|
710
|
-
LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
// src/oauth-refresh.ts
|
|
714
|
-
var exports_oauth_refresh = {};
|
|
715
|
-
__export(exports_oauth_refresh, {
|
|
716
|
-
refreshOAuthToken: () => refreshOAuthToken
|
|
717
|
-
});
|
|
718
|
-
import {
|
|
719
|
-
closeSync,
|
|
720
|
-
openSync,
|
|
721
|
-
renameSync,
|
|
722
|
-
rmSync,
|
|
723
|
-
statSync,
|
|
724
|
-
writeSync
|
|
725
|
-
} from "node:fs";
|
|
726
|
-
import { join as join2 } from "node:path";
|
|
727
|
-
function lockPath() {
|
|
728
|
-
return join2(getConfigDir(), LOCK_FILENAME);
|
|
729
|
-
}
|
|
730
|
-
function sleep(ms) {
|
|
731
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
732
|
-
}
|
|
733
|
-
async function withRefreshLock(fn) {
|
|
734
|
-
const path = lockPath();
|
|
735
|
-
const deadline = Date.now() + LOCK_ACQUIRE_TIMEOUT_MS;
|
|
736
|
-
let held = false;
|
|
737
|
-
while (Date.now() < deadline) {
|
|
738
|
-
try {
|
|
739
|
-
const fd = openSync(path, "wx");
|
|
740
|
-
writeSync(fd, String(process.pid));
|
|
741
|
-
closeSync(fd);
|
|
742
|
-
held = true;
|
|
743
|
-
break;
|
|
744
|
-
} catch (err) {
|
|
745
|
-
if (err.code !== "EEXIST") {
|
|
746
|
-
break;
|
|
747
|
-
}
|
|
748
|
-
try {
|
|
749
|
-
const age = Date.now() - statSync(path).mtimeMs;
|
|
750
|
-
if (age > LOCK_STALE_MS) {
|
|
751
|
-
const claim = `${path}.stale.${process.pid}`;
|
|
752
|
-
try {
|
|
753
|
-
renameSync(path, claim);
|
|
754
|
-
rmSync(claim, { force: true });
|
|
755
|
-
} catch {}
|
|
756
|
-
continue;
|
|
757
|
-
}
|
|
758
|
-
} catch {
|
|
759
|
-
continue;
|
|
760
|
-
}
|
|
761
|
-
await sleep(LOCK_RETRY_MS);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
try {
|
|
765
|
-
return await fn();
|
|
766
|
-
} finally {
|
|
767
|
-
if (held) {
|
|
768
|
-
try {
|
|
769
|
-
rmSync(path, { force: true });
|
|
770
|
-
} catch {}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
function refreshOAuthToken() {
|
|
775
|
-
if (inFlight)
|
|
776
|
-
return inFlight;
|
|
777
|
-
inFlight = doRefresh().finally(() => {
|
|
778
|
-
inFlight = null;
|
|
779
|
-
});
|
|
780
|
-
return inFlight;
|
|
781
|
-
}
|
|
782
|
-
async function doRefresh() {
|
|
783
|
-
const before = loadConfig();
|
|
784
|
-
if (!before.oauthRefreshToken || !before.oauthClientId)
|
|
785
|
-
return null;
|
|
786
|
-
return withRefreshLock(async () => {
|
|
787
|
-
const config = loadConfig();
|
|
788
|
-
if (config.oauthRefreshToken !== before.oauthRefreshToken && config.oauthAccessToken) {
|
|
789
|
-
return config.oauthAccessToken;
|
|
790
|
-
}
|
|
791
|
-
if (!config.oauthRefreshToken || !config.oauthClientId)
|
|
792
|
-
return null;
|
|
793
|
-
const base = oauthBaseFromApiUrl(config.apiUrl);
|
|
794
|
-
let res;
|
|
795
|
-
try {
|
|
796
|
-
res = await fetch(`${base}/token`, {
|
|
797
|
-
method: "POST",
|
|
798
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
799
|
-
body: new URLSearchParams({
|
|
800
|
-
grant_type: "refresh_token",
|
|
801
|
-
refresh_token: config.oauthRefreshToken,
|
|
802
|
-
client_id: config.oauthClientId
|
|
803
|
-
}).toString()
|
|
804
|
-
});
|
|
805
|
-
} catch {
|
|
806
|
-
return null;
|
|
807
|
-
}
|
|
808
|
-
if (!res.ok) {
|
|
809
|
-
const errorCode = await res.json().then((b) => b?.error ?? null).catch(() => null);
|
|
810
|
-
if (errorCode === "invalid_grant") {
|
|
811
|
-
saveConfig({
|
|
812
|
-
oauthAccessToken: null,
|
|
813
|
-
oauthRefreshToken: null,
|
|
814
|
-
oauthExpiresAt: null,
|
|
815
|
-
oauthClientId: null
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
return null;
|
|
819
|
-
}
|
|
820
|
-
const body = await res.json().catch(() => null);
|
|
821
|
-
if (!body?.access_token || !body.refresh_token)
|
|
822
|
-
return null;
|
|
823
|
-
saveConfig({
|
|
824
|
-
oauthAccessToken: body.access_token,
|
|
825
|
-
oauthRefreshToken: body.refresh_token,
|
|
826
|
-
oauthExpiresAt: Date.now() + (body.expires_in ?? 0) * 1000
|
|
827
|
-
});
|
|
828
|
-
return body.access_token;
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
|
-
var inFlight = null, LOCK_FILENAME = "refresh.lock", LOCK_STALE_MS = 30000, LOCK_ACQUIRE_TIMEOUT_MS = 35000, LOCK_RETRY_MS = 100;
|
|
832
|
-
var init_oauth_refresh = __esm(() => {
|
|
833
|
-
init_config();
|
|
834
|
-
init_oauth_login();
|
|
835
|
-
});
|
|
836
836
|
// ../harmony-shared/dist/cardLinks.js
|
|
837
837
|
var LINK_TYPE_INVERSES = {
|
|
838
838
|
relates_to: "relates_to",
|
|
@@ -904,6 +904,8 @@ function serializeCommentThread(comments, options = {}) {
|
|
|
904
904
|
const tags = [];
|
|
905
905
|
if (c.edited_at)
|
|
906
906
|
tags.push("edited");
|
|
907
|
+
if (c.reply_to_id)
|
|
908
|
+
tags.push(`reply to ${ref(c.reply_to_id)}`);
|
|
907
909
|
if (c.supersedes_id)
|
|
908
910
|
tags.push(`supersedes ${ref(c.supersedes_id)}`);
|
|
909
911
|
if (c.confirms_id)
|
|
@@ -1315,6 +1317,12 @@ class HarmonyApiClient {
|
|
|
1315
1317
|
async getCardExternalLinks(cardId) {
|
|
1316
1318
|
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
1317
1319
|
}
|
|
1320
|
+
async addExternalLink(cardId, url, title) {
|
|
1321
|
+
return this.request("POST", `/cards/${cardId}/external-links`, {
|
|
1322
|
+
url,
|
|
1323
|
+
title
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1318
1326
|
async uploadArtifact(data) {
|
|
1319
1327
|
return this.request("POST", "/artifacts", data);
|
|
1320
1328
|
}
|
|
@@ -1366,6 +1374,7 @@ class HarmonyApiClient {
|
|
|
1366
1374
|
commentType: opts?.commentType,
|
|
1367
1375
|
supersedesId: opts?.supersedesId,
|
|
1368
1376
|
confirmsId: opts?.confirmsId,
|
|
1377
|
+
replyToId: opts?.replyToId,
|
|
1369
1378
|
agentSessionId: opts?.agentSessionId
|
|
1370
1379
|
});
|
|
1371
1380
|
}
|
|
@@ -1818,12 +1827,6 @@ ${planContent.trim()}`;
|
|
|
1818
1827
|
async updatePlaybook(playbookId, updates) {
|
|
1819
1828
|
return this.request("PATCH", `/playbooks/${playbookId}`, updates);
|
|
1820
1829
|
}
|
|
1821
|
-
async runPlaybook(playbookId) {
|
|
1822
|
-
return this.request("POST", `/playbooks/${playbookId}/run`);
|
|
1823
|
-
}
|
|
1824
|
-
async savePlaybookFromCard(data) {
|
|
1825
|
-
return this.request("POST", "/playbooks/from-card", data);
|
|
1826
|
-
}
|
|
1827
1830
|
}
|
|
1828
1831
|
var _promptModules = null;
|
|
1829
1832
|
async function loadPromptModules() {
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __returnValue = (v) => v;
|
|
3
|
+
function __exportSetter(name, newValue) {
|
|
4
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
5
|
+
}
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
set: __exportSetter.bind(all, name)
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
16
|
+
|
|
17
|
+
// src/config.ts
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
function getConfigDir() {
|
|
22
|
+
return join(homedir(), ".harmony-mcp");
|
|
23
|
+
}
|
|
24
|
+
function getConfigPath() {
|
|
25
|
+
return join(getConfigDir(), "config.json");
|
|
26
|
+
}
|
|
27
|
+
function getLocalConfigPath(cwd) {
|
|
28
|
+
return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
|
|
29
|
+
}
|
|
30
|
+
function emptyConfig() {
|
|
31
|
+
return {
|
|
32
|
+
apiKey: null,
|
|
33
|
+
apiUrl: DEFAULT_API_URL,
|
|
34
|
+
activeWorkspaceId: null,
|
|
35
|
+
activeProjectId: null,
|
|
36
|
+
userEmail: null,
|
|
37
|
+
memoryDir: null,
|
|
38
|
+
oauthAccessToken: null,
|
|
39
|
+
oauthRefreshToken: null,
|
|
40
|
+
oauthExpiresAt: null,
|
|
41
|
+
oauthClientId: null
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function loadConfig() {
|
|
45
|
+
const configPath = getConfigPath();
|
|
46
|
+
if (!existsSync(configPath)) {
|
|
47
|
+
return emptyConfig();
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const data = readFileSync(configPath, "utf-8");
|
|
51
|
+
const config = JSON.parse(data);
|
|
52
|
+
return {
|
|
53
|
+
apiKey: config.apiKey || null,
|
|
54
|
+
apiUrl: config.apiUrl || DEFAULT_API_URL,
|
|
55
|
+
activeWorkspaceId: config.activeWorkspaceId || null,
|
|
56
|
+
activeProjectId: config.activeProjectId || null,
|
|
57
|
+
userEmail: config.userEmail || null,
|
|
58
|
+
memoryDir: config.memoryDir || null,
|
|
59
|
+
oauthAccessToken: config.oauthAccessToken || null,
|
|
60
|
+
oauthRefreshToken: config.oauthRefreshToken || null,
|
|
61
|
+
oauthExpiresAt: typeof config.oauthExpiresAt === "number" ? config.oauthExpiresAt : null,
|
|
62
|
+
oauthClientId: config.oauthClientId || null
|
|
63
|
+
};
|
|
64
|
+
} catch {
|
|
65
|
+
return emptyConfig();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function saveConfig(config) {
|
|
69
|
+
const configDir = getConfigDir();
|
|
70
|
+
const configPath = getConfigPath();
|
|
71
|
+
if (!existsSync(configDir)) {
|
|
72
|
+
mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
73
|
+
}
|
|
74
|
+
const existingConfig = loadConfig();
|
|
75
|
+
const newConfig = { ...existingConfig, ...config };
|
|
76
|
+
writeFileSync(configPath, JSON.stringify(newConfig, null, 2), {
|
|
77
|
+
mode: 384
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function loadLocalConfig(cwd) {
|
|
81
|
+
const localConfigPath = getLocalConfigPath(cwd);
|
|
82
|
+
if (!existsSync(localConfigPath)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const data = readFileSync(localConfigPath, "utf-8");
|
|
87
|
+
const config = JSON.parse(data);
|
|
88
|
+
return {
|
|
89
|
+
workspaceId: config.workspaceId || null,
|
|
90
|
+
projectId: config.projectId || null
|
|
91
|
+
};
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function saveLocalConfig(config, cwd) {
|
|
97
|
+
const localConfigPath = getLocalConfigPath(cwd);
|
|
98
|
+
const existingConfig = loadLocalConfig(cwd) || {
|
|
99
|
+
workspaceId: null,
|
|
100
|
+
projectId: null
|
|
101
|
+
};
|
|
102
|
+
const newConfig = { ...existingConfig, ...config };
|
|
103
|
+
const cleanConfig = {};
|
|
104
|
+
if (newConfig.workspaceId)
|
|
105
|
+
cleanConfig.workspaceId = newConfig.workspaceId;
|
|
106
|
+
if (newConfig.projectId)
|
|
107
|
+
cleanConfig.projectId = newConfig.projectId;
|
|
108
|
+
writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
|
|
109
|
+
}
|
|
110
|
+
function hasLocalConfig(cwd) {
|
|
111
|
+
return existsSync(getLocalConfigPath(cwd));
|
|
112
|
+
}
|
|
113
|
+
function getActiveCredential() {
|
|
114
|
+
const config = loadConfig();
|
|
115
|
+
if (config.oauthAccessToken)
|
|
116
|
+
return config.oauthAccessToken;
|
|
117
|
+
if (config.apiKey)
|
|
118
|
+
return config.apiKey;
|
|
119
|
+
throw new Error(`Not configured. Run "npx @gethmy/mcp setup" to connect Harmony.
|
|
120
|
+
` + "Setup authorizes in your browser — no API key handling required.");
|
|
121
|
+
}
|
|
122
|
+
function getApiKey() {
|
|
123
|
+
return getActiveCredential();
|
|
124
|
+
}
|
|
125
|
+
function getApiUrl() {
|
|
126
|
+
const config = loadConfig();
|
|
127
|
+
return config.apiUrl;
|
|
128
|
+
}
|
|
129
|
+
function getUserEmail() {
|
|
130
|
+
const config = loadConfig();
|
|
131
|
+
return config.userEmail;
|
|
132
|
+
}
|
|
133
|
+
function setUserEmail(email) {
|
|
134
|
+
saveConfig({ userEmail: email });
|
|
135
|
+
}
|
|
136
|
+
function setActiveWorkspace(workspaceId, options) {
|
|
137
|
+
if (options?.local) {
|
|
138
|
+
saveLocalConfig({ workspaceId }, options.cwd);
|
|
139
|
+
} else {
|
|
140
|
+
saveConfig({ activeWorkspaceId: workspaceId });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function setActiveProject(projectId, options) {
|
|
144
|
+
if (options?.local) {
|
|
145
|
+
saveLocalConfig({ projectId }, options.cwd);
|
|
146
|
+
} else {
|
|
147
|
+
saveConfig({ activeProjectId: projectId });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function getActiveWorkspaceId(cwd) {
|
|
151
|
+
const localConfig = loadLocalConfig(cwd);
|
|
152
|
+
if (localConfig?.workspaceId) {
|
|
153
|
+
return localConfig.workspaceId;
|
|
154
|
+
}
|
|
155
|
+
return loadConfig().activeWorkspaceId;
|
|
156
|
+
}
|
|
157
|
+
function getActiveProjectId(cwd) {
|
|
158
|
+
const localConfig = loadLocalConfig(cwd);
|
|
159
|
+
if (localConfig?.projectId) {
|
|
160
|
+
return localConfig.projectId;
|
|
161
|
+
}
|
|
162
|
+
return loadConfig().activeProjectId;
|
|
163
|
+
}
|
|
164
|
+
function isConfigured() {
|
|
165
|
+
const config = loadConfig();
|
|
166
|
+
return !!(config.apiKey || config.oauthAccessToken);
|
|
167
|
+
}
|
|
168
|
+
function areSkillsInstalled(cwd) {
|
|
169
|
+
const home = homedir();
|
|
170
|
+
const workingDir = cwd || process.cwd();
|
|
171
|
+
const foundPaths = [];
|
|
172
|
+
const globalSkillsDir = join(home, ".agents", "skills");
|
|
173
|
+
const globalSkillPath = join(globalSkillsDir, "hmy", "SKILL.md");
|
|
174
|
+
if (existsSync(globalSkillPath)) {
|
|
175
|
+
foundPaths.push(globalSkillPath);
|
|
176
|
+
return { installed: true, location: "global", paths: foundPaths };
|
|
177
|
+
}
|
|
178
|
+
const claudeGlobalSkill = join(home, ".claude", "skills", "hmy.md");
|
|
179
|
+
if (existsSync(claudeGlobalSkill)) {
|
|
180
|
+
foundPaths.push(claudeGlobalSkill);
|
|
181
|
+
return { installed: true, location: "global", paths: foundPaths };
|
|
182
|
+
}
|
|
183
|
+
const claudeGlobalSkillAlt = join(home, ".claude", "skills", "hmy", "SKILL.md");
|
|
184
|
+
if (existsSync(claudeGlobalSkillAlt)) {
|
|
185
|
+
foundPaths.push(claudeGlobalSkillAlt);
|
|
186
|
+
return { installed: true, location: "global", paths: foundPaths };
|
|
187
|
+
}
|
|
188
|
+
const localSkillPath = join(workingDir, ".claude", "skills", "hmy.md");
|
|
189
|
+
if (existsSync(localSkillPath)) {
|
|
190
|
+
foundPaths.push(localSkillPath);
|
|
191
|
+
return { installed: true, location: "local", paths: foundPaths };
|
|
192
|
+
}
|
|
193
|
+
const localSkillPathAlt = join(workingDir, ".claude", "skills", "hmy", "SKILL.md");
|
|
194
|
+
if (existsSync(localSkillPathAlt)) {
|
|
195
|
+
foundPaths.push(localSkillPathAlt);
|
|
196
|
+
return { installed: true, location: "local", paths: foundPaths };
|
|
197
|
+
}
|
|
198
|
+
return { installed: false, location: null, paths: [] };
|
|
199
|
+
}
|
|
200
|
+
function hasProjectContext(cwd) {
|
|
201
|
+
const localConfig = loadLocalConfig(cwd);
|
|
202
|
+
return !!(localConfig?.workspaceId || localConfig?.projectId);
|
|
203
|
+
}
|
|
204
|
+
function getMemoryDir() {
|
|
205
|
+
const config = loadConfig();
|
|
206
|
+
if (config.memoryDir)
|
|
207
|
+
return config.memoryDir;
|
|
208
|
+
return join(homedir(), ".harmony", "memory");
|
|
209
|
+
}
|
|
210
|
+
var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
211
|
+
var init_config = () => {};
|
|
212
|
+
|
|
213
|
+
// src/oauth-login.ts
|
|
214
|
+
function oauthBaseFromApiUrl(apiUrl) {
|
|
215
|
+
return `${new URL(apiUrl).origin}/oauth`;
|
|
216
|
+
}
|
|
217
|
+
var MCP_RESOURCE_URL, LOGIN_TIMEOUT_MS;
|
|
218
|
+
var init_oauth_login = __esm(() => {
|
|
219
|
+
MCP_RESOURCE_URL = process.env.HARMONY_MCP_RESOURCE || "https://mcp.gethmy.com";
|
|
220
|
+
LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// src/oauth-refresh.ts
|
|
224
|
+
var exports_oauth_refresh = {};
|
|
225
|
+
__export(exports_oauth_refresh, {
|
|
226
|
+
refreshOAuthToken: () => refreshOAuthToken
|
|
227
|
+
});
|
|
228
|
+
import {
|
|
229
|
+
closeSync,
|
|
230
|
+
openSync,
|
|
231
|
+
renameSync,
|
|
232
|
+
rmSync,
|
|
233
|
+
statSync,
|
|
234
|
+
writeSync
|
|
235
|
+
} from "node:fs";
|
|
236
|
+
import { join as join2 } from "node:path";
|
|
237
|
+
function lockPath() {
|
|
238
|
+
return join2(getConfigDir(), LOCK_FILENAME);
|
|
239
|
+
}
|
|
240
|
+
function sleep(ms) {
|
|
241
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
242
|
+
}
|
|
243
|
+
async function withRefreshLock(fn) {
|
|
244
|
+
const path = lockPath();
|
|
245
|
+
const deadline = Date.now() + LOCK_ACQUIRE_TIMEOUT_MS;
|
|
246
|
+
let held = false;
|
|
247
|
+
while (Date.now() < deadline) {
|
|
248
|
+
try {
|
|
249
|
+
const fd = openSync(path, "wx");
|
|
250
|
+
writeSync(fd, String(process.pid));
|
|
251
|
+
closeSync(fd);
|
|
252
|
+
held = true;
|
|
253
|
+
break;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (err.code !== "EEXIST") {
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const age = Date.now() - statSync(path).mtimeMs;
|
|
260
|
+
if (age > LOCK_STALE_MS) {
|
|
261
|
+
const claim = `${path}.stale.${process.pid}`;
|
|
262
|
+
try {
|
|
263
|
+
renameSync(path, claim);
|
|
264
|
+
rmSync(claim, { force: true });
|
|
265
|
+
} catch {}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
await sleep(LOCK_RETRY_MS);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
return await fn();
|
|
276
|
+
} finally {
|
|
277
|
+
if (held) {
|
|
278
|
+
try {
|
|
279
|
+
rmSync(path, { force: true });
|
|
280
|
+
} catch {}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function refreshOAuthToken() {
|
|
285
|
+
if (inFlight)
|
|
286
|
+
return inFlight;
|
|
287
|
+
inFlight = doRefresh().finally(() => {
|
|
288
|
+
inFlight = null;
|
|
289
|
+
});
|
|
290
|
+
return inFlight;
|
|
291
|
+
}
|
|
292
|
+
async function doRefresh() {
|
|
293
|
+
const before = loadConfig();
|
|
294
|
+
if (!before.oauthRefreshToken || !before.oauthClientId)
|
|
295
|
+
return null;
|
|
296
|
+
return withRefreshLock(async () => {
|
|
297
|
+
const config = loadConfig();
|
|
298
|
+
if (config.oauthRefreshToken !== before.oauthRefreshToken && config.oauthAccessToken) {
|
|
299
|
+
return config.oauthAccessToken;
|
|
300
|
+
}
|
|
301
|
+
if (!config.oauthRefreshToken || !config.oauthClientId)
|
|
302
|
+
return null;
|
|
303
|
+
const base = oauthBaseFromApiUrl(config.apiUrl);
|
|
304
|
+
let res;
|
|
305
|
+
try {
|
|
306
|
+
res = await fetch(`${base}/token`, {
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
309
|
+
body: new URLSearchParams({
|
|
310
|
+
grant_type: "refresh_token",
|
|
311
|
+
refresh_token: config.oauthRefreshToken,
|
|
312
|
+
client_id: config.oauthClientId
|
|
313
|
+
}).toString()
|
|
314
|
+
});
|
|
315
|
+
} catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
if (!res.ok) {
|
|
319
|
+
const errorCode = await res.json().then((b) => b?.error ?? null).catch(() => null);
|
|
320
|
+
if (errorCode === "invalid_grant") {
|
|
321
|
+
saveConfig({
|
|
322
|
+
oauthAccessToken: null,
|
|
323
|
+
oauthRefreshToken: null,
|
|
324
|
+
oauthExpiresAt: null,
|
|
325
|
+
oauthClientId: null
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const body = await res.json().catch(() => null);
|
|
331
|
+
if (!body?.access_token || !body.refresh_token)
|
|
332
|
+
return null;
|
|
333
|
+
saveConfig({
|
|
334
|
+
oauthAccessToken: body.access_token,
|
|
335
|
+
oauthRefreshToken: body.refresh_token,
|
|
336
|
+
oauthExpiresAt: Date.now() + (body.expires_in ?? 0) * 1000
|
|
337
|
+
});
|
|
338
|
+
return body.access_token;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
var inFlight = null, LOCK_FILENAME = "refresh.lock", LOCK_STALE_MS = 30000, LOCK_ACQUIRE_TIMEOUT_MS = 35000, LOCK_RETRY_MS = 100;
|
|
342
|
+
var init_oauth_refresh = __esm(() => {
|
|
343
|
+
init_config();
|
|
344
|
+
init_oauth_login();
|
|
345
|
+
});
|
|
346
|
+
init_oauth_refresh();
|
|
347
|
+
|
|
348
|
+
export {
|
|
349
|
+
refreshOAuthToken
|
|
350
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.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"
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"./src/config.js": {
|
|
17
17
|
"types": "./src/config.ts",
|
|
18
18
|
"default": "./dist/lib/config.js"
|
|
19
|
+
},
|
|
20
|
+
"./src/oauth-refresh.js": {
|
|
21
|
+
"types": "./src/oauth-refresh.ts",
|
|
22
|
+
"default": "./dist/lib/oauth-refresh.js"
|
|
19
23
|
}
|
|
20
24
|
},
|
|
21
25
|
"bin": {
|
|
@@ -55,7 +59,7 @@
|
|
|
55
59
|
"bun": ">=1.0.0"
|
|
56
60
|
},
|
|
57
61
|
"scripts": {
|
|
58
|
-
"build": "rm -rf dist && bun build src/index.ts src/cli.ts --outdir dist --target node --external @clack/prompts --external @modelcontextprotocol/sdk --external commander --external hono --external picocolors --external zod && bun build src/api-client.ts src/config.ts --outdir dist/lib --root src --target node --external @clack/prompts --external @modelcontextprotocol/sdk --external commander --external hono --external picocolors --external zod",
|
|
62
|
+
"build": "rm -rf dist && bun build src/index.ts src/cli.ts --outdir dist --target node --external @clack/prompts --external @modelcontextprotocol/sdk --external commander --external hono --external picocolors --external zod && bun build src/api-client.ts src/config.ts src/oauth-refresh.ts --outdir dist/lib --root src --target node --external @clack/prompts --external @modelcontextprotocol/sdk --external commander --external hono --external picocolors --external zod",
|
|
59
63
|
"build:bun": "bun build src/index.ts src/http.ts src/remote.ts src/cli.ts --outdir dist --target bun",
|
|
60
64
|
"serve:remote": "bun src/remote.ts",
|
|
61
65
|
"dev": "bun --watch src/index.ts",
|