@gethmy/mcp 2.13.4 → 2.15.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 +333 -276
- package/dist/index.js +333 -276
- package/dist/lib/api-client.js +170 -140
- package/dist/lib/oauth-refresh.js +350 -0
- package/package.json +7 -3
- package/src/api-client.ts +77 -12
- package/src/auto-session.ts +91 -1
- package/src/server.ts +403 -326
- 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)
|
|
@@ -1238,6 +1240,33 @@ class HarmonyApiClient {
|
|
|
1238
1240
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1239
1241
|
return this.request("GET", `/board/${projectId}${query}`);
|
|
1240
1242
|
}
|
|
1243
|
+
async getFullBoard(projectId, options) {
|
|
1244
|
+
const { pageSize = 200, ...boardOpts } = options ?? {};
|
|
1245
|
+
const cards = [];
|
|
1246
|
+
let offset = 0;
|
|
1247
|
+
let last = null;
|
|
1248
|
+
for (;; ) {
|
|
1249
|
+
const page = await this.getBoard(projectId, {
|
|
1250
|
+
...boardOpts,
|
|
1251
|
+
limit: pageSize,
|
|
1252
|
+
offset
|
|
1253
|
+
});
|
|
1254
|
+
last = page;
|
|
1255
|
+
const pageCards = page.cards ?? [];
|
|
1256
|
+
cards.push(...pageCards);
|
|
1257
|
+
const hasMore = page.pagination?.hasMore ?? false;
|
|
1258
|
+
if (!hasMore || pageCards.length === 0)
|
|
1259
|
+
break;
|
|
1260
|
+
offset += pageSize;
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
project: last?.project,
|
|
1264
|
+
columns: last?.columns ?? [],
|
|
1265
|
+
cards,
|
|
1266
|
+
labels: last?.labels ?? [],
|
|
1267
|
+
totalCards: last?.pagination?.totalCards ?? cards.length
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1241
1270
|
async createCard(projectId, data) {
|
|
1242
1271
|
return this.request("POST", "/cards", { projectId, ...data });
|
|
1243
1272
|
}
|
|
@@ -1315,6 +1344,12 @@ class HarmonyApiClient {
|
|
|
1315
1344
|
async getCardExternalLinks(cardId) {
|
|
1316
1345
|
return this.request("GET", `/cards/${cardId}/external-links`);
|
|
1317
1346
|
}
|
|
1347
|
+
async addExternalLink(cardId, url, title) {
|
|
1348
|
+
return this.request("POST", `/cards/${cardId}/external-links`, {
|
|
1349
|
+
url,
|
|
1350
|
+
title
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1318
1353
|
async uploadArtifact(data) {
|
|
1319
1354
|
return this.request("POST", "/artifacts", data);
|
|
1320
1355
|
}
|
|
@@ -1366,6 +1401,7 @@ class HarmonyApiClient {
|
|
|
1366
1401
|
commentType: opts?.commentType,
|
|
1367
1402
|
supersedesId: opts?.supersedesId,
|
|
1368
1403
|
confirmsId: opts?.confirmsId,
|
|
1404
|
+
replyToId: opts?.replyToId,
|
|
1369
1405
|
agentSessionId: opts?.agentSessionId
|
|
1370
1406
|
});
|
|
1371
1407
|
}
|
|
@@ -1818,12 +1854,6 @@ ${planContent.trim()}`;
|
|
|
1818
1854
|
async updatePlaybook(playbookId, updates) {
|
|
1819
1855
|
return this.request("PATCH", `/playbooks/${playbookId}`, updates);
|
|
1820
1856
|
}
|
|
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
1857
|
}
|
|
1828
1858
|
var _promptModules = null;
|
|
1829
1859
|
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
|
+
};
|