@gethmy/mcp 2.9.3 → 2.9.5
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 +15 -15
- package/dist/cli.js +674 -275
- package/dist/index.js +530 -205
- package/dist/lib/api-client.js +352 -182
- package/dist/lib/config.js +37 -26
- package/package.json +2 -2
- package/src/api-client.ts +52 -4
- package/src/cli.ts +13 -2
- package/src/config.ts +53 -25
- package/src/graph-expansion.ts +7 -4
- package/src/http.ts +3 -14
- package/src/memory-floor.ts +2 -1
- package/src/oauth-login.ts +288 -0
- package/src/oauth-refresh.ts +209 -0
- package/src/server.ts +6 -6
- package/src/skills.ts +4 -2
- package/src/tui/setup.ts +106 -16
package/dist/lib/api-client.js
CHANGED
|
@@ -14,6 +14,202 @@ var __export = (target, all) => {
|
|
|
14
14
|
};
|
|
15
15
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
16
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
|
+
|
|
17
213
|
// src/prompt-builder.ts
|
|
18
214
|
var exports_prompt_builder = {};
|
|
19
215
|
__export(exports_prompt_builder, {
|
|
@@ -504,193 +700,139 @@ var init_prompt_builder = __esm(() => {
|
|
|
504
700
|
};
|
|
505
701
|
});
|
|
506
702
|
|
|
507
|
-
// src/
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
import { join } from "node:path";
|
|
511
|
-
var DEFAULT_API_URL = "https://app.gethmy.com/api";
|
|
512
|
-
var LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
513
|
-
function getConfigDir() {
|
|
514
|
-
return join(homedir(), ".harmony-mcp");
|
|
703
|
+
// src/oauth-login.ts
|
|
704
|
+
function oauthBaseFromApiUrl(apiUrl) {
|
|
705
|
+
return `${new URL(apiUrl).origin}/oauth`;
|
|
515
706
|
}
|
|
516
|
-
|
|
517
|
-
|
|
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);
|
|
518
729
|
}
|
|
519
|
-
function
|
|
520
|
-
return
|
|
730
|
+
function sleep(ms) {
|
|
731
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
521
732
|
}
|
|
522
|
-
function
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
+
}
|
|
533
763
|
}
|
|
534
764
|
try {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
userEmail: config.userEmail || null,
|
|
543
|
-
memoryDir: config.memoryDir || null
|
|
544
|
-
};
|
|
545
|
-
} catch {
|
|
546
|
-
return {
|
|
547
|
-
apiKey: null,
|
|
548
|
-
apiUrl: DEFAULT_API_URL,
|
|
549
|
-
activeWorkspaceId: null,
|
|
550
|
-
activeProjectId: null,
|
|
551
|
-
userEmail: null,
|
|
552
|
-
memoryDir: null
|
|
553
|
-
};
|
|
765
|
+
return await fn();
|
|
766
|
+
} finally {
|
|
767
|
+
if (held) {
|
|
768
|
+
try {
|
|
769
|
+
rmSync(path, { force: true });
|
|
770
|
+
} catch {}
|
|
771
|
+
}
|
|
554
772
|
}
|
|
555
773
|
}
|
|
556
|
-
function
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
const existingConfig = loadConfig();
|
|
563
|
-
const newConfig = { ...existingConfig, ...config };
|
|
564
|
-
writeFileSync(configPath, JSON.stringify(newConfig, null, 2), {
|
|
565
|
-
mode: 384
|
|
774
|
+
function refreshOAuthToken() {
|
|
775
|
+
if (inFlight)
|
|
776
|
+
return inFlight;
|
|
777
|
+
inFlight = doRefresh().finally(() => {
|
|
778
|
+
inFlight = null;
|
|
566
779
|
});
|
|
780
|
+
return inFlight;
|
|
567
781
|
}
|
|
568
|
-
function
|
|
569
|
-
const
|
|
570
|
-
if (!
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
|
-
try {
|
|
574
|
-
const data = readFileSync(localConfigPath, "utf-8");
|
|
575
|
-
const config = JSON.parse(data);
|
|
576
|
-
return {
|
|
577
|
-
workspaceId: config.workspaceId || null,
|
|
578
|
-
projectId: config.projectId || null
|
|
579
|
-
};
|
|
580
|
-
} catch {
|
|
782
|
+
async function doRefresh() {
|
|
783
|
+
const before = loadConfig();
|
|
784
|
+
if (!before.oauthRefreshToken || !before.oauthClientId)
|
|
581
785
|
return null;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
function setActiveProject(projectId, options) {
|
|
628
|
-
if (options?.local) {
|
|
629
|
-
saveLocalConfig({ projectId }, options.cwd);
|
|
630
|
-
} else {
|
|
631
|
-
saveConfig({ activeProjectId: projectId });
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
function getActiveWorkspaceId(cwd) {
|
|
635
|
-
const localConfig = loadLocalConfig(cwd);
|
|
636
|
-
if (localConfig?.workspaceId) {
|
|
637
|
-
return localConfig.workspaceId;
|
|
638
|
-
}
|
|
639
|
-
return loadConfig().activeWorkspaceId;
|
|
640
|
-
}
|
|
641
|
-
function getActiveProjectId(cwd) {
|
|
642
|
-
const localConfig = loadLocalConfig(cwd);
|
|
643
|
-
if (localConfig?.projectId) {
|
|
644
|
-
return localConfig.projectId;
|
|
645
|
-
}
|
|
646
|
-
return loadConfig().activeProjectId;
|
|
647
|
-
}
|
|
648
|
-
function isConfigured() {
|
|
649
|
-
const config = loadConfig();
|
|
650
|
-
return !!config.apiKey;
|
|
651
|
-
}
|
|
652
|
-
function areSkillsInstalled(cwd) {
|
|
653
|
-
const home = homedir();
|
|
654
|
-
const workingDir = cwd || process.cwd();
|
|
655
|
-
const foundPaths = [];
|
|
656
|
-
const globalSkillsDir = join(home, ".agents", "skills");
|
|
657
|
-
const globalSkillPath = join(globalSkillsDir, "hmy", "SKILL.md");
|
|
658
|
-
if (existsSync(globalSkillPath)) {
|
|
659
|
-
foundPaths.push(globalSkillPath);
|
|
660
|
-
return { installed: true, location: "global", paths: foundPaths };
|
|
661
|
-
}
|
|
662
|
-
const claudeGlobalSkill = join(home, ".claude", "skills", "hmy.md");
|
|
663
|
-
if (existsSync(claudeGlobalSkill)) {
|
|
664
|
-
foundPaths.push(claudeGlobalSkill);
|
|
665
|
-
return { installed: true, location: "global", paths: foundPaths };
|
|
666
|
-
}
|
|
667
|
-
const claudeGlobalSkillAlt = join(home, ".claude", "skills", "hmy", "SKILL.md");
|
|
668
|
-
if (existsSync(claudeGlobalSkillAlt)) {
|
|
669
|
-
foundPaths.push(claudeGlobalSkillAlt);
|
|
670
|
-
return { installed: true, location: "global", paths: foundPaths };
|
|
671
|
-
}
|
|
672
|
-
const localSkillPath = join(workingDir, ".claude", "skills", "hmy.md");
|
|
673
|
-
if (existsSync(localSkillPath)) {
|
|
674
|
-
foundPaths.push(localSkillPath);
|
|
675
|
-
return { installed: true, location: "local", paths: foundPaths };
|
|
676
|
-
}
|
|
677
|
-
const localSkillPathAlt = join(workingDir, ".claude", "skills", "hmy", "SKILL.md");
|
|
678
|
-
if (existsSync(localSkillPathAlt)) {
|
|
679
|
-
foundPaths.push(localSkillPathAlt);
|
|
680
|
-
return { installed: true, location: "local", paths: foundPaths };
|
|
681
|
-
}
|
|
682
|
-
return { installed: false, location: null, paths: [] };
|
|
683
|
-
}
|
|
684
|
-
function hasProjectContext(cwd) {
|
|
685
|
-
const localConfig = loadLocalConfig(cwd);
|
|
686
|
-
return !!(localConfig?.workspaceId || localConfig?.projectId);
|
|
687
|
-
}
|
|
688
|
-
function getMemoryDir() {
|
|
689
|
-
const config = loadConfig();
|
|
690
|
-
if (config.memoryDir)
|
|
691
|
-
return config.memoryDir;
|
|
692
|
-
return join(homedir(), ".harmony", "memory");
|
|
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
|
+
});
|
|
693
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
|
+
});
|
|
694
836
|
|
|
695
837
|
// ../harmony-shared/dist/cardLinks.js
|
|
696
838
|
var LINK_TYPE_INVERSES = {
|
|
@@ -804,6 +946,7 @@ var TIMINGS = {
|
|
|
804
946
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
805
947
|
};
|
|
806
948
|
// src/api-client.ts
|
|
949
|
+
init_config();
|
|
807
950
|
var RETRY_CONFIG = {
|
|
808
951
|
maxRetries: 3,
|
|
809
952
|
baseDelayMs: 1000,
|
|
@@ -823,7 +966,7 @@ function getRetryDelay(attempt) {
|
|
|
823
966
|
const delay = Math.min(RETRY_CONFIG.baseDelayMs * 2 ** attempt, RETRY_CONFIG.maxDelayMs);
|
|
824
967
|
return Math.round(delay + delay * 0.25 * (Math.random() * 2 - 1));
|
|
825
968
|
}
|
|
826
|
-
var
|
|
969
|
+
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
827
970
|
|
|
828
971
|
class Semaphore {
|
|
829
972
|
permits;
|
|
@@ -888,10 +1031,12 @@ class HarmonyApiClient {
|
|
|
888
1031
|
apiKey;
|
|
889
1032
|
apiUrl;
|
|
890
1033
|
onUnauthorized;
|
|
1034
|
+
refreshCredential;
|
|
891
1035
|
constructor(options) {
|
|
892
1036
|
this.apiKey = options?.apiKey ?? getApiKey();
|
|
893
1037
|
this.apiUrl = options?.apiUrl ?? getApiUrl();
|
|
894
1038
|
this.onUnauthorized = options?.onUnauthorized;
|
|
1039
|
+
this.refreshCredential = options?.refreshCredential;
|
|
895
1040
|
}
|
|
896
1041
|
getApiUrl() {
|
|
897
1042
|
return this.apiUrl;
|
|
@@ -921,6 +1066,7 @@ class HarmonyApiClient {
|
|
|
921
1066
|
async requestWithRetry(method, path, body, options) {
|
|
922
1067
|
const url = `${this.apiUrl}/v1${path}`;
|
|
923
1068
|
let lastError = null;
|
|
1069
|
+
let refreshed = false;
|
|
924
1070
|
const contentType = options?.contentType || "application/json";
|
|
925
1071
|
const accept = options?.accept || "application/json";
|
|
926
1072
|
for (let attempt = 0;attempt <= RETRY_CONFIG.maxRetries; attempt++) {
|
|
@@ -949,6 +1095,15 @@ class HarmonyApiClient {
|
|
|
949
1095
|
if (!response.ok) {
|
|
950
1096
|
const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
|
|
951
1097
|
if (response.status === 401) {
|
|
1098
|
+
if (this.refreshCredential && !refreshed) {
|
|
1099
|
+
refreshed = true;
|
|
1100
|
+
const fresh = await this.refreshCredential();
|
|
1101
|
+
if (fresh) {
|
|
1102
|
+
this.apiKey = fresh;
|
|
1103
|
+
attempt--;
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
952
1107
|
this.onUnauthorized?.();
|
|
953
1108
|
throw new HarmonyUnauthorizedError(errorMsg);
|
|
954
1109
|
}
|
|
@@ -957,7 +1112,7 @@ class HarmonyApiClient {
|
|
|
957
1112
|
}
|
|
958
1113
|
lastError = new Error(errorMsg);
|
|
959
1114
|
if (attempt < RETRY_CONFIG.maxRetries) {
|
|
960
|
-
await
|
|
1115
|
+
await sleep2(getRetryDelay(attempt));
|
|
961
1116
|
continue;
|
|
962
1117
|
}
|
|
963
1118
|
throw lastError;
|
|
@@ -971,7 +1126,7 @@ class HarmonyApiClient {
|
|
|
971
1126
|
if (!isRetryableError(error))
|
|
972
1127
|
throw lastError;
|
|
973
1128
|
if (attempt < RETRY_CONFIG.maxRetries) {
|
|
974
|
-
await
|
|
1129
|
+
await sleep2(getRetryDelay(attempt));
|
|
975
1130
|
}
|
|
976
1131
|
}
|
|
977
1132
|
}
|
|
@@ -980,6 +1135,7 @@ class HarmonyApiClient {
|
|
|
980
1135
|
async requestRawWithRetry(method, path, body, options) {
|
|
981
1136
|
const url = `${this.apiUrl}/v1${path}`;
|
|
982
1137
|
let lastError = null;
|
|
1138
|
+
let refreshed = false;
|
|
983
1139
|
const contentType = options?.contentType || "application/json";
|
|
984
1140
|
const accept = options?.accept || "text/markdown";
|
|
985
1141
|
for (let attempt = 0;attempt <= RETRY_CONFIG.maxRetries; attempt++) {
|
|
@@ -1002,6 +1158,15 @@ class HarmonyApiClient {
|
|
|
1002
1158
|
errorMsg = text || `API error: ${response.status}`;
|
|
1003
1159
|
}
|
|
1004
1160
|
if (response.status === 401) {
|
|
1161
|
+
if (this.refreshCredential && !refreshed) {
|
|
1162
|
+
refreshed = true;
|
|
1163
|
+
const fresh = await this.refreshCredential();
|
|
1164
|
+
if (fresh) {
|
|
1165
|
+
this.apiKey = fresh;
|
|
1166
|
+
attempt--;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1005
1170
|
this.onUnauthorized?.();
|
|
1006
1171
|
throw new HarmonyUnauthorizedError(errorMsg);
|
|
1007
1172
|
}
|
|
@@ -1010,7 +1175,7 @@ class HarmonyApiClient {
|
|
|
1010
1175
|
}
|
|
1011
1176
|
lastError = new Error(errorMsg);
|
|
1012
1177
|
if (attempt < RETRY_CONFIG.maxRetries) {
|
|
1013
|
-
await
|
|
1178
|
+
await sleep2(getRetryDelay(attempt));
|
|
1014
1179
|
continue;
|
|
1015
1180
|
}
|
|
1016
1181
|
throw lastError;
|
|
@@ -1021,7 +1186,7 @@ class HarmonyApiClient {
|
|
|
1021
1186
|
if (!isRetryableError(error))
|
|
1022
1187
|
throw lastError;
|
|
1023
1188
|
if (attempt < RETRY_CONFIG.maxRetries) {
|
|
1024
|
-
await
|
|
1189
|
+
await sleep2(getRetryDelay(attempt));
|
|
1025
1190
|
}
|
|
1026
1191
|
}
|
|
1027
1192
|
}
|
|
@@ -1603,7 +1768,12 @@ async function loadPromptModules() {
|
|
|
1603
1768
|
var client = null;
|
|
1604
1769
|
function getClient() {
|
|
1605
1770
|
if (!client) {
|
|
1606
|
-
client = new HarmonyApiClient
|
|
1771
|
+
client = new HarmonyApiClient({
|
|
1772
|
+
refreshCredential: async () => {
|
|
1773
|
+
const { refreshOAuthToken: refreshOAuthToken2 } = await Promise.resolve().then(() => (init_oauth_refresh(), exports_oauth_refresh));
|
|
1774
|
+
return refreshOAuthToken2();
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1607
1777
|
}
|
|
1608
1778
|
return client;
|
|
1609
1779
|
}
|