@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/cli.js
CHANGED
|
@@ -17,6 +17,199 @@ var __export = (target, all) => {
|
|
|
17
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
18
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
19
|
|
|
20
|
+
// src/config.ts
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
function getConfigDir() {
|
|
25
|
+
return join(homedir(), ".harmony-mcp");
|
|
26
|
+
}
|
|
27
|
+
function getConfigPath() {
|
|
28
|
+
return join(getConfigDir(), "config.json");
|
|
29
|
+
}
|
|
30
|
+
function getLocalConfigPath(cwd) {
|
|
31
|
+
return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
|
|
32
|
+
}
|
|
33
|
+
function emptyConfig() {
|
|
34
|
+
return {
|
|
35
|
+
apiKey: null,
|
|
36
|
+
apiUrl: DEFAULT_API_URL,
|
|
37
|
+
activeWorkspaceId: null,
|
|
38
|
+
activeProjectId: null,
|
|
39
|
+
userEmail: null,
|
|
40
|
+
memoryDir: null,
|
|
41
|
+
oauthAccessToken: null,
|
|
42
|
+
oauthRefreshToken: null,
|
|
43
|
+
oauthExpiresAt: null,
|
|
44
|
+
oauthClientId: null
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function loadConfig() {
|
|
48
|
+
const configPath = getConfigPath();
|
|
49
|
+
if (!existsSync(configPath)) {
|
|
50
|
+
return emptyConfig();
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const data = readFileSync(configPath, "utf-8");
|
|
54
|
+
const config = JSON.parse(data);
|
|
55
|
+
return {
|
|
56
|
+
apiKey: config.apiKey || null,
|
|
57
|
+
apiUrl: config.apiUrl || DEFAULT_API_URL,
|
|
58
|
+
activeWorkspaceId: config.activeWorkspaceId || null,
|
|
59
|
+
activeProjectId: config.activeProjectId || null,
|
|
60
|
+
userEmail: config.userEmail || null,
|
|
61
|
+
memoryDir: config.memoryDir || null,
|
|
62
|
+
oauthAccessToken: config.oauthAccessToken || null,
|
|
63
|
+
oauthRefreshToken: config.oauthRefreshToken || null,
|
|
64
|
+
oauthExpiresAt: typeof config.oauthExpiresAt === "number" ? config.oauthExpiresAt : null,
|
|
65
|
+
oauthClientId: config.oauthClientId || null
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
return emptyConfig();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function saveConfig(config) {
|
|
72
|
+
const configDir = getConfigDir();
|
|
73
|
+
const configPath = getConfigPath();
|
|
74
|
+
if (!existsSync(configDir)) {
|
|
75
|
+
mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
76
|
+
}
|
|
77
|
+
const existingConfig = loadConfig();
|
|
78
|
+
const newConfig = { ...existingConfig, ...config };
|
|
79
|
+
writeFileSync(configPath, JSON.stringify(newConfig, null, 2), {
|
|
80
|
+
mode: 384
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function loadLocalConfig(cwd) {
|
|
84
|
+
const localConfigPath = getLocalConfigPath(cwd);
|
|
85
|
+
if (!existsSync(localConfigPath)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const data = readFileSync(localConfigPath, "utf-8");
|
|
90
|
+
const config = JSON.parse(data);
|
|
91
|
+
return {
|
|
92
|
+
workspaceId: config.workspaceId || null,
|
|
93
|
+
projectId: config.projectId || null
|
|
94
|
+
};
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function saveLocalConfig(config, cwd) {
|
|
100
|
+
const localConfigPath = getLocalConfigPath(cwd);
|
|
101
|
+
const existingConfig = loadLocalConfig(cwd) || {
|
|
102
|
+
workspaceId: null,
|
|
103
|
+
projectId: null
|
|
104
|
+
};
|
|
105
|
+
const newConfig = { ...existingConfig, ...config };
|
|
106
|
+
const cleanConfig = {};
|
|
107
|
+
if (newConfig.workspaceId)
|
|
108
|
+
cleanConfig.workspaceId = newConfig.workspaceId;
|
|
109
|
+
if (newConfig.projectId)
|
|
110
|
+
cleanConfig.projectId = newConfig.projectId;
|
|
111
|
+
writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
|
|
112
|
+
}
|
|
113
|
+
function hasLocalConfig(cwd) {
|
|
114
|
+
return existsSync(getLocalConfigPath(cwd));
|
|
115
|
+
}
|
|
116
|
+
function getActiveCredential() {
|
|
117
|
+
const config = loadConfig();
|
|
118
|
+
if (config.oauthAccessToken)
|
|
119
|
+
return config.oauthAccessToken;
|
|
120
|
+
if (config.apiKey)
|
|
121
|
+
return config.apiKey;
|
|
122
|
+
throw new Error(`Not configured. Run "npx @gethmy/mcp setup" to connect Harmony.
|
|
123
|
+
` + "Setup authorizes in your browser — no API key handling required.");
|
|
124
|
+
}
|
|
125
|
+
function getApiKey() {
|
|
126
|
+
return getActiveCredential();
|
|
127
|
+
}
|
|
128
|
+
function getApiUrl() {
|
|
129
|
+
const config = loadConfig();
|
|
130
|
+
return config.apiUrl;
|
|
131
|
+
}
|
|
132
|
+
function getUserEmail() {
|
|
133
|
+
const config = loadConfig();
|
|
134
|
+
return config.userEmail;
|
|
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
|
+
|
|
20
213
|
// src/prompt-builder.ts
|
|
21
214
|
var exports_prompt_builder = {};
|
|
22
215
|
__export(exports_prompt_builder, {
|
|
@@ -507,194 +700,294 @@ var init_prompt_builder = __esm(() => {
|
|
|
507
700
|
};
|
|
508
701
|
});
|
|
509
702
|
|
|
510
|
-
// src/
|
|
511
|
-
import {
|
|
512
|
-
import {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
function loadConfig() {
|
|
530
|
-
const configPath = getConfigPath();
|
|
531
|
-
if (!existsSync(configPath)) {
|
|
532
|
-
return {
|
|
533
|
-
apiKey: null,
|
|
534
|
-
apiUrl: DEFAULT_API_URL,
|
|
535
|
-
activeWorkspaceId: null,
|
|
536
|
-
activeProjectId: null,
|
|
537
|
-
userEmail: null,
|
|
538
|
-
memoryDir: null
|
|
539
|
-
};
|
|
540
|
-
}
|
|
703
|
+
// src/oauth-login.ts
|
|
704
|
+
import { spawn } from "node:child_process";
|
|
705
|
+
import { createHash as createHash3, randomBytes } from "node:crypto";
|
|
706
|
+
import { createServer } from "node:http";
|
|
707
|
+
function base64Url(buf) {
|
|
708
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
709
|
+
}
|
|
710
|
+
function generatePkce() {
|
|
711
|
+
const verifier = base64Url(randomBytes(32));
|
|
712
|
+
const challenge = base64Url(createHash3("sha256").update(verifier).digest());
|
|
713
|
+
return { verifier, challenge };
|
|
714
|
+
}
|
|
715
|
+
function oauthBaseFromApiUrl(apiUrl) {
|
|
716
|
+
return `${new URL(apiUrl).origin}/oauth`;
|
|
717
|
+
}
|
|
718
|
+
function openBrowser(url) {
|
|
719
|
+
const platform = process.platform;
|
|
720
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
721
|
+
const args = platform === "win32" ? ["/c", "start", "", url.replace(/&/g, "^&")] : [url];
|
|
541
722
|
try {
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
apiUrl: config.apiUrl || DEFAULT_API_URL,
|
|
547
|
-
activeWorkspaceId: config.activeWorkspaceId || null,
|
|
548
|
-
activeProjectId: config.activeProjectId || null,
|
|
549
|
-
userEmail: config.userEmail || null,
|
|
550
|
-
memoryDir: config.memoryDir || null
|
|
551
|
-
};
|
|
552
|
-
} catch {
|
|
553
|
-
return {
|
|
554
|
-
apiKey: null,
|
|
555
|
-
apiUrl: DEFAULT_API_URL,
|
|
556
|
-
activeWorkspaceId: null,
|
|
557
|
-
activeProjectId: null,
|
|
558
|
-
userEmail: null,
|
|
559
|
-
memoryDir: null
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
function saveConfig(config) {
|
|
564
|
-
const configDir = getConfigDir();
|
|
565
|
-
const configPath = getConfigPath();
|
|
566
|
-
if (!existsSync(configDir)) {
|
|
567
|
-
mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
568
|
-
}
|
|
569
|
-
const existingConfig = loadConfig();
|
|
570
|
-
const newConfig = { ...existingConfig, ...config };
|
|
571
|
-
writeFileSync(configPath, JSON.stringify(newConfig, null, 2), {
|
|
572
|
-
mode: 384
|
|
573
|
-
});
|
|
723
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
724
|
+
child.on("error", () => {});
|
|
725
|
+
child.unref();
|
|
726
|
+
} catch {}
|
|
574
727
|
}
|
|
575
|
-
function
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
728
|
+
async function loginWithBrowser(opts) {
|
|
729
|
+
const base = oauthBaseFromApiUrl(opts.apiUrl);
|
|
730
|
+
const { verifier, challenge } = generatePkce();
|
|
731
|
+
const state = base64Url(randomBytes(16));
|
|
732
|
+
const { server, port, waitForCode } = await startLoopbackServer(state);
|
|
733
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
580
734
|
try {
|
|
581
|
-
const
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
return existsSync(getLocalConfigPath(cwd));
|
|
607
|
-
}
|
|
608
|
-
function getApiKey() {
|
|
609
|
-
const config = loadConfig();
|
|
610
|
-
if (!config.apiKey) {
|
|
611
|
-
throw new Error(`Not configured. Run "npx @gethmy/mcp setup" to set your API key.
|
|
612
|
-
` + "You can generate an API key at https://gethmy.com → Settings → API Keys.");
|
|
735
|
+
const clientId = await registerClient(base, redirectUri);
|
|
736
|
+
const authorizeUrl = new URL(`${base}/authorize`);
|
|
737
|
+
authorizeUrl.searchParams.set("response_type", "code");
|
|
738
|
+
authorizeUrl.searchParams.set("client_id", clientId);
|
|
739
|
+
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
740
|
+
authorizeUrl.searchParams.set("code_challenge", challenge);
|
|
741
|
+
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
742
|
+
authorizeUrl.searchParams.set("state", state);
|
|
743
|
+
authorizeUrl.searchParams.set("scope", "mcp");
|
|
744
|
+
authorizeUrl.searchParams.set("resource", MCP_RESOURCE_URL);
|
|
745
|
+
if (opts.workspaceId) {
|
|
746
|
+
authorizeUrl.searchParams.set("workspace_id", opts.workspaceId);
|
|
747
|
+
}
|
|
748
|
+
const authUrlStr = authorizeUrl.toString();
|
|
749
|
+
opts.onUrl?.(authUrlStr);
|
|
750
|
+
openBrowser(authUrlStr);
|
|
751
|
+
const code = await waitForCode;
|
|
752
|
+
return await exchangeCode(base, {
|
|
753
|
+
code,
|
|
754
|
+
redirectUri,
|
|
755
|
+
clientId,
|
|
756
|
+
verifier
|
|
757
|
+
});
|
|
758
|
+
} finally {
|
|
759
|
+
server.close();
|
|
613
760
|
}
|
|
614
|
-
return config.apiKey;
|
|
615
|
-
}
|
|
616
|
-
function getApiUrl() {
|
|
617
|
-
const config = loadConfig();
|
|
618
|
-
return config.apiUrl;
|
|
619
761
|
}
|
|
620
|
-
function
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
762
|
+
async function registerClient(base, redirectUri) {
|
|
763
|
+
const res = await fetch(`${base}/register`, {
|
|
764
|
+
method: "POST",
|
|
765
|
+
headers: { "Content-Type": "application/json" },
|
|
766
|
+
body: JSON.stringify({
|
|
767
|
+
client_name: "Harmony CLI (setup)",
|
|
768
|
+
redirect_uris: [redirectUri],
|
|
769
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
770
|
+
response_types: ["code"],
|
|
771
|
+
token_endpoint_auth_method: "none"
|
|
772
|
+
})
|
|
773
|
+
});
|
|
774
|
+
const body = await res.json().catch(() => ({}));
|
|
775
|
+
if (!res.ok || !body.client_id) {
|
|
776
|
+
throw new Error(body.error_description || `Could not register OAuth client (HTTP ${res.status})`);
|
|
629
777
|
}
|
|
778
|
+
return body.client_id;
|
|
630
779
|
}
|
|
631
|
-
function
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
780
|
+
async function exchangeCode(base, params) {
|
|
781
|
+
const res = await fetch(`${base}/token`, {
|
|
782
|
+
method: "POST",
|
|
783
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
784
|
+
body: new URLSearchParams({
|
|
785
|
+
grant_type: "authorization_code",
|
|
786
|
+
code: params.code,
|
|
787
|
+
redirect_uri: params.redirectUri,
|
|
788
|
+
client_id: params.clientId,
|
|
789
|
+
code_verifier: params.verifier
|
|
790
|
+
}).toString()
|
|
791
|
+
});
|
|
792
|
+
const body = await res.json().catch(() => ({}));
|
|
793
|
+
if (!res.ok || !body.access_token || !body.refresh_token) {
|
|
794
|
+
throw new Error(body.error_description || `Token exchange failed (HTTP ${res.status})`);
|
|
636
795
|
}
|
|
796
|
+
return {
|
|
797
|
+
accessToken: body.access_token,
|
|
798
|
+
refreshToken: body.refresh_token,
|
|
799
|
+
expiresAt: Date.now() + (body.expires_in ?? 0) * 1000,
|
|
800
|
+
clientId: params.clientId
|
|
801
|
+
};
|
|
637
802
|
}
|
|
638
|
-
function
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
803
|
+
function startLoopbackServer(expectedState) {
|
|
804
|
+
return new Promise((resolveOuter, rejectOuter) => {
|
|
805
|
+
let resolveCode;
|
|
806
|
+
let rejectCode;
|
|
807
|
+
const waitForCode = new Promise((res, rej) => {
|
|
808
|
+
resolveCode = res;
|
|
809
|
+
rejectCode = rej;
|
|
810
|
+
});
|
|
811
|
+
const timeout = setTimeout(() => {
|
|
812
|
+
rejectCode(new Error("Timed out waiting for browser authorization (5 min)."));
|
|
813
|
+
}, LOGIN_TIMEOUT_MS);
|
|
814
|
+
timeout.unref?.();
|
|
815
|
+
const server = createServer((req, res) => {
|
|
816
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
817
|
+
if (url.pathname !== "/callback") {
|
|
818
|
+
res.writeHead(404).end();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
clearTimeout(timeout);
|
|
822
|
+
const error = url.searchParams.get("error");
|
|
823
|
+
const code = url.searchParams.get("code");
|
|
824
|
+
const state = url.searchParams.get("state");
|
|
825
|
+
const fail = (msg) => {
|
|
826
|
+
res.writeHead(400, { "Content-Type": "text/html" }).end(FAILURE_HTML);
|
|
827
|
+
rejectCode(new Error(msg));
|
|
828
|
+
};
|
|
829
|
+
if (error) {
|
|
830
|
+
return fail(`Authorization denied: ${url.searchParams.get("error_description") || error}`);
|
|
831
|
+
}
|
|
832
|
+
if (!state || state !== expectedState) {
|
|
833
|
+
return fail("State mismatch — possible CSRF, aborting.");
|
|
834
|
+
}
|
|
835
|
+
if (!code) {
|
|
836
|
+
return fail("No authorization code returned.");
|
|
837
|
+
}
|
|
838
|
+
res.writeHead(200, { "Content-Type": "text/html" }).end(SUCCESS_HTML);
|
|
839
|
+
resolveCode(code);
|
|
840
|
+
});
|
|
841
|
+
server.on("error", (err) => {
|
|
842
|
+
clearTimeout(timeout);
|
|
843
|
+
rejectOuter(err);
|
|
844
|
+
});
|
|
845
|
+
server.listen(0, "127.0.0.1", () => {
|
|
846
|
+
const addr = server.address();
|
|
847
|
+
resolveOuter({ server, port: addr.port, waitForCode });
|
|
848
|
+
});
|
|
849
|
+
});
|
|
644
850
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
851
|
+
var MCP_RESOURCE_URL, LOGIN_TIMEOUT_MS, SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Harmony connected</title></head>
|
|
852
|
+
<body style="font-family:system-ui,sans-serif;background:#0f1729;color:#dce4ef;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
|
|
853
|
+
<div style="text-align:center"><h1 style="color:#57b8a5">✓ Connected to Harmony</h1>
|
|
854
|
+
<p>You can close this tab and return to your terminal.</p></div></body></html>`, FAILURE_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Harmony</title></head>
|
|
855
|
+
<body style="font-family:system-ui,sans-serif;background:#0f1729;color:#dce4ef;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
|
|
856
|
+
<div style="text-align:center"><h1 style="color:#ff6b6b">Authorization failed</h1>
|
|
857
|
+
<p>Return to your terminal for details.</p></div></body></html>`;
|
|
858
|
+
var init_oauth_login = __esm(() => {
|
|
859
|
+
MCP_RESOURCE_URL = process.env.HARMONY_MCP_RESOURCE || "https://mcp.gethmy.com";
|
|
860
|
+
LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// src/oauth-refresh.ts
|
|
864
|
+
var exports_oauth_refresh = {};
|
|
865
|
+
__export(exports_oauth_refresh, {
|
|
866
|
+
refreshOAuthToken: () => refreshOAuthToken
|
|
867
|
+
});
|
|
868
|
+
import {
|
|
869
|
+
closeSync,
|
|
870
|
+
openSync,
|
|
871
|
+
renameSync,
|
|
872
|
+
rmSync as rmSync2,
|
|
873
|
+
statSync,
|
|
874
|
+
writeSync
|
|
875
|
+
} from "node:fs";
|
|
876
|
+
import { join as join3 } from "node:path";
|
|
877
|
+
function lockPath() {
|
|
878
|
+
return join3(getConfigDir(), LOCK_FILENAME);
|
|
651
879
|
}
|
|
652
|
-
function
|
|
653
|
-
|
|
654
|
-
return !!config.apiKey;
|
|
880
|
+
function sleep(ms) {
|
|
881
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
655
882
|
}
|
|
656
|
-
function
|
|
657
|
-
const
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
883
|
+
async function withRefreshLock(fn) {
|
|
884
|
+
const path = lockPath();
|
|
885
|
+
const deadline = Date.now() + LOCK_ACQUIRE_TIMEOUT_MS;
|
|
886
|
+
let held = false;
|
|
887
|
+
while (Date.now() < deadline) {
|
|
888
|
+
try {
|
|
889
|
+
const fd = openSync(path, "wx");
|
|
890
|
+
writeSync(fd, String(process.pid));
|
|
891
|
+
closeSync(fd);
|
|
892
|
+
held = true;
|
|
893
|
+
break;
|
|
894
|
+
} catch (err) {
|
|
895
|
+
if (err.code !== "EEXIST") {
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
try {
|
|
899
|
+
const age = Date.now() - statSync(path).mtimeMs;
|
|
900
|
+
if (age > LOCK_STALE_MS) {
|
|
901
|
+
const claim = `${path}.stale.${process.pid}`;
|
|
902
|
+
try {
|
|
903
|
+
renameSync(path, claim);
|
|
904
|
+
rmSync2(claim, { force: true });
|
|
905
|
+
} catch {}
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
} catch {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
await sleep(LOCK_RETRY_MS);
|
|
912
|
+
}
|
|
680
913
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
914
|
+
try {
|
|
915
|
+
return await fn();
|
|
916
|
+
} finally {
|
|
917
|
+
if (held) {
|
|
918
|
+
try {
|
|
919
|
+
rmSync2(path, { force: true });
|
|
920
|
+
} catch {}
|
|
921
|
+
}
|
|
685
922
|
}
|
|
686
|
-
return { installed: false, location: null, paths: [] };
|
|
687
923
|
}
|
|
688
|
-
function
|
|
689
|
-
|
|
690
|
-
|
|
924
|
+
function refreshOAuthToken() {
|
|
925
|
+
if (inFlight)
|
|
926
|
+
return inFlight;
|
|
927
|
+
inFlight = doRefresh().finally(() => {
|
|
928
|
+
inFlight = null;
|
|
929
|
+
});
|
|
930
|
+
return inFlight;
|
|
691
931
|
}
|
|
692
|
-
function
|
|
693
|
-
const
|
|
694
|
-
if (
|
|
695
|
-
return
|
|
696
|
-
return
|
|
932
|
+
async function doRefresh() {
|
|
933
|
+
const before = loadConfig();
|
|
934
|
+
if (!before.oauthRefreshToken || !before.oauthClientId)
|
|
935
|
+
return null;
|
|
936
|
+
return withRefreshLock(async () => {
|
|
937
|
+
const config = loadConfig();
|
|
938
|
+
if (config.oauthRefreshToken !== before.oauthRefreshToken && config.oauthAccessToken) {
|
|
939
|
+
return config.oauthAccessToken;
|
|
940
|
+
}
|
|
941
|
+
if (!config.oauthRefreshToken || !config.oauthClientId)
|
|
942
|
+
return null;
|
|
943
|
+
const base = oauthBaseFromApiUrl(config.apiUrl);
|
|
944
|
+
let res;
|
|
945
|
+
try {
|
|
946
|
+
res = await fetch(`${base}/token`, {
|
|
947
|
+
method: "POST",
|
|
948
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
949
|
+
body: new URLSearchParams({
|
|
950
|
+
grant_type: "refresh_token",
|
|
951
|
+
refresh_token: config.oauthRefreshToken,
|
|
952
|
+
client_id: config.oauthClientId
|
|
953
|
+
}).toString()
|
|
954
|
+
});
|
|
955
|
+
} catch {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
if (!res.ok) {
|
|
959
|
+
const errorCode = await res.json().then((b) => b?.error ?? null).catch(() => null);
|
|
960
|
+
if (errorCode === "invalid_grant") {
|
|
961
|
+
saveConfig({
|
|
962
|
+
oauthAccessToken: null,
|
|
963
|
+
oauthRefreshToken: null,
|
|
964
|
+
oauthExpiresAt: null,
|
|
965
|
+
oauthClientId: null
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
const body = await res.json().catch(() => null);
|
|
971
|
+
if (!body?.access_token || !body.refresh_token)
|
|
972
|
+
return null;
|
|
973
|
+
saveConfig({
|
|
974
|
+
oauthAccessToken: body.access_token,
|
|
975
|
+
oauthRefreshToken: body.refresh_token,
|
|
976
|
+
oauthExpiresAt: Date.now() + (body.expires_in ?? 0) * 1000
|
|
977
|
+
});
|
|
978
|
+
return body.access_token;
|
|
979
|
+
});
|
|
697
980
|
}
|
|
981
|
+
var inFlight = null, LOCK_FILENAME = "refresh.lock", LOCK_STALE_MS = 30000, LOCK_ACQUIRE_TIMEOUT_MS = 35000, LOCK_RETRY_MS = 100;
|
|
982
|
+
var init_oauth_refresh = __esm(() => {
|
|
983
|
+
init_config();
|
|
984
|
+
init_oauth_login();
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
// src/cli.ts
|
|
988
|
+
init_config();
|
|
989
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
990
|
+
import { program } from "commander";
|
|
698
991
|
|
|
699
992
|
// src/server.ts
|
|
700
993
|
import { readFile } from "node:fs/promises";
|
|
@@ -1205,6 +1498,7 @@ var TIMINGS = {
|
|
|
1205
1498
|
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
1206
1499
|
};
|
|
1207
1500
|
// src/api-client.ts
|
|
1501
|
+
init_config();
|
|
1208
1502
|
var RETRY_CONFIG = {
|
|
1209
1503
|
maxRetries: 3,
|
|
1210
1504
|
baseDelayMs: 1000,
|
|
@@ -1224,7 +1518,7 @@ function getRetryDelay(attempt) {
|
|
|
1224
1518
|
const delay = Math.min(RETRY_CONFIG.baseDelayMs * 2 ** attempt, RETRY_CONFIG.maxDelayMs);
|
|
1225
1519
|
return Math.round(delay + delay * 0.25 * (Math.random() * 2 - 1));
|
|
1226
1520
|
}
|
|
1227
|
-
var
|
|
1521
|
+
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1228
1522
|
|
|
1229
1523
|
class Semaphore {
|
|
1230
1524
|
permits;
|
|
@@ -1289,10 +1583,12 @@ class HarmonyApiClient {
|
|
|
1289
1583
|
apiKey;
|
|
1290
1584
|
apiUrl;
|
|
1291
1585
|
onUnauthorized;
|
|
1586
|
+
refreshCredential;
|
|
1292
1587
|
constructor(options) {
|
|
1293
1588
|
this.apiKey = options?.apiKey ?? getApiKey();
|
|
1294
1589
|
this.apiUrl = options?.apiUrl ?? getApiUrl();
|
|
1295
1590
|
this.onUnauthorized = options?.onUnauthorized;
|
|
1591
|
+
this.refreshCredential = options?.refreshCredential;
|
|
1296
1592
|
}
|
|
1297
1593
|
getApiUrl() {
|
|
1298
1594
|
return this.apiUrl;
|
|
@@ -1322,6 +1618,7 @@ class HarmonyApiClient {
|
|
|
1322
1618
|
async requestWithRetry(method, path, body, options) {
|
|
1323
1619
|
const url = `${this.apiUrl}/v1${path}`;
|
|
1324
1620
|
let lastError = null;
|
|
1621
|
+
let refreshed = false;
|
|
1325
1622
|
const contentType = options?.contentType || "application/json";
|
|
1326
1623
|
const accept = options?.accept || "application/json";
|
|
1327
1624
|
for (let attempt = 0;attempt <= RETRY_CONFIG.maxRetries; attempt++) {
|
|
@@ -1350,6 +1647,15 @@ class HarmonyApiClient {
|
|
|
1350
1647
|
if (!response.ok) {
|
|
1351
1648
|
const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
|
|
1352
1649
|
if (response.status === 401) {
|
|
1650
|
+
if (this.refreshCredential && !refreshed) {
|
|
1651
|
+
refreshed = true;
|
|
1652
|
+
const fresh = await this.refreshCredential();
|
|
1653
|
+
if (fresh) {
|
|
1654
|
+
this.apiKey = fresh;
|
|
1655
|
+
attempt--;
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1353
1659
|
this.onUnauthorized?.();
|
|
1354
1660
|
throw new HarmonyUnauthorizedError(errorMsg);
|
|
1355
1661
|
}
|
|
@@ -1358,7 +1664,7 @@ class HarmonyApiClient {
|
|
|
1358
1664
|
}
|
|
1359
1665
|
lastError = new Error(errorMsg);
|
|
1360
1666
|
if (attempt < RETRY_CONFIG.maxRetries) {
|
|
1361
|
-
await
|
|
1667
|
+
await sleep2(getRetryDelay(attempt));
|
|
1362
1668
|
continue;
|
|
1363
1669
|
}
|
|
1364
1670
|
throw lastError;
|
|
@@ -1372,7 +1678,7 @@ class HarmonyApiClient {
|
|
|
1372
1678
|
if (!isRetryableError(error))
|
|
1373
1679
|
throw lastError;
|
|
1374
1680
|
if (attempt < RETRY_CONFIG.maxRetries) {
|
|
1375
|
-
await
|
|
1681
|
+
await sleep2(getRetryDelay(attempt));
|
|
1376
1682
|
}
|
|
1377
1683
|
}
|
|
1378
1684
|
}
|
|
@@ -1381,6 +1687,7 @@ class HarmonyApiClient {
|
|
|
1381
1687
|
async requestRawWithRetry(method, path, body, options) {
|
|
1382
1688
|
const url = `${this.apiUrl}/v1${path}`;
|
|
1383
1689
|
let lastError = null;
|
|
1690
|
+
let refreshed = false;
|
|
1384
1691
|
const contentType = options?.contentType || "application/json";
|
|
1385
1692
|
const accept = options?.accept || "text/markdown";
|
|
1386
1693
|
for (let attempt = 0;attempt <= RETRY_CONFIG.maxRetries; attempt++) {
|
|
@@ -1403,6 +1710,15 @@ class HarmonyApiClient {
|
|
|
1403
1710
|
errorMsg = text || `API error: ${response.status}`;
|
|
1404
1711
|
}
|
|
1405
1712
|
if (response.status === 401) {
|
|
1713
|
+
if (this.refreshCredential && !refreshed) {
|
|
1714
|
+
refreshed = true;
|
|
1715
|
+
const fresh = await this.refreshCredential();
|
|
1716
|
+
if (fresh) {
|
|
1717
|
+
this.apiKey = fresh;
|
|
1718
|
+
attempt--;
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1406
1722
|
this.onUnauthorized?.();
|
|
1407
1723
|
throw new HarmonyUnauthorizedError(errorMsg);
|
|
1408
1724
|
}
|
|
@@ -1411,7 +1727,7 @@ class HarmonyApiClient {
|
|
|
1411
1727
|
}
|
|
1412
1728
|
lastError = new Error(errorMsg);
|
|
1413
1729
|
if (attempt < RETRY_CONFIG.maxRetries) {
|
|
1414
|
-
await
|
|
1730
|
+
await sleep2(getRetryDelay(attempt));
|
|
1415
1731
|
continue;
|
|
1416
1732
|
}
|
|
1417
1733
|
throw lastError;
|
|
@@ -1422,7 +1738,7 @@ class HarmonyApiClient {
|
|
|
1422
1738
|
if (!isRetryableError(error))
|
|
1423
1739
|
throw lastError;
|
|
1424
1740
|
if (attempt < RETRY_CONFIG.maxRetries) {
|
|
1425
|
-
await
|
|
1741
|
+
await sleep2(getRetryDelay(attempt));
|
|
1426
1742
|
}
|
|
1427
1743
|
}
|
|
1428
1744
|
}
|
|
@@ -2004,7 +2320,12 @@ async function loadPromptModules() {
|
|
|
2004
2320
|
var client2 = null;
|
|
2005
2321
|
function getClient() {
|
|
2006
2322
|
if (!client2) {
|
|
2007
|
-
client2 = new HarmonyApiClient
|
|
2323
|
+
client2 = new HarmonyApiClient({
|
|
2324
|
+
refreshCredential: async () => {
|
|
2325
|
+
const { refreshOAuthToken: refreshOAuthToken2 } = await Promise.resolve().then(() => (init_oauth_refresh(), exports_oauth_refresh));
|
|
2326
|
+
return refreshOAuthToken2();
|
|
2327
|
+
}
|
|
2328
|
+
});
|
|
2008
2329
|
}
|
|
2009
2330
|
return client2;
|
|
2010
2331
|
}
|
|
@@ -2183,6 +2504,9 @@ async function autoEndSession(scope, client3, cardId, status) {
|
|
|
2183
2504
|
} catch {}
|
|
2184
2505
|
}
|
|
2185
2506
|
|
|
2507
|
+
// src/server.ts
|
|
2508
|
+
init_config();
|
|
2509
|
+
|
|
2186
2510
|
// src/graph-expansion.ts
|
|
2187
2511
|
async function autoExpandGraph(client3, entityId, title, content, _tags, workspaceId, projectId, maxRelations = 5) {
|
|
2188
2512
|
try {
|
|
@@ -2193,14 +2517,14 @@ async function autoExpandGraph(client3, entityId, title, content, _tags, workspa
|
|
|
2193
2517
|
project_id: projectId,
|
|
2194
2518
|
limit: 20
|
|
2195
2519
|
});
|
|
2196
|
-
candidates = entities.filter((e) => e.id !== entityId).slice(0, maxRelations);
|
|
2520
|
+
candidates = entities.filter((e) => e.id !== entityId && (e.confidence ?? 1) >= 0.4).slice(0, maxRelations);
|
|
2197
2521
|
if (candidates.length === 0) {
|
|
2198
2522
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2199
2523
|
const retry = await client3.searchMemoryEntities(workspaceId, query, {
|
|
2200
2524
|
project_id: projectId,
|
|
2201
2525
|
limit: 20
|
|
2202
2526
|
});
|
|
2203
|
-
candidates = retry.entities.filter((e) => e.id !== entityId).slice(0, maxRelations);
|
|
2527
|
+
candidates = retry.entities.filter((e) => e.id !== entityId && (e.confidence ?? 1) >= 0.4).slice(0, maxRelations);
|
|
2204
2528
|
}
|
|
2205
2529
|
let relationsCreated = 0;
|
|
2206
2530
|
for (const candidate of candidates) {
|
|
@@ -2737,6 +3061,7 @@ function lintTags(tags) {
|
|
|
2737
3061
|
}
|
|
2738
3062
|
|
|
2739
3063
|
// src/onboard.ts
|
|
3064
|
+
init_config();
|
|
2740
3065
|
async function onboardNewUser(params) {
|
|
2741
3066
|
const {
|
|
2742
3067
|
email,
|
|
@@ -2782,19 +3107,20 @@ import {
|
|
|
2782
3107
|
existsSync as existsSync4,
|
|
2783
3108
|
mkdirSync as mkdirSync3,
|
|
2784
3109
|
readFileSync as readFileSync4,
|
|
2785
|
-
renameSync,
|
|
3110
|
+
renameSync as renameSync2,
|
|
2786
3111
|
writeFileSync as writeFileSync3
|
|
2787
3112
|
} from "node:fs";
|
|
2788
3113
|
import { homedir as homedir3 } from "node:os";
|
|
2789
|
-
import { dirname, join as
|
|
3114
|
+
import { dirname, join as join5 } from "node:path";
|
|
3115
|
+
init_config();
|
|
2790
3116
|
|
|
2791
3117
|
// src/hmy-config.ts
|
|
2792
3118
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
2793
3119
|
import { homedir as homedir2 } from "node:os";
|
|
2794
|
-
import { join as
|
|
3120
|
+
import { join as join4 } from "node:path";
|
|
2795
3121
|
var DEFAULTS = { updateCheck: true, pin: null };
|
|
2796
3122
|
function getHmyConfigPath() {
|
|
2797
|
-
return
|
|
3123
|
+
return join4(homedir2(), ".hmy", "config.yaml");
|
|
2798
3124
|
}
|
|
2799
3125
|
function loadHmyConfig() {
|
|
2800
3126
|
const path = getHmyConfigPath();
|
|
@@ -2937,7 +3263,7 @@ function atomicWrite(filePath, content) {
|
|
|
2937
3263
|
mkdirSync3(dir, { recursive: true });
|
|
2938
3264
|
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
2939
3265
|
writeFileSync3(tmp, content);
|
|
2940
|
-
|
|
3266
|
+
renameSync2(tmp, filePath);
|
|
2941
3267
|
}
|
|
2942
3268
|
function hasMetadataVersion(content) {
|
|
2943
3269
|
return parseSkillVersion(content) !== null;
|
|
@@ -2977,9 +3303,9 @@ function findSkillFiles(paths, knownNames) {
|
|
|
2977
3303
|
}
|
|
2978
3304
|
return results;
|
|
2979
3305
|
}
|
|
2980
|
-
var HMY_DIR =
|
|
2981
|
-
var HMY_VERSION_FILE =
|
|
2982
|
-
var LAST_CHECK_FILE =
|
|
3306
|
+
var HMY_DIR = join5(homedir3(), ".hmy");
|
|
3307
|
+
var HMY_VERSION_FILE = join5(HMY_DIR, "VERSION");
|
|
3308
|
+
var LAST_CHECK_FILE = join5(HMY_DIR, "last-update-check");
|
|
2983
3309
|
var CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
2984
3310
|
function checkedRecently(now = Date.now()) {
|
|
2985
3311
|
try {
|
|
@@ -3012,7 +3338,7 @@ async function refreshSkills(opts = {}) {
|
|
|
3012
3338
|
const status = areSkillsInstalled();
|
|
3013
3339
|
if (!status.installed)
|
|
3014
3340
|
return { updated: false };
|
|
3015
|
-
const client3 =
|
|
3341
|
+
const client3 = getClient();
|
|
3016
3342
|
const versionInfo = await client3.fetchSkillsVersion();
|
|
3017
3343
|
recordCheck();
|
|
3018
3344
|
const skillFiles = findSkillFiles(status.paths, versionInfo.skills);
|
|
@@ -4875,7 +5201,7 @@ async function handleToolCall(name, args, deps) {
|
|
|
4875
5201
|
case "harmony_delete_label": {
|
|
4876
5202
|
const labelId = z.string().uuid().parse(args.labelId);
|
|
4877
5203
|
const result = await client3.deleteLabel(labelId);
|
|
4878
|
-
return {
|
|
5204
|
+
return { ...result };
|
|
4879
5205
|
}
|
|
4880
5206
|
case "harmony_add_label_to_card": {
|
|
4881
5207
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
@@ -5832,8 +6158,8 @@ async function handleToolCall(name, args, deps) {
|
|
|
5832
6158
|
content: summary,
|
|
5833
6159
|
type: "lesson",
|
|
5834
6160
|
scope: "project",
|
|
5835
|
-
workspaceId,
|
|
5836
|
-
|
|
6161
|
+
workspace_id: workspaceId,
|
|
6162
|
+
project_id: plan.project_id,
|
|
5837
6163
|
tags: ["plan", "archived"],
|
|
5838
6164
|
confidence: 0.8
|
|
5839
6165
|
});
|
|
@@ -6015,7 +6341,7 @@ class HarmonyMCPServer {
|
|
|
6015
6341
|
}
|
|
6016
6342
|
|
|
6017
6343
|
// src/tui/setup.ts
|
|
6018
|
-
import { createHash as
|
|
6344
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
6019
6345
|
import {
|
|
6020
6346
|
existsSync as existsSync8,
|
|
6021
6347
|
lstatSync,
|
|
@@ -6024,20 +6350,22 @@ import {
|
|
|
6024
6350
|
unlinkSync
|
|
6025
6351
|
} from "node:fs";
|
|
6026
6352
|
import { homedir as homedir6 } from "node:os";
|
|
6027
|
-
import { dirname as dirname3, join as
|
|
6353
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
6028
6354
|
import * as p3 from "@clack/prompts";
|
|
6355
|
+
init_config();
|
|
6356
|
+
init_oauth_login();
|
|
6029
6357
|
|
|
6030
6358
|
// src/tui/agents.ts
|
|
6031
6359
|
import { existsSync as existsSync5 } from "node:fs";
|
|
6032
6360
|
import { homedir as homedir4 } from "node:os";
|
|
6033
|
-
import { join as
|
|
6361
|
+
import { join as join6 } from "node:path";
|
|
6034
6362
|
var AGENT_DEFINITIONS = [
|
|
6035
6363
|
{
|
|
6036
6364
|
id: "claude",
|
|
6037
6365
|
name: "Claude Code",
|
|
6038
6366
|
description: "Anthropic CLI agent",
|
|
6039
6367
|
hint: "/hmy <card>",
|
|
6040
|
-
globalPaths: [
|
|
6368
|
+
globalPaths: [join6(homedir4(), ".claude")],
|
|
6041
6369
|
localPaths: [".claude"]
|
|
6042
6370
|
},
|
|
6043
6371
|
{
|
|
@@ -6045,7 +6373,7 @@ var AGENT_DEFINITIONS = [
|
|
|
6045
6373
|
name: "Codex",
|
|
6046
6374
|
description: "OpenAI coding agent",
|
|
6047
6375
|
hint: "/prompts:hmy <card>",
|
|
6048
|
-
globalPaths: [
|
|
6376
|
+
globalPaths: [join6(homedir4(), ".codex")],
|
|
6049
6377
|
localPaths: ["AGENTS.md"]
|
|
6050
6378
|
},
|
|
6051
6379
|
{
|
|
@@ -6061,20 +6389,20 @@ var AGENT_DEFINITIONS = [
|
|
|
6061
6389
|
name: "Windsurf",
|
|
6062
6390
|
description: "Codeium AI IDE",
|
|
6063
6391
|
hint: "MCP tools available automatically",
|
|
6064
|
-
globalPaths: [
|
|
6392
|
+
globalPaths: [join6(homedir4(), ".codeium", "windsurf")],
|
|
6065
6393
|
localPaths: [".windsurf", ".windsurfrules"]
|
|
6066
6394
|
}
|
|
6067
6395
|
];
|
|
6068
6396
|
function detectAgents(cwd = process.cwd()) {
|
|
6069
6397
|
return AGENT_DEFINITIONS.map((def) => {
|
|
6070
6398
|
const globalPath = def.globalPaths.find((p) => existsSync5(p)) || null;
|
|
6071
|
-
const localPath = def.localPaths.find((p) => existsSync5(
|
|
6399
|
+
const localPath = def.localPaths.find((p) => existsSync5(join6(cwd, p))) || null;
|
|
6072
6400
|
return {
|
|
6073
6401
|
id: def.id,
|
|
6074
6402
|
name: def.name,
|
|
6075
6403
|
detected: !!(globalPath || localPath),
|
|
6076
6404
|
globalPath,
|
|
6077
|
-
localPath: localPath ?
|
|
6405
|
+
localPath: localPath ? join6(cwd, localPath) : null,
|
|
6078
6406
|
description: def.description,
|
|
6079
6407
|
hint: def.hint
|
|
6080
6408
|
};
|
|
@@ -6082,8 +6410,8 @@ function detectAgents(cwd = process.cwd()) {
|
|
|
6082
6410
|
}
|
|
6083
6411
|
|
|
6084
6412
|
// src/tui/docs.ts
|
|
6085
|
-
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync } from "node:fs";
|
|
6086
|
-
import { isAbsolute, join as
|
|
6413
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
|
|
6414
|
+
import { isAbsolute, join as join7, resolve, sep as sep2 } from "node:path";
|
|
6087
6415
|
import * as p from "@clack/prompts";
|
|
6088
6416
|
|
|
6089
6417
|
// src/tui/theme.ts
|
|
@@ -6188,7 +6516,7 @@ function listDirs(dirPath) {
|
|
|
6188
6516
|
if (IGNORED_DIRS.has(entry) || entry.startsWith("."))
|
|
6189
6517
|
return false;
|
|
6190
6518
|
try {
|
|
6191
|
-
return
|
|
6519
|
+
return statSync2(join7(dirPath, entry)).isDirectory();
|
|
6192
6520
|
} catch {
|
|
6193
6521
|
return false;
|
|
6194
6522
|
}
|
|
@@ -6236,25 +6564,25 @@ function describeDir(name) {
|
|
|
6236
6564
|
}
|
|
6237
6565
|
function scanProject(cwd) {
|
|
6238
6566
|
let packageManager = null;
|
|
6239
|
-
if (existsSync6(
|
|
6567
|
+
if (existsSync6(join7(cwd, "bun.lock")) || existsSync6(join7(cwd, "bun.lockb"))) {
|
|
6240
6568
|
packageManager = "bun";
|
|
6241
|
-
} else if (existsSync6(
|
|
6569
|
+
} else if (existsSync6(join7(cwd, "pnpm-lock.yaml"))) {
|
|
6242
6570
|
packageManager = "pnpm";
|
|
6243
|
-
} else if (existsSync6(
|
|
6571
|
+
} else if (existsSync6(join7(cwd, "yarn.lock"))) {
|
|
6244
6572
|
packageManager = "yarn";
|
|
6245
|
-
} else if (existsSync6(
|
|
6573
|
+
} else if (existsSync6(join7(cwd, "package.json"))) {
|
|
6246
6574
|
packageManager = "npm";
|
|
6247
6575
|
}
|
|
6248
|
-
const pkg = readJson(
|
|
6576
|
+
const pkg = readJson(join7(cwd, "package.json"));
|
|
6249
6577
|
const scripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
|
|
6250
6578
|
let language = "unknown";
|
|
6251
|
-
if (existsSync6(
|
|
6579
|
+
if (existsSync6(join7(cwd, "tsconfig.json"))) {
|
|
6252
6580
|
language = "typescript";
|
|
6253
|
-
} else if (existsSync6(
|
|
6581
|
+
} else if (existsSync6(join7(cwd, "go.mod"))) {
|
|
6254
6582
|
language = "go";
|
|
6255
|
-
} else if (existsSync6(
|
|
6583
|
+
} else if (existsSync6(join7(cwd, "Cargo.toml"))) {
|
|
6256
6584
|
language = "rust";
|
|
6257
|
-
} else if (existsSync6(
|
|
6585
|
+
} else if (existsSync6(join7(cwd, "setup.py")) || existsSync6(join7(cwd, "pyproject.toml"))) {
|
|
6258
6586
|
language = "python";
|
|
6259
6587
|
} else if (pkg) {
|
|
6260
6588
|
language = "javascript";
|
|
@@ -6290,7 +6618,7 @@ function scanProject(cwd) {
|
|
|
6290
6618
|
}
|
|
6291
6619
|
}
|
|
6292
6620
|
let linter = null;
|
|
6293
|
-
if (existsSync6(
|
|
6621
|
+
if (existsSync6(join7(cwd, "biome.json")) || existsSync6(join7(cwd, "biome.jsonc"))) {
|
|
6294
6622
|
linter = "biome";
|
|
6295
6623
|
} else {
|
|
6296
6624
|
const eslintFiles = [
|
|
@@ -6305,7 +6633,7 @@ function scanProject(cwd) {
|
|
|
6305
6633
|
"eslint.config.cjs",
|
|
6306
6634
|
"eslint.config.ts"
|
|
6307
6635
|
];
|
|
6308
|
-
if (eslintFiles.some((f) => existsSync6(
|
|
6636
|
+
if (eslintFiles.some((f) => existsSync6(join7(cwd, f)))) {
|
|
6309
6637
|
linter = "eslint";
|
|
6310
6638
|
} else {
|
|
6311
6639
|
const prettierFiles = [
|
|
@@ -6317,13 +6645,13 @@ function scanProject(cwd) {
|
|
|
6317
6645
|
"prettier.config.js",
|
|
6318
6646
|
"prettier.config.mjs"
|
|
6319
6647
|
];
|
|
6320
|
-
if (prettierFiles.some((f) => existsSync6(
|
|
6648
|
+
if (prettierFiles.some((f) => existsSync6(join7(cwd, f)))) {
|
|
6321
6649
|
linter = "prettier";
|
|
6322
6650
|
}
|
|
6323
6651
|
}
|
|
6324
6652
|
}
|
|
6325
6653
|
let indentStyle = null;
|
|
6326
|
-
const biome = readJson(
|
|
6654
|
+
const biome = readJson(join7(cwd, "biome.json")) ?? readJson(join7(cwd, "biome.jsonc"));
|
|
6327
6655
|
if (biome) {
|
|
6328
6656
|
const formatter = biome.formatter;
|
|
6329
6657
|
if (formatter) {
|
|
@@ -6333,7 +6661,7 @@ function scanProject(cwd) {
|
|
|
6333
6661
|
}
|
|
6334
6662
|
}
|
|
6335
6663
|
if (!indentStyle) {
|
|
6336
|
-
const editorConfig = readText(
|
|
6664
|
+
const editorConfig = readText(join7(cwd, ".editorconfig"));
|
|
6337
6665
|
if (editorConfig) {
|
|
6338
6666
|
const styleMatch = editorConfig.match(/indent_style\s*=\s*(space|tab)/);
|
|
6339
6667
|
const sizeMatch = editorConfig.match(/indent_size\s*=\s*(\d+)/);
|
|
@@ -6346,13 +6674,13 @@ function scanProject(cwd) {
|
|
|
6346
6674
|
}
|
|
6347
6675
|
}
|
|
6348
6676
|
const dirs = listDirs(cwd);
|
|
6349
|
-
const srcDirs = existsSync6(
|
|
6350
|
-
const monorepo = existsSync6(
|
|
6677
|
+
const srcDirs = existsSync6(join7(cwd, "src")) ? listDirs(join7(cwd, "src")) : [];
|
|
6678
|
+
const monorepo = existsSync6(join7(cwd, "packages")) || existsSync6(join7(cwd, "apps"));
|
|
6351
6679
|
const existingDocs = {
|
|
6352
|
-
agentsMd: existsSync6(
|
|
6353
|
-
claudeMd: existsSync6(
|
|
6354
|
-
docsDir: existsSync6(
|
|
6355
|
-
architectureMd: existsSync6(
|
|
6680
|
+
agentsMd: existsSync6(join7(cwd, "AGENTS.md")),
|
|
6681
|
+
claudeMd: existsSync6(join7(cwd, "CLAUDE.md")),
|
|
6682
|
+
docsDir: existsSync6(join7(cwd, "docs")),
|
|
6683
|
+
architectureMd: existsSync6(join7(cwd, "docs", "architecture.md"))
|
|
6356
6684
|
};
|
|
6357
6685
|
return {
|
|
6358
6686
|
packageManager,
|
|
@@ -6503,9 +6831,9 @@ var VAGUE_STANDARDS = [
|
|
|
6503
6831
|
];
|
|
6504
6832
|
function verifyDocs(cwd) {
|
|
6505
6833
|
const issues = [];
|
|
6506
|
-
const claudeMd = readText(
|
|
6507
|
-
const agentsMd = readText(
|
|
6508
|
-
const pkg = readJson(
|
|
6834
|
+
const claudeMd = readText(join7(cwd, "CLAUDE.md"));
|
|
6835
|
+
const agentsMd = readText(join7(cwd, "AGENTS.md"));
|
|
6836
|
+
const pkg = readJson(join7(cwd, "package.json"));
|
|
6509
6837
|
const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
|
|
6510
6838
|
const projectRoot = resolve(cwd);
|
|
6511
6839
|
if (claudeMd) {
|
|
@@ -6671,7 +6999,7 @@ function verifyDocs(cwd) {
|
|
|
6671
6999
|
}
|
|
6672
7000
|
checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
|
|
6673
7001
|
}
|
|
6674
|
-
const archMd = readText(
|
|
7002
|
+
const archMd = readText(join7(cwd, "docs", "architecture.md"));
|
|
6675
7003
|
if (archMd) {
|
|
6676
7004
|
checkBacktickPaths(archMd, "docs/architecture.md", cwd, issues);
|
|
6677
7005
|
}
|
|
@@ -6741,18 +7069,18 @@ async function runDocsStep(cwd) {
|
|
|
6741
7069
|
}
|
|
6742
7070
|
const files = [];
|
|
6743
7071
|
files.push({
|
|
6744
|
-
path:
|
|
7072
|
+
path: join7(cwd, "AGENTS.md"),
|
|
6745
7073
|
content: generateAgentsMd(info, cwd),
|
|
6746
7074
|
type: "text"
|
|
6747
7075
|
});
|
|
6748
7076
|
files.push({
|
|
6749
|
-
path:
|
|
7077
|
+
path: join7(cwd, "CLAUDE.md"),
|
|
6750
7078
|
content: generateClaudeMd(info),
|
|
6751
7079
|
type: "text"
|
|
6752
7080
|
});
|
|
6753
7081
|
if (info.dirs.includes("docs") || info.srcDirs.length > 0) {
|
|
6754
7082
|
files.push({
|
|
6755
|
-
path:
|
|
7083
|
+
path: join7(cwd, "docs", "architecture.md"),
|
|
6756
7084
|
content: generateArchitectureMd(info, cwd),
|
|
6757
7085
|
type: "text"
|
|
6758
7086
|
});
|
|
@@ -7016,7 +7344,7 @@ var SAFE_HARMONY_TOOLS = [
|
|
|
7016
7344
|
"harmony_process_command",
|
|
7017
7345
|
"harmony_sync"
|
|
7018
7346
|
];
|
|
7019
|
-
var GLOBAL_SKILLS_DIR =
|
|
7347
|
+
var GLOBAL_SKILLS_DIR = join8(homedir6(), ".agents", "skills");
|
|
7020
7348
|
var API_URL = "https://app.gethmy.com/api";
|
|
7021
7349
|
async function registerMcpServer() {
|
|
7022
7350
|
try {
|
|
@@ -7031,7 +7359,7 @@ async function registerMcpServer() {
|
|
|
7031
7359
|
}
|
|
7032
7360
|
async function writeMcpConfigFallback(home) {
|
|
7033
7361
|
const { readFileSync: readFileSync7, writeFileSync: writeFileSync5, mkdirSync: mkdirSync6, existsSync: existsSync9 } = await import("node:fs");
|
|
7034
|
-
const settingsPath =
|
|
7362
|
+
const settingsPath = join8(home, ".claude", "settings.json");
|
|
7035
7363
|
const settingsDir = dirname3(settingsPath);
|
|
7036
7364
|
if (!existsSync9(settingsDir)) {
|
|
7037
7365
|
mkdirSync6(settingsDir, { recursive: true });
|
|
@@ -7050,7 +7378,7 @@ async function writeMcpConfigFallback(home) {
|
|
|
7050
7378
|
}
|
|
7051
7379
|
async function allowlistHarmonyTools(home, allowAll) {
|
|
7052
7380
|
const { readFileSync: readFileSync7, writeFileSync: writeFileSync5, mkdirSync: mkdirSync6, existsSync: existsSync9 } = await import("node:fs");
|
|
7053
|
-
const settingsPath =
|
|
7381
|
+
const settingsPath = join8(home, ".claude", "settings.json");
|
|
7054
7382
|
const settingsDir = dirname3(settingsPath);
|
|
7055
7383
|
if (!existsSync9(settingsDir)) {
|
|
7056
7384
|
mkdirSync6(settingsDir, { recursive: true });
|
|
@@ -7168,17 +7496,17 @@ async function getAgentFiles(agentId, cwd, installMode = "global") {
|
|
|
7168
7496
|
const content = buildSkillFile(fetched);
|
|
7169
7497
|
if (installMode === "global") {
|
|
7170
7498
|
files.push({
|
|
7171
|
-
path:
|
|
7499
|
+
path: join8(GLOBAL_SKILLS_DIR, name, "SKILL.md"),
|
|
7172
7500
|
content,
|
|
7173
7501
|
type: "text"
|
|
7174
7502
|
});
|
|
7175
7503
|
symlinks.push({
|
|
7176
|
-
target:
|
|
7177
|
-
link:
|
|
7504
|
+
target: join8(GLOBAL_SKILLS_DIR, name),
|
|
7505
|
+
link: join8(home, ".claude", "skills", name)
|
|
7178
7506
|
});
|
|
7179
7507
|
} else {
|
|
7180
7508
|
files.push({
|
|
7181
|
-
path:
|
|
7509
|
+
path: join8(cwd, ".claude", "skills", name, "SKILL.md"),
|
|
7182
7510
|
content,
|
|
7183
7511
|
type: "text"
|
|
7184
7512
|
});
|
|
@@ -7196,18 +7524,18 @@ ${summary}`);
|
|
|
7196
7524
|
}
|
|
7197
7525
|
try {
|
|
7198
7526
|
const updateCheckFetched = await client3.fetchSkill("hmy-update-check");
|
|
7199
|
-
const actualHash =
|
|
7527
|
+
const actualHash = createHash4("sha256").update(updateCheckFetched.content).digest("hex");
|
|
7200
7528
|
if (actualHash !== updateCheckFetched.sha256) {
|
|
7201
7529
|
throw new Error(`hmy-update-check integrity check failed: expected ${updateCheckFetched.sha256}, got ${actualHash}`);
|
|
7202
7530
|
}
|
|
7203
7531
|
files.push({
|
|
7204
|
-
path:
|
|
7532
|
+
path: join8(home, ".hmy", "bin", "hmy-update-check"),
|
|
7205
7533
|
content: updateCheckFetched.content,
|
|
7206
7534
|
type: "text",
|
|
7207
7535
|
mode: 493
|
|
7208
7536
|
});
|
|
7209
7537
|
files.push({
|
|
7210
|
-
path:
|
|
7538
|
+
path: join8(home, ".hmy", "VERSION"),
|
|
7211
7539
|
content: versionInfo.version,
|
|
7212
7540
|
type: "text"
|
|
7213
7541
|
});
|
|
@@ -7253,7 +7581,7 @@ Skip if: work was already started with a card reference, or no matching card exi
|
|
|
7253
7581
|
- \`harmony_generate_prompt\` - Get role-based guidance and focus areas for the card
|
|
7254
7582
|
`;
|
|
7255
7583
|
files.push({
|
|
7256
|
-
path:
|
|
7584
|
+
path: join8(cwd, "AGENTS.md"),
|
|
7257
7585
|
content: agentsContent,
|
|
7258
7586
|
type: "text"
|
|
7259
7587
|
});
|
|
@@ -7270,17 +7598,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "{{card}}").replace("Your agent
|
|
|
7270
7598
|
`;
|
|
7271
7599
|
if (installMode === "global") {
|
|
7272
7600
|
files.push({
|
|
7273
|
-
path:
|
|
7601
|
+
path: join8(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
|
|
7274
7602
|
content: promptContent,
|
|
7275
7603
|
type: "text"
|
|
7276
7604
|
});
|
|
7277
7605
|
symlinks.push({
|
|
7278
|
-
target:
|
|
7279
|
-
link:
|
|
7606
|
+
target: join8(GLOBAL_SKILLS_DIR, "codex", "hmy.md"),
|
|
7607
|
+
link: join8(home, ".codex", "prompts", "hmy.md")
|
|
7280
7608
|
});
|
|
7281
7609
|
} else {
|
|
7282
7610
|
files.push({
|
|
7283
|
-
path:
|
|
7611
|
+
path: join8(home, ".codex", "prompts", "hmy.md"),
|
|
7284
7612
|
content: promptContent,
|
|
7285
7613
|
type: "text"
|
|
7286
7614
|
});
|
|
@@ -7292,7 +7620,7 @@ command = "npx"
|
|
|
7292
7620
|
args = ["-y", "@gethmy/mcp@latest", "serve"]
|
|
7293
7621
|
`;
|
|
7294
7622
|
files.push({
|
|
7295
|
-
path:
|
|
7623
|
+
path: join8(home, ".codex", "config.toml"),
|
|
7296
7624
|
content: tomlContent,
|
|
7297
7625
|
type: "toml",
|
|
7298
7626
|
tomlSection: "mcp_servers.harmony"
|
|
@@ -7301,7 +7629,7 @@ args = ["-y", "@gethmy/mcp@latest", "serve"]
|
|
|
7301
7629
|
}
|
|
7302
7630
|
case "cursor": {
|
|
7303
7631
|
files.push({
|
|
7304
|
-
path:
|
|
7632
|
+
path: join8(cwd, ".cursor", "mcp.json"),
|
|
7305
7633
|
content: JSON.stringify({
|
|
7306
7634
|
mcpServers: {
|
|
7307
7635
|
harmony: {
|
|
@@ -7327,17 +7655,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
7327
7655
|
`;
|
|
7328
7656
|
if (installMode === "global") {
|
|
7329
7657
|
files.push({
|
|
7330
|
-
path:
|
|
7658
|
+
path: join8(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
|
|
7331
7659
|
content: ruleContent,
|
|
7332
7660
|
type: "text"
|
|
7333
7661
|
});
|
|
7334
7662
|
symlinks.push({
|
|
7335
|
-
target:
|
|
7336
|
-
link:
|
|
7663
|
+
target: join8(GLOBAL_SKILLS_DIR, "cursor", "harmony.mdc"),
|
|
7664
|
+
link: join8(home, ".cursor", "rules", "harmony.mdc")
|
|
7337
7665
|
});
|
|
7338
7666
|
} else {
|
|
7339
7667
|
files.push({
|
|
7340
|
-
path:
|
|
7668
|
+
path: join8(cwd, ".cursor", "rules", "harmony.mdc"),
|
|
7341
7669
|
content: ruleContent,
|
|
7342
7670
|
type: "text"
|
|
7343
7671
|
});
|
|
@@ -7346,7 +7674,7 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
7346
7674
|
}
|
|
7347
7675
|
case "windsurf": {
|
|
7348
7676
|
files.push({
|
|
7349
|
-
path:
|
|
7677
|
+
path: join8(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
7350
7678
|
content: JSON.stringify({
|
|
7351
7679
|
mcpServers: {
|
|
7352
7680
|
harmony: {
|
|
@@ -7372,17 +7700,17 @@ ${HARMONY_WORKFLOW_PROMPT.replace("$ARGUMENTS", "the card reference").replace("Y
|
|
|
7372
7700
|
`;
|
|
7373
7701
|
if (installMode === "global") {
|
|
7374
7702
|
files.push({
|
|
7375
|
-
path:
|
|
7703
|
+
path: join8(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
|
|
7376
7704
|
content: ruleContent,
|
|
7377
7705
|
type: "text"
|
|
7378
7706
|
});
|
|
7379
7707
|
symlinks.push({
|
|
7380
|
-
target:
|
|
7381
|
-
link:
|
|
7708
|
+
target: join8(GLOBAL_SKILLS_DIR, "windsurf", "harmony.md"),
|
|
7709
|
+
link: join8(home, ".codeium", "windsurf", "rules", "harmony.md")
|
|
7382
7710
|
});
|
|
7383
7711
|
} else {
|
|
7384
7712
|
files.push({
|
|
7385
|
-
path:
|
|
7713
|
+
path: join8(cwd, ".windsurf", "rules", "harmony.md"),
|
|
7386
7714
|
content: ruleContent,
|
|
7387
7715
|
type: "text"
|
|
7388
7716
|
});
|
|
@@ -7407,39 +7735,84 @@ async function runSetup(options = {}) {
|
|
|
7407
7735
|
if (options.workspaceId || options.projectId || options.projectSlug) {
|
|
7408
7736
|
needsContext = true;
|
|
7409
7737
|
}
|
|
7410
|
-
let apiKey = options.apiKey || existingConfig.apiKey;
|
|
7738
|
+
let apiKey = options.apiKey || existingConfig.oauthAccessToken || existingConfig.apiKey;
|
|
7411
7739
|
let userEmail = options.userEmail || existingConfig.userEmail || undefined;
|
|
7412
7740
|
let selectedWorkspaceIdFromSignup;
|
|
7413
7741
|
let selectedProjectIdFromSignup;
|
|
7414
7742
|
let selectedWorkspaceNameFromSignup;
|
|
7415
7743
|
let selectedProjectNameFromSignup;
|
|
7416
7744
|
let createdNewAccount = false;
|
|
7745
|
+
let oauthTokens;
|
|
7746
|
+
if (options.apiKey) {
|
|
7747
|
+
p3.log.warn(colors.warning(`--api-key is deprecated and insecure: the key is exposed in your shell
|
|
7748
|
+
history, terminal scrollback, and the process list. Prefer the browser
|
|
7749
|
+
sign-in (run \`npx @gethmy/mcp setup\` with no --api-key). Use --api-key
|
|
7750
|
+
only for unattended CI where you accept that risk.`));
|
|
7751
|
+
}
|
|
7417
7752
|
if (needsApiKey || !apiKey || !apiKey.startsWith("hmy_")) {
|
|
7418
7753
|
let useNewAccount = options.newAccount === true;
|
|
7754
|
+
let useBrowserAuth = false;
|
|
7419
7755
|
if (!useNewAccount && options.apiKey) {
|
|
7420
7756
|
useNewAccount = false;
|
|
7421
7757
|
} else if (!useNewAccount && !options.apiKey) {
|
|
7422
7758
|
const getStarted = await p3.select({
|
|
7423
|
-
message: "How would you like to
|
|
7759
|
+
message: "How would you like to connect?",
|
|
7424
7760
|
options: [
|
|
7761
|
+
{
|
|
7762
|
+
value: "browser",
|
|
7763
|
+
label: "Sign in with your browser",
|
|
7764
|
+
hint: "recommended — secure, no key handling"
|
|
7765
|
+
},
|
|
7425
7766
|
{
|
|
7426
7767
|
value: "create",
|
|
7427
|
-
label: "Create a free account"
|
|
7428
|
-
hint: "recommended for new users"
|
|
7768
|
+
label: "Create a free account"
|
|
7429
7769
|
},
|
|
7430
7770
|
{
|
|
7431
7771
|
value: "apikey",
|
|
7432
|
-
label: "
|
|
7772
|
+
label: "Paste an API key",
|
|
7773
|
+
hint: "advanced / CI"
|
|
7433
7774
|
}
|
|
7434
|
-
]
|
|
7775
|
+
],
|
|
7776
|
+
initialValue: "browser"
|
|
7435
7777
|
});
|
|
7436
7778
|
if (p3.isCancel(getStarted)) {
|
|
7437
7779
|
p3.cancel("Setup cancelled");
|
|
7438
7780
|
process.exit(0);
|
|
7439
7781
|
}
|
|
7440
7782
|
useNewAccount = getStarted === "create";
|
|
7783
|
+
useBrowserAuth = getStarted === "browser";
|
|
7441
7784
|
}
|
|
7442
|
-
if (
|
|
7785
|
+
if (useBrowserAuth) {
|
|
7786
|
+
const spinner4 = p3.spinner();
|
|
7787
|
+
spinner4.start("Opening your browser to authorize…");
|
|
7788
|
+
try {
|
|
7789
|
+
oauthTokens = await loginWithBrowser({
|
|
7790
|
+
apiUrl: API_URL,
|
|
7791
|
+
workspaceId: options.workspaceId,
|
|
7792
|
+
onUrl: (url) => {
|
|
7793
|
+
spinner4.message(`Authorize in your browser. If it didn't open:
|
|
7794
|
+
${colors.dim(url)}`);
|
|
7795
|
+
}
|
|
7796
|
+
});
|
|
7797
|
+
apiKey = oauthTokens.accessToken;
|
|
7798
|
+
needsApiKey = true;
|
|
7799
|
+
saveConfig({
|
|
7800
|
+
apiKey: null,
|
|
7801
|
+
oauthAccessToken: oauthTokens.accessToken,
|
|
7802
|
+
oauthRefreshToken: oauthTokens.refreshToken,
|
|
7803
|
+
oauthExpiresAt: oauthTokens.expiresAt,
|
|
7804
|
+
oauthClientId: oauthTokens.clientId,
|
|
7805
|
+
apiUrl: API_URL
|
|
7806
|
+
});
|
|
7807
|
+
spinner4.stop(colors.success("Authorized"));
|
|
7808
|
+
} catch (error) {
|
|
7809
|
+
spinner4.stop(colors.error("Browser authorization failed"));
|
|
7810
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
7811
|
+
p3.log.error(msg);
|
|
7812
|
+
p3.log.info("You can retry, or run with --api-key for unattended setup.");
|
|
7813
|
+
process.exit(1);
|
|
7814
|
+
}
|
|
7815
|
+
} else if (useNewAccount) {
|
|
7443
7816
|
const fullName = options.name || await p3.text({
|
|
7444
7817
|
message: "Full name",
|
|
7445
7818
|
placeholder: "Jane Smith",
|
|
@@ -7520,6 +7893,9 @@ async function runSetup(options = {}) {
|
|
|
7520
7893
|
}
|
|
7521
7894
|
process.exit(1);
|
|
7522
7895
|
}
|
|
7896
|
+
} else if (options.apiKey) {
|
|
7897
|
+
apiKey = options.apiKey;
|
|
7898
|
+
needsApiKey = true;
|
|
7523
7899
|
} else {
|
|
7524
7900
|
const keyInput = await p3.text({
|
|
7525
7901
|
message: "Enter your Harmony API key",
|
|
@@ -7703,15 +8079,26 @@ async function runSetup(options = {}) {
|
|
|
7703
8079
|
const allFiles = [];
|
|
7704
8080
|
const allSymlinks = [];
|
|
7705
8081
|
if (needsApiKey || !alreadyConfigured) {
|
|
8082
|
+
const configToWrite = oauthTokens ? {
|
|
8083
|
+
apiKey: null,
|
|
8084
|
+
apiUrl: API_URL,
|
|
8085
|
+
userEmail: userEmail || null,
|
|
8086
|
+
activeWorkspaceId: null,
|
|
8087
|
+
activeProjectId: null,
|
|
8088
|
+
oauthAccessToken: oauthTokens.accessToken,
|
|
8089
|
+
oauthRefreshToken: oauthTokens.refreshToken,
|
|
8090
|
+
oauthExpiresAt: oauthTokens.expiresAt,
|
|
8091
|
+
oauthClientId: oauthTokens.clientId
|
|
8092
|
+
} : {
|
|
8093
|
+
apiKey,
|
|
8094
|
+
apiUrl: API_URL,
|
|
8095
|
+
userEmail: userEmail || null,
|
|
8096
|
+
activeWorkspaceId: null,
|
|
8097
|
+
activeProjectId: null
|
|
8098
|
+
};
|
|
7706
8099
|
allFiles.push({
|
|
7707
8100
|
path: getConfigPath(),
|
|
7708
|
-
content: JSON.stringify(
|
|
7709
|
-
apiKey,
|
|
7710
|
-
apiUrl: API_URL,
|
|
7711
|
-
userEmail: userEmail || null,
|
|
7712
|
-
activeWorkspaceId: null,
|
|
7713
|
-
activeProjectId: null
|
|
7714
|
-
}, null, 2),
|
|
8101
|
+
content: JSON.stringify(configToWrite, null, 2),
|
|
7715
8102
|
type: "text"
|
|
7716
8103
|
});
|
|
7717
8104
|
}
|
|
@@ -7734,7 +8121,11 @@ async function runSetup(options = {}) {
|
|
|
7734
8121
|
console.log("");
|
|
7735
8122
|
p3.log.step("Summary");
|
|
7736
8123
|
console.log("");
|
|
7737
|
-
|
|
8124
|
+
if (oauthTokens) {
|
|
8125
|
+
console.log(` ${colors.bold("Credential:")} Browser sign-in (OAuth, workspace-scoped)`);
|
|
8126
|
+
} else {
|
|
8127
|
+
console.log(` ${colors.bold("API Key:")} ${apiKey.slice(0, 8)}...`);
|
|
8128
|
+
}
|
|
7738
8129
|
if (userEmail) {
|
|
7739
8130
|
console.log(` ${colors.bold("Email:")} ${userEmail}`);
|
|
7740
8131
|
}
|
|
@@ -7831,7 +8222,7 @@ async function runSetup(options = {}) {
|
|
|
7831
8222
|
} else {
|
|
7832
8223
|
try {
|
|
7833
8224
|
await writeMcpConfigFallback(home);
|
|
7834
|
-
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(
|
|
8225
|
+
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join8(home, ".claude", "settings.json"), home))} ${colors.dim("(updated)")}`);
|
|
7835
8226
|
} catch {
|
|
7836
8227
|
p3.log.warning("Could not register MCP server. Run manually: claude mcp add --transport stdio harmony -- npx -y @gethmy/mcp@latest serve");
|
|
7837
8228
|
}
|
|
@@ -7849,7 +8240,7 @@ async function runSetup(options = {}) {
|
|
|
7849
8240
|
try {
|
|
7850
8241
|
const result = await allowlistHarmonyTools(home, allowAll);
|
|
7851
8242
|
const scope = allowAll ? "all tools" : "safe tools";
|
|
7852
|
-
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(
|
|
8243
|
+
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(join8(home, ".claude", "settings.json"), home))} ${colors.dim(result === "added" ? `(${scope} allowlisted)` : `(${scope} already allowlisted)`)}`);
|
|
7853
8244
|
} catch {
|
|
7854
8245
|
p3.log.warning("Could not allowlist Harmony tools. Run /permissions in Claude Code and choose “always allow” for Harmony, or add mcp__harmony to permissions.allow in ~/.claude/settings.json.");
|
|
7855
8246
|
}
|
|
@@ -7941,7 +8332,11 @@ program.command("status").description("Show configuration status").action(() =>
|
|
|
7941
8332
|
console.log(`Status: Configured
|
|
7942
8333
|
`);
|
|
7943
8334
|
console.log("API:");
|
|
7944
|
-
|
|
8335
|
+
if (globalConfig.oauthAccessToken) {
|
|
8336
|
+
console.log(" Credential: Browser sign-in (OAuth, workspace-scoped)");
|
|
8337
|
+
} else {
|
|
8338
|
+
console.log(` Key: ${globalConfig.apiKey?.slice(0, 8)}...`);
|
|
8339
|
+
}
|
|
7945
8340
|
console.log(` URL: ${globalConfig.apiUrl}`);
|
|
7946
8341
|
console.log(` Email: ${globalConfig.userEmail ? maskEmail(globalConfig.userEmail) : "(not set)"}`);
|
|
7947
8342
|
console.log(`
|
|
@@ -7985,13 +8380,17 @@ program.command("reset").description("Remove stored configuration").action(() =>
|
|
|
7985
8380
|
apiKey: null,
|
|
7986
8381
|
activeWorkspaceId: null,
|
|
7987
8382
|
activeProjectId: null,
|
|
7988
|
-
userEmail: null
|
|
8383
|
+
userEmail: null,
|
|
8384
|
+
oauthAccessToken: null,
|
|
8385
|
+
oauthRefreshToken: null,
|
|
8386
|
+
oauthExpiresAt: null,
|
|
8387
|
+
oauthClientId: null
|
|
7989
8388
|
});
|
|
7990
8389
|
console.log("Configuration reset successfully");
|
|
7991
8390
|
console.log(`
|
|
7992
8391
|
To reconfigure, run: npx @gethmy/mcp setup`);
|
|
7993
8392
|
});
|
|
7994
|
-
program.command("setup").description("Smart setup wizard for Harmony MCP (recommended)").argument("[slug]", "Project slug — resolves to workspace + project in one step (e.g. harmony-6590761b)").option("-f, --force", "Overwrite existing configuration files").option("-k, --api-key <key>", "
|
|
8393
|
+
program.command("setup").description("Smart setup wizard for Harmony MCP (recommended)").argument("[slug]", "Project slug — resolves to workspace + project in one step (e.g. harmony-6590761b)").option("-f, --force", "Overwrite existing configuration files").option("-k, --api-key <key>", "DEPRECATED (insecure: key leaks via argv/shell history). For unattended CI only — interactive setup uses browser sign-in.").option("-e, --email <email>", "Your email for auto-assignment").option("-a, --agents <agents...>", "Agents to configure: claude, codex, cursor, windsurf").option("-l, --local", "Install skills locally in project directory").option("-g, --global", "Install skills globally (recommended)").option("-w, --workspace <id>", "Set workspace context (UUID)").option("-p, --project <id>", "Set project context (UUID)").option("--skip-context", "Skip workspace/project selection").option("--skip-docs", "Skip project docs scaffold/verification").option("--new", "Create a new account (skip the choice prompt)").option("-n, --name <name>", "Full name (for account creation)").option("--allow-all-tools", "Allowlist every Harmony tool (incl. destructive: delete/archive/api-key/invite) without confirmation. Default allowlists only read + routine-write tools; destructive tools keep prompting.").action(async (slug, options) => {
|
|
7995
8394
|
await runSetup({
|
|
7996
8395
|
force: options.force,
|
|
7997
8396
|
apiKey: options.apiKey,
|