@gethmy/mcp 3.5.0 → 3.6.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 -5
- package/dist/cli.js +51 -12
- package/dist/index.js +45 -8
- package/dist/lib/api-client.js +49 -8
- package/dist/lib/config.js +56 -9
- package/dist/lib/oauth-refresh.js +49 -8
- package/dist/run-hook-cli.js +1 -0
- package/package.json +1 -1
- package/src/config.ts +162 -18
- package/src/hook-install.ts +1 -1
- package/src/oauth-refresh.ts +1 -1
- package/src/run-redaction.ts +25 -3
- package/src/run-state.ts +1 -1
- package/src/server.ts +1 -1
- package/src/tui/setup.ts +12 -3
- package/src/tui/writer.ts +21 -1
package/README.md
CHANGED
|
@@ -434,7 +434,7 @@ curl -X GET "https://gethmy.com/api/v1/workspaces" \
|
|
|
434
434
|
|
|
435
435
|
### Global Configuration
|
|
436
436
|
|
|
437
|
-
Stored in `~/.
|
|
437
|
+
Stored in `~/.hmy/agent/config.json`. Browser sign-in writes OAuth tokens (and nulls `apiKey`); API-key setup writes `apiKey` instead. The server prefers a live OAuth token and refreshes it automatically:
|
|
438
438
|
|
|
439
439
|
```json
|
|
440
440
|
{
|
|
@@ -452,7 +452,7 @@ Stored in `~/.harmony-mcp/config.json`. Browser sign-in writes OAuth tokens (and
|
|
|
452
452
|
|
|
453
453
|
### Local Project Configuration
|
|
454
454
|
|
|
455
|
-
Stored in `.
|
|
455
|
+
Stored in `.hmy.json` in your project root:
|
|
456
456
|
|
|
457
457
|
```json
|
|
458
458
|
{
|
|
@@ -498,10 +498,10 @@ Skills:
|
|
|
498
498
|
~/.agents/skills/hmy/SKILL.md
|
|
499
499
|
|
|
500
500
|
Context:
|
|
501
|
-
Local config: .
|
|
501
|
+
Local config: .hmy.json
|
|
502
502
|
Workspace: my-team-id
|
|
503
503
|
Project: my-project-id
|
|
504
|
-
Global config: ~/.
|
|
504
|
+
Global config: ~/.hmy/agent/config.json
|
|
505
505
|
Workspace: (not set)
|
|
506
506
|
Project: (not set)
|
|
507
507
|
|
|
@@ -515,7 +515,7 @@ Context:
|
|
|
515
515
|
Add this to prevent accidentally committing local config:
|
|
516
516
|
|
|
517
517
|
```
|
|
518
|
-
.
|
|
518
|
+
.hmy.json
|
|
519
519
|
```
|
|
520
520
|
|
|
521
521
|
## Architecture
|
package/dist/cli.js
CHANGED
|
@@ -21,12 +21,36 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
21
21
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { dirname, join, parse, resolve } from "node:path";
|
|
24
|
+
function noteLegacyConfigDir(path) {
|
|
25
|
+
if (warnedLegacyConfigDir)
|
|
26
|
+
return;
|
|
27
|
+
warnedLegacyConfigDir = true;
|
|
28
|
+
console.error(`Harmony: reading the pre-#1082 config at ${path}. ` + `The current location is ${getConfigPath()}; ` + `run the agent daemon once to migrate, or move the file yourself.`);
|
|
29
|
+
}
|
|
30
|
+
function noteLegacyLocalPin(path) {
|
|
31
|
+
if (warnedLegacyLocalPin)
|
|
32
|
+
return;
|
|
33
|
+
warnedLegacyLocalPin = true;
|
|
34
|
+
console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} — the fallback that finds it is temporary.`);
|
|
35
|
+
}
|
|
36
|
+
function noteLocalPinRename(from, to) {
|
|
37
|
+
console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
|
|
38
|
+
}
|
|
39
|
+
function getHmyRootDir() {
|
|
40
|
+
return join(homedir(), CONFIG_DIR_NAME);
|
|
41
|
+
}
|
|
24
42
|
function getConfigDir() {
|
|
25
|
-
return join(
|
|
43
|
+
return join(getHmyRootDir(), CONFIG_DIR_SUBDIR);
|
|
44
|
+
}
|
|
45
|
+
function getLegacyConfigDir() {
|
|
46
|
+
return join(homedir(), LEGACY_CONFIG_DIR_NAME);
|
|
26
47
|
}
|
|
27
48
|
function getConfigPath() {
|
|
28
49
|
return join(getConfigDir(), "config.json");
|
|
29
50
|
}
|
|
51
|
+
function getLegacyConfigPath() {
|
|
52
|
+
return join(getLegacyConfigDir(), "config.json");
|
|
53
|
+
}
|
|
30
54
|
function getLocalConfigPath(cwd) {
|
|
31
55
|
return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
|
|
32
56
|
}
|
|
@@ -36,9 +60,14 @@ function findLocalConfigPath(cwd) {
|
|
|
36
60
|
const { root } = parse(dir);
|
|
37
61
|
for (;; ) {
|
|
38
62
|
if (dir !== home && dir !== root) {
|
|
39
|
-
const
|
|
40
|
-
if (existsSync(
|
|
41
|
-
return
|
|
63
|
+
const current = join(dir, LOCAL_CONFIG_FILENAME);
|
|
64
|
+
if (existsSync(current))
|
|
65
|
+
return current;
|
|
66
|
+
const legacy = join(dir, LEGACY_LOCAL_CONFIG_FILENAME);
|
|
67
|
+
if (existsSync(legacy)) {
|
|
68
|
+
noteLegacyLocalPin(legacy);
|
|
69
|
+
return legacy;
|
|
70
|
+
}
|
|
42
71
|
}
|
|
43
72
|
const parent = dirname(dir);
|
|
44
73
|
if (parent === dir)
|
|
@@ -61,9 +90,12 @@ function emptyConfig() {
|
|
|
61
90
|
};
|
|
62
91
|
}
|
|
63
92
|
function loadConfig() {
|
|
64
|
-
|
|
93
|
+
let configPath = getConfigPath();
|
|
65
94
|
if (!existsSync(configPath)) {
|
|
66
|
-
|
|
95
|
+
configPath = getLegacyConfigPath();
|
|
96
|
+
if (!existsSync(configPath))
|
|
97
|
+
return emptyConfig();
|
|
98
|
+
noteLegacyConfigDir(configPath);
|
|
67
99
|
}
|
|
68
100
|
try {
|
|
69
101
|
const data = readFileSync(configPath, "utf-8");
|
|
@@ -113,7 +145,11 @@ function loadLocalConfig(cwd) {
|
|
|
113
145
|
}
|
|
114
146
|
}
|
|
115
147
|
function saveLocalConfig(config, cwd) {
|
|
116
|
-
const
|
|
148
|
+
const foundPath = findLocalConfigPath(cwd);
|
|
149
|
+
const localConfigPath = foundPath ? join(dirname(foundPath), LOCAL_CONFIG_FILENAME) : getLocalConfigPath(cwd);
|
|
150
|
+
if (foundPath !== null && foundPath !== localConfigPath) {
|
|
151
|
+
noteLocalPinRename(foundPath, localConfigPath);
|
|
152
|
+
}
|
|
117
153
|
const existingConfig = loadLocalConfig(cwd) || {
|
|
118
154
|
workspaceId: null,
|
|
119
155
|
projectId: null
|
|
@@ -125,6 +161,7 @@ function saveLocalConfig(config, cwd) {
|
|
|
125
161
|
if (newConfig.projectId)
|
|
126
162
|
cleanConfig.projectId = newConfig.projectId;
|
|
127
163
|
writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
|
|
164
|
+
return localConfigPath;
|
|
128
165
|
}
|
|
129
166
|
function hasLocalConfig(cwd) {
|
|
130
167
|
return findLocalConfigPath(cwd) !== null;
|
|
@@ -259,7 +296,7 @@ function getMemoryDir() {
|
|
|
259
296
|
return config.memoryDir;
|
|
260
297
|
return join(homedir(), ".harmony", "memory");
|
|
261
298
|
}
|
|
262
|
-
var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
299
|
+
var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false;
|
|
263
300
|
var init_config = () => {};
|
|
264
301
|
|
|
265
302
|
// src/prompt-builder.ts
|
|
@@ -9436,6 +9473,7 @@ function ensureDir(dirPath) {
|
|
|
9436
9473
|
mkdirSync5(dirPath, { recursive: true, mode: 493 });
|
|
9437
9474
|
}
|
|
9438
9475
|
}
|
|
9476
|
+
var CONFIG_DIR_MARKERS = [".hmy", ".harmony-mcp"];
|
|
9439
9477
|
function writeFile(filePath, content, options = {}) {
|
|
9440
9478
|
const exists = existsSync8(filePath);
|
|
9441
9479
|
if (exists && !options.force) {
|
|
@@ -9443,7 +9481,7 @@ function writeFile(filePath, content, options = {}) {
|
|
|
9443
9481
|
}
|
|
9444
9482
|
try {
|
|
9445
9483
|
ensureDir(dirname3(filePath));
|
|
9446
|
-
const defaultMode = filePath.includes(
|
|
9484
|
+
const defaultMode = CONFIG_DIR_MARKERS.some((marker) => filePath.includes(marker)) ? 384 : 420;
|
|
9447
9485
|
const mode = options.mode ?? defaultMode;
|
|
9448
9486
|
writeFileSync5(filePath, content, { mode });
|
|
9449
9487
|
if (options.mode !== undefined) {
|
|
@@ -10612,14 +10650,15 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
10612
10650
|
console.log(` ${colors.dim("Skipped tool allowlist — you'll be prompted per tool, or run /permissions in Claude Code later.")}`);
|
|
10613
10651
|
}
|
|
10614
10652
|
}
|
|
10653
|
+
let writtenLocalConfigPath = null;
|
|
10615
10654
|
if (selectedWorkspaceId || selectedProjectId) {
|
|
10616
10655
|
const localConfig = {};
|
|
10617
10656
|
if (selectedWorkspaceId)
|
|
10618
10657
|
localConfig.workspaceId = selectedWorkspaceId;
|
|
10619
10658
|
if (selectedProjectId)
|
|
10620
10659
|
localConfig.projectId = selectedProjectId;
|
|
10621
|
-
saveLocalConfig(localConfig, cwd);
|
|
10622
|
-
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(
|
|
10660
|
+
writtenLocalConfigPath = saveLocalConfig(localConfig, cwd);
|
|
10661
|
+
console.log(` ${colors.success("✓")} ${colors.dim(formatPath(writtenLocalConfigPath, home))} ${colors.dim("(created)")}`);
|
|
10623
10662
|
if (selectedWorkspaceId || selectedProjectId) {
|
|
10624
10663
|
setActiveContext({
|
|
10625
10664
|
workspaceId: selectedWorkspaceId ?? null,
|
|
@@ -10649,7 +10688,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
10649
10688
|
console.log(` Skills: ${installMode === "global" ? "~/.agents/skills/ (global)" : ".claude/skills/ (local)"}`);
|
|
10650
10689
|
}
|
|
10651
10690
|
if (selectedWorkspaceId || selectedProjectId) {
|
|
10652
|
-
console.log(` Context: ${formatPath(getLocalConfigPath(cwd), home)}`);
|
|
10691
|
+
console.log(` Context: ${formatPath(writtenLocalConfigPath ?? getLocalConfigPath(cwd), home)}`);
|
|
10653
10692
|
}
|
|
10654
10693
|
console.log("");
|
|
10655
10694
|
console.log(` ${colors.bold("Usage:")}`);
|
package/dist/index.js
CHANGED
|
@@ -21,12 +21,36 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
21
21
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { dirname, join as join2, parse, resolve } from "node:path";
|
|
24
|
+
function noteLegacyConfigDir(path) {
|
|
25
|
+
if (warnedLegacyConfigDir)
|
|
26
|
+
return;
|
|
27
|
+
warnedLegacyConfigDir = true;
|
|
28
|
+
console.error(`Harmony: reading the pre-#1082 config at ${path}. ` + `The current location is ${getConfigPath()}; ` + `run the agent daemon once to migrate, or move the file yourself.`);
|
|
29
|
+
}
|
|
30
|
+
function noteLegacyLocalPin(path) {
|
|
31
|
+
if (warnedLegacyLocalPin)
|
|
32
|
+
return;
|
|
33
|
+
warnedLegacyLocalPin = true;
|
|
34
|
+
console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} — the fallback that finds it is temporary.`);
|
|
35
|
+
}
|
|
36
|
+
function noteLocalPinRename(from, to) {
|
|
37
|
+
console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
|
|
38
|
+
}
|
|
39
|
+
function getHmyRootDir() {
|
|
40
|
+
return join2(homedir(), CONFIG_DIR_NAME);
|
|
41
|
+
}
|
|
24
42
|
function getConfigDir() {
|
|
25
|
-
return join2(
|
|
43
|
+
return join2(getHmyRootDir(), CONFIG_DIR_SUBDIR);
|
|
44
|
+
}
|
|
45
|
+
function getLegacyConfigDir() {
|
|
46
|
+
return join2(homedir(), LEGACY_CONFIG_DIR_NAME);
|
|
26
47
|
}
|
|
27
48
|
function getConfigPath() {
|
|
28
49
|
return join2(getConfigDir(), "config.json");
|
|
29
50
|
}
|
|
51
|
+
function getLegacyConfigPath() {
|
|
52
|
+
return join2(getLegacyConfigDir(), "config.json");
|
|
53
|
+
}
|
|
30
54
|
function getLocalConfigPath(cwd) {
|
|
31
55
|
return join2(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
|
|
32
56
|
}
|
|
@@ -36,9 +60,14 @@ function findLocalConfigPath(cwd) {
|
|
|
36
60
|
const { root } = parse(dir);
|
|
37
61
|
for (;; ) {
|
|
38
62
|
if (dir !== home && dir !== root) {
|
|
39
|
-
const
|
|
40
|
-
if (existsSync2(
|
|
41
|
-
return
|
|
63
|
+
const current = join2(dir, LOCAL_CONFIG_FILENAME);
|
|
64
|
+
if (existsSync2(current))
|
|
65
|
+
return current;
|
|
66
|
+
const legacy = join2(dir, LEGACY_LOCAL_CONFIG_FILENAME);
|
|
67
|
+
if (existsSync2(legacy)) {
|
|
68
|
+
noteLegacyLocalPin(legacy);
|
|
69
|
+
return legacy;
|
|
70
|
+
}
|
|
42
71
|
}
|
|
43
72
|
const parent = dirname(dir);
|
|
44
73
|
if (parent === dir)
|
|
@@ -61,9 +90,12 @@ function emptyConfig() {
|
|
|
61
90
|
};
|
|
62
91
|
}
|
|
63
92
|
function loadConfig() {
|
|
64
|
-
|
|
93
|
+
let configPath = getConfigPath();
|
|
65
94
|
if (!existsSync2(configPath)) {
|
|
66
|
-
|
|
95
|
+
configPath = getLegacyConfigPath();
|
|
96
|
+
if (!existsSync2(configPath))
|
|
97
|
+
return emptyConfig();
|
|
98
|
+
noteLegacyConfigDir(configPath);
|
|
67
99
|
}
|
|
68
100
|
try {
|
|
69
101
|
const data = readFileSync2(configPath, "utf-8");
|
|
@@ -113,7 +145,11 @@ function loadLocalConfig(cwd) {
|
|
|
113
145
|
}
|
|
114
146
|
}
|
|
115
147
|
function saveLocalConfig(config, cwd) {
|
|
116
|
-
const
|
|
148
|
+
const foundPath = findLocalConfigPath(cwd);
|
|
149
|
+
const localConfigPath = foundPath ? join2(dirname(foundPath), LOCAL_CONFIG_FILENAME) : getLocalConfigPath(cwd);
|
|
150
|
+
if (foundPath !== null && foundPath !== localConfigPath) {
|
|
151
|
+
noteLocalPinRename(foundPath, localConfigPath);
|
|
152
|
+
}
|
|
117
153
|
const existingConfig = loadLocalConfig(cwd) || {
|
|
118
154
|
workspaceId: null,
|
|
119
155
|
projectId: null
|
|
@@ -125,6 +161,7 @@ function saveLocalConfig(config, cwd) {
|
|
|
125
161
|
if (newConfig.projectId)
|
|
126
162
|
cleanConfig.projectId = newConfig.projectId;
|
|
127
163
|
writeFileSync2(localConfigPath, JSON.stringify(cleanConfig, null, 2));
|
|
164
|
+
return localConfigPath;
|
|
128
165
|
}
|
|
129
166
|
function hasLocalConfig(cwd) {
|
|
130
167
|
return findLocalConfigPath(cwd) !== null;
|
|
@@ -259,7 +296,7 @@ function getMemoryDir() {
|
|
|
259
296
|
return config.memoryDir;
|
|
260
297
|
return join2(homedir(), ".harmony", "memory");
|
|
261
298
|
}
|
|
262
|
-
var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
299
|
+
var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false;
|
|
263
300
|
var init_config = () => {};
|
|
264
301
|
|
|
265
302
|
// src/prompt-builder.ts
|
package/dist/lib/api-client.js
CHANGED
|
@@ -18,12 +18,40 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { dirname, join, parse, resolve } from "node:path";
|
|
21
|
+
function resetLegacyNoticesForTest() {
|
|
22
|
+
warnedLegacyConfigDir = false;
|
|
23
|
+
warnedLegacyLocalPin = false;
|
|
24
|
+
}
|
|
25
|
+
function noteLegacyConfigDir(path) {
|
|
26
|
+
if (warnedLegacyConfigDir)
|
|
27
|
+
return;
|
|
28
|
+
warnedLegacyConfigDir = true;
|
|
29
|
+
console.error(`Harmony: reading the pre-#1082 config at ${path}. ` + `The current location is ${getConfigPath()}; ` + `run the agent daemon once to migrate, or move the file yourself.`);
|
|
30
|
+
}
|
|
31
|
+
function noteLegacyLocalPin(path) {
|
|
32
|
+
if (warnedLegacyLocalPin)
|
|
33
|
+
return;
|
|
34
|
+
warnedLegacyLocalPin = true;
|
|
35
|
+
console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} — the fallback that finds it is temporary.`);
|
|
36
|
+
}
|
|
37
|
+
function noteLocalPinRename(from, to) {
|
|
38
|
+
console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
|
|
39
|
+
}
|
|
40
|
+
function getHmyRootDir() {
|
|
41
|
+
return join(homedir(), CONFIG_DIR_NAME);
|
|
42
|
+
}
|
|
21
43
|
function getConfigDir() {
|
|
22
|
-
return join(
|
|
44
|
+
return join(getHmyRootDir(), CONFIG_DIR_SUBDIR);
|
|
45
|
+
}
|
|
46
|
+
function getLegacyConfigDir() {
|
|
47
|
+
return join(homedir(), LEGACY_CONFIG_DIR_NAME);
|
|
23
48
|
}
|
|
24
49
|
function getConfigPath() {
|
|
25
50
|
return join(getConfigDir(), "config.json");
|
|
26
51
|
}
|
|
52
|
+
function getLegacyConfigPath() {
|
|
53
|
+
return join(getLegacyConfigDir(), "config.json");
|
|
54
|
+
}
|
|
27
55
|
function getLocalConfigPath(cwd) {
|
|
28
56
|
return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
|
|
29
57
|
}
|
|
@@ -33,9 +61,14 @@ function findLocalConfigPath(cwd) {
|
|
|
33
61
|
const { root } = parse(dir);
|
|
34
62
|
for (;; ) {
|
|
35
63
|
if (dir !== home && dir !== root) {
|
|
36
|
-
const
|
|
37
|
-
if (existsSync(
|
|
38
|
-
return
|
|
64
|
+
const current = join(dir, LOCAL_CONFIG_FILENAME);
|
|
65
|
+
if (existsSync(current))
|
|
66
|
+
return current;
|
|
67
|
+
const legacy = join(dir, LEGACY_LOCAL_CONFIG_FILENAME);
|
|
68
|
+
if (existsSync(legacy)) {
|
|
69
|
+
noteLegacyLocalPin(legacy);
|
|
70
|
+
return legacy;
|
|
71
|
+
}
|
|
39
72
|
}
|
|
40
73
|
const parent = dirname(dir);
|
|
41
74
|
if (parent === dir)
|
|
@@ -58,9 +91,12 @@ function emptyConfig() {
|
|
|
58
91
|
};
|
|
59
92
|
}
|
|
60
93
|
function loadConfig() {
|
|
61
|
-
|
|
94
|
+
let configPath = getConfigPath();
|
|
62
95
|
if (!existsSync(configPath)) {
|
|
63
|
-
|
|
96
|
+
configPath = getLegacyConfigPath();
|
|
97
|
+
if (!existsSync(configPath))
|
|
98
|
+
return emptyConfig();
|
|
99
|
+
noteLegacyConfigDir(configPath);
|
|
64
100
|
}
|
|
65
101
|
try {
|
|
66
102
|
const data = readFileSync(configPath, "utf-8");
|
|
@@ -110,7 +146,11 @@ function loadLocalConfig(cwd) {
|
|
|
110
146
|
}
|
|
111
147
|
}
|
|
112
148
|
function saveLocalConfig(config, cwd) {
|
|
113
|
-
const
|
|
149
|
+
const foundPath = findLocalConfigPath(cwd);
|
|
150
|
+
const localConfigPath = foundPath ? join(dirname(foundPath), LOCAL_CONFIG_FILENAME) : getLocalConfigPath(cwd);
|
|
151
|
+
if (foundPath !== null && foundPath !== localConfigPath) {
|
|
152
|
+
noteLocalPinRename(foundPath, localConfigPath);
|
|
153
|
+
}
|
|
114
154
|
const existingConfig = loadLocalConfig(cwd) || {
|
|
115
155
|
workspaceId: null,
|
|
116
156
|
projectId: null
|
|
@@ -122,6 +162,7 @@ function saveLocalConfig(config, cwd) {
|
|
|
122
162
|
if (newConfig.projectId)
|
|
123
163
|
cleanConfig.projectId = newConfig.projectId;
|
|
124
164
|
writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
|
|
165
|
+
return localConfigPath;
|
|
125
166
|
}
|
|
126
167
|
function hasLocalConfig(cwd) {
|
|
127
168
|
return findLocalConfigPath(cwd) !== null;
|
|
@@ -259,7 +300,7 @@ function getMemoryDir() {
|
|
|
259
300
|
return config.memoryDir;
|
|
260
301
|
return join(homedir(), ".harmony", "memory");
|
|
261
302
|
}
|
|
262
|
-
var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
303
|
+
var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false;
|
|
263
304
|
var init_config = () => {};
|
|
264
305
|
|
|
265
306
|
// src/oauth-login.ts
|
package/dist/lib/config.js
CHANGED
|
@@ -18,12 +18,40 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { dirname, join, parse, resolve } from "node:path";
|
|
21
|
+
function resetLegacyNoticesForTest() {
|
|
22
|
+
warnedLegacyConfigDir = false;
|
|
23
|
+
warnedLegacyLocalPin = false;
|
|
24
|
+
}
|
|
25
|
+
function noteLegacyConfigDir(path) {
|
|
26
|
+
if (warnedLegacyConfigDir)
|
|
27
|
+
return;
|
|
28
|
+
warnedLegacyConfigDir = true;
|
|
29
|
+
console.error(`Harmony: reading the pre-#1082 config at ${path}. ` + `The current location is ${getConfigPath()}; ` + `run the agent daemon once to migrate, or move the file yourself.`);
|
|
30
|
+
}
|
|
31
|
+
function noteLegacyLocalPin(path) {
|
|
32
|
+
if (warnedLegacyLocalPin)
|
|
33
|
+
return;
|
|
34
|
+
warnedLegacyLocalPin = true;
|
|
35
|
+
console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} — the fallback that finds it is temporary.`);
|
|
36
|
+
}
|
|
37
|
+
function noteLocalPinRename(from, to) {
|
|
38
|
+
console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
|
|
39
|
+
}
|
|
40
|
+
function getHmyRootDir() {
|
|
41
|
+
return join(homedir(), CONFIG_DIR_NAME);
|
|
42
|
+
}
|
|
21
43
|
function getConfigDir() {
|
|
22
|
-
return join(
|
|
44
|
+
return join(getHmyRootDir(), CONFIG_DIR_SUBDIR);
|
|
45
|
+
}
|
|
46
|
+
function getLegacyConfigDir() {
|
|
47
|
+
return join(homedir(), LEGACY_CONFIG_DIR_NAME);
|
|
23
48
|
}
|
|
24
49
|
function getConfigPath() {
|
|
25
50
|
return join(getConfigDir(), "config.json");
|
|
26
51
|
}
|
|
52
|
+
function getLegacyConfigPath() {
|
|
53
|
+
return join(getLegacyConfigDir(), "config.json");
|
|
54
|
+
}
|
|
27
55
|
function getLocalConfigPath(cwd) {
|
|
28
56
|
return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
|
|
29
57
|
}
|
|
@@ -33,9 +61,14 @@ function findLocalConfigPath(cwd) {
|
|
|
33
61
|
const { root } = parse(dir);
|
|
34
62
|
for (;; ) {
|
|
35
63
|
if (dir !== home && dir !== root) {
|
|
36
|
-
const
|
|
37
|
-
if (existsSync(
|
|
38
|
-
return
|
|
64
|
+
const current = join(dir, LOCAL_CONFIG_FILENAME);
|
|
65
|
+
if (existsSync(current))
|
|
66
|
+
return current;
|
|
67
|
+
const legacy = join(dir, LEGACY_LOCAL_CONFIG_FILENAME);
|
|
68
|
+
if (existsSync(legacy)) {
|
|
69
|
+
noteLegacyLocalPin(legacy);
|
|
70
|
+
return legacy;
|
|
71
|
+
}
|
|
39
72
|
}
|
|
40
73
|
const parent = dirname(dir);
|
|
41
74
|
if (parent === dir)
|
|
@@ -58,9 +91,12 @@ function emptyConfig() {
|
|
|
58
91
|
};
|
|
59
92
|
}
|
|
60
93
|
function loadConfig() {
|
|
61
|
-
|
|
94
|
+
let configPath = getConfigPath();
|
|
62
95
|
if (!existsSync(configPath)) {
|
|
63
|
-
|
|
96
|
+
configPath = getLegacyConfigPath();
|
|
97
|
+
if (!existsSync(configPath))
|
|
98
|
+
return emptyConfig();
|
|
99
|
+
noteLegacyConfigDir(configPath);
|
|
64
100
|
}
|
|
65
101
|
try {
|
|
66
102
|
const data = readFileSync(configPath, "utf-8");
|
|
@@ -110,7 +146,11 @@ function loadLocalConfig(cwd) {
|
|
|
110
146
|
}
|
|
111
147
|
}
|
|
112
148
|
function saveLocalConfig(config, cwd) {
|
|
113
|
-
const
|
|
149
|
+
const foundPath = findLocalConfigPath(cwd);
|
|
150
|
+
const localConfigPath = foundPath ? join(dirname(foundPath), LOCAL_CONFIG_FILENAME) : getLocalConfigPath(cwd);
|
|
151
|
+
if (foundPath !== null && foundPath !== localConfigPath) {
|
|
152
|
+
noteLocalPinRename(foundPath, localConfigPath);
|
|
153
|
+
}
|
|
114
154
|
const existingConfig = loadLocalConfig(cwd) || {
|
|
115
155
|
workspaceId: null,
|
|
116
156
|
projectId: null
|
|
@@ -122,6 +162,7 @@ function saveLocalConfig(config, cwd) {
|
|
|
122
162
|
if (newConfig.projectId)
|
|
123
163
|
cleanConfig.projectId = newConfig.projectId;
|
|
124
164
|
writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
|
|
165
|
+
return localConfigPath;
|
|
125
166
|
}
|
|
126
167
|
function hasLocalConfig(cwd) {
|
|
127
168
|
return findLocalConfigPath(cwd) !== null;
|
|
@@ -259,7 +300,7 @@ function getMemoryDir() {
|
|
|
259
300
|
return config.memoryDir;
|
|
260
301
|
return join(homedir(), ".harmony", "memory");
|
|
261
302
|
}
|
|
262
|
-
var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
303
|
+
var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false;
|
|
263
304
|
var init_config = () => {};
|
|
264
305
|
init_config();
|
|
265
306
|
|
|
@@ -269,6 +310,8 @@ export {
|
|
|
269
310
|
setActiveContext,
|
|
270
311
|
saveLocalConfig,
|
|
271
312
|
saveConfig,
|
|
313
|
+
resetLegacyNoticesForTest,
|
|
314
|
+
noteLegacyLocalPin,
|
|
272
315
|
loadLocalConfig,
|
|
273
316
|
loadConfig,
|
|
274
317
|
isConfigured,
|
|
@@ -277,6 +320,9 @@ export {
|
|
|
277
320
|
getUserEmail,
|
|
278
321
|
getMemoryDir,
|
|
279
322
|
getLocalConfigPath,
|
|
323
|
+
getLegacyConfigPath,
|
|
324
|
+
getLegacyConfigDir,
|
|
325
|
+
getHmyRootDir,
|
|
280
326
|
getConfigPath,
|
|
281
327
|
getConfigDir,
|
|
282
328
|
getApiUrl,
|
|
@@ -287,5 +333,6 @@ export {
|
|
|
287
333
|
getActiveContext,
|
|
288
334
|
findLocalConfigPath,
|
|
289
335
|
describeActiveContext,
|
|
290
|
-
areSkillsInstalled
|
|
336
|
+
areSkillsInstalled,
|
|
337
|
+
LEGACY_LOCAL_CONFIG_FILENAME
|
|
291
338
|
};
|
|
@@ -18,12 +18,40 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { dirname, join, parse, resolve } from "node:path";
|
|
21
|
+
function resetLegacyNoticesForTest() {
|
|
22
|
+
warnedLegacyConfigDir = false;
|
|
23
|
+
warnedLegacyLocalPin = false;
|
|
24
|
+
}
|
|
25
|
+
function noteLegacyConfigDir(path) {
|
|
26
|
+
if (warnedLegacyConfigDir)
|
|
27
|
+
return;
|
|
28
|
+
warnedLegacyConfigDir = true;
|
|
29
|
+
console.error(`Harmony: reading the pre-#1082 config at ${path}. ` + `The current location is ${getConfigPath()}; ` + `run the agent daemon once to migrate, or move the file yourself.`);
|
|
30
|
+
}
|
|
31
|
+
function noteLegacyLocalPin(path) {
|
|
32
|
+
if (warnedLegacyLocalPin)
|
|
33
|
+
return;
|
|
34
|
+
warnedLegacyLocalPin = true;
|
|
35
|
+
console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} — the fallback that finds it is temporary.`);
|
|
36
|
+
}
|
|
37
|
+
function noteLocalPinRename(from, to) {
|
|
38
|
+
console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
|
|
39
|
+
}
|
|
40
|
+
function getHmyRootDir() {
|
|
41
|
+
return join(homedir(), CONFIG_DIR_NAME);
|
|
42
|
+
}
|
|
21
43
|
function getConfigDir() {
|
|
22
|
-
return join(
|
|
44
|
+
return join(getHmyRootDir(), CONFIG_DIR_SUBDIR);
|
|
45
|
+
}
|
|
46
|
+
function getLegacyConfigDir() {
|
|
47
|
+
return join(homedir(), LEGACY_CONFIG_DIR_NAME);
|
|
23
48
|
}
|
|
24
49
|
function getConfigPath() {
|
|
25
50
|
return join(getConfigDir(), "config.json");
|
|
26
51
|
}
|
|
52
|
+
function getLegacyConfigPath() {
|
|
53
|
+
return join(getLegacyConfigDir(), "config.json");
|
|
54
|
+
}
|
|
27
55
|
function getLocalConfigPath(cwd) {
|
|
28
56
|
return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
|
|
29
57
|
}
|
|
@@ -33,9 +61,14 @@ function findLocalConfigPath(cwd) {
|
|
|
33
61
|
const { root } = parse(dir);
|
|
34
62
|
for (;; ) {
|
|
35
63
|
if (dir !== home && dir !== root) {
|
|
36
|
-
const
|
|
37
|
-
if (existsSync(
|
|
38
|
-
return
|
|
64
|
+
const current = join(dir, LOCAL_CONFIG_FILENAME);
|
|
65
|
+
if (existsSync(current))
|
|
66
|
+
return current;
|
|
67
|
+
const legacy = join(dir, LEGACY_LOCAL_CONFIG_FILENAME);
|
|
68
|
+
if (existsSync(legacy)) {
|
|
69
|
+
noteLegacyLocalPin(legacy);
|
|
70
|
+
return legacy;
|
|
71
|
+
}
|
|
39
72
|
}
|
|
40
73
|
const parent = dirname(dir);
|
|
41
74
|
if (parent === dir)
|
|
@@ -58,9 +91,12 @@ function emptyConfig() {
|
|
|
58
91
|
};
|
|
59
92
|
}
|
|
60
93
|
function loadConfig() {
|
|
61
|
-
|
|
94
|
+
let configPath = getConfigPath();
|
|
62
95
|
if (!existsSync(configPath)) {
|
|
63
|
-
|
|
96
|
+
configPath = getLegacyConfigPath();
|
|
97
|
+
if (!existsSync(configPath))
|
|
98
|
+
return emptyConfig();
|
|
99
|
+
noteLegacyConfigDir(configPath);
|
|
64
100
|
}
|
|
65
101
|
try {
|
|
66
102
|
const data = readFileSync(configPath, "utf-8");
|
|
@@ -110,7 +146,11 @@ function loadLocalConfig(cwd) {
|
|
|
110
146
|
}
|
|
111
147
|
}
|
|
112
148
|
function saveLocalConfig(config, cwd) {
|
|
113
|
-
const
|
|
149
|
+
const foundPath = findLocalConfigPath(cwd);
|
|
150
|
+
const localConfigPath = foundPath ? join(dirname(foundPath), LOCAL_CONFIG_FILENAME) : getLocalConfigPath(cwd);
|
|
151
|
+
if (foundPath !== null && foundPath !== localConfigPath) {
|
|
152
|
+
noteLocalPinRename(foundPath, localConfigPath);
|
|
153
|
+
}
|
|
114
154
|
const existingConfig = loadLocalConfig(cwd) || {
|
|
115
155
|
workspaceId: null,
|
|
116
156
|
projectId: null
|
|
@@ -122,6 +162,7 @@ function saveLocalConfig(config, cwd) {
|
|
|
122
162
|
if (newConfig.projectId)
|
|
123
163
|
cleanConfig.projectId = newConfig.projectId;
|
|
124
164
|
writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
|
|
165
|
+
return localConfigPath;
|
|
125
166
|
}
|
|
126
167
|
function hasLocalConfig(cwd) {
|
|
127
168
|
return findLocalConfigPath(cwd) !== null;
|
|
@@ -259,7 +300,7 @@ function getMemoryDir() {
|
|
|
259
300
|
return config.memoryDir;
|
|
260
301
|
return join(homedir(), ".harmony", "memory");
|
|
261
302
|
}
|
|
262
|
-
var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
303
|
+
var DEFAULT_API_URL = "https://app.gethmy.com/api", LOCAL_CONFIG_FILENAME = ".hmy.json", LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json", CONFIG_DIR_NAME = ".hmy", CONFIG_DIR_SUBDIR = "agent", LEGACY_CONFIG_DIR_NAME = ".harmony-mcp", warnedLegacyConfigDir = false, warnedLegacyLocalPin = false;
|
|
263
304
|
var init_config = () => {};
|
|
264
305
|
|
|
265
306
|
// src/oauth-login.ts
|
package/dist/run-hook-cli.js
CHANGED
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface HarmonyConfig {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Local project-level config (stored in .
|
|
25
|
+
* Local project-level config (stored in .hmy.json in project root).
|
|
26
26
|
* Only contains context IDs - API key stays global for security.
|
|
27
27
|
*/
|
|
28
28
|
export interface LocalConfig {
|
|
@@ -31,22 +31,131 @@ export interface LocalConfig {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const DEFAULT_API_URL = "https://app.gethmy.com/api";
|
|
34
|
-
const LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
35
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The config surface was renamed to `hmy` in card #1082 — `.harmony-mcp.json`
|
|
37
|
+
* became `.hmy.json`, and the home directory `~/.harmony-mcp/` became
|
|
38
|
+
* `~/.hmy/agent/`.
|
|
39
|
+
*
|
|
40
|
+
* `~/.hmy` already existed and belongs to the skill auto-update layer
|
|
41
|
+
* (`config.yaml`, `VERSION`, `bin/`, see `hmy-config.ts`), so the daemon's own
|
|
42
|
+
* state took the `agent/` subdirectory rather than landing a second file named
|
|
43
|
+
* `config.*` — in a different format, with a different owner — beside it.
|
|
44
|
+
*
|
|
45
|
+
* **Writes always go to the new name; reads fall back to the old one.**
|
|
46
|
+
* `@gethmy/mcp` and `@gethmy/agent` are published and the daemon runs from a
|
|
47
|
+
* frozen npx cache, so a hard cut would leave a running daemon unable to find
|
|
48
|
+
* its config until a coordinated release. `migrateConfigDir` (harmony-agent)
|
|
49
|
+
* moves the data once, at daemon startup, which makes the fallback moot in
|
|
50
|
+
* practice — it stays only for a client that never runs the daemon.
|
|
51
|
+
*
|
|
52
|
+
* The legacy spellings are exported because two things still need them after
|
|
53
|
+
* the fallback is removed: the migration, and the harness read-denylists. The
|
|
54
|
+
* old directory keeps holding an API key and 3430 run logs until an operator
|
|
55
|
+
* deletes it, so denying only the new name would OPEN it. See
|
|
56
|
+
* `credentialDirectories()` in harmony-harness and `HARNESS_CREDENTIAL_LEAVES`
|
|
57
|
+
* in `run-redaction.ts`; both name old and new.
|
|
58
|
+
*/
|
|
59
|
+
const LOCAL_CONFIG_FILENAME = ".hmy.json";
|
|
60
|
+
export const LEGACY_LOCAL_CONFIG_FILENAME = ".harmony-mcp.json";
|
|
61
|
+
const CONFIG_DIR_NAME = ".hmy";
|
|
62
|
+
const CONFIG_DIR_SUBDIR = "agent";
|
|
63
|
+
const LEGACY_CONFIG_DIR_NAME = ".harmony-mcp";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fallbacks announce themselves ONCE per process (#1082).
|
|
67
|
+
*
|
|
68
|
+
* A silent fallback is the failure mode this whole compatibility window is
|
|
69
|
+
* exposed to: an operator whose migration failed, or whose daemon runs from a
|
|
70
|
+
* frozen npx cache, gets no signal that they still depend on a path scheduled
|
|
71
|
+
* for removal — and then the follow-up card that deletes the fallback breaks
|
|
72
|
+
* them with nothing in any log to connect the two.
|
|
73
|
+
*
|
|
74
|
+
* Latched per process rather than per call because these sit on hot paths —
|
|
75
|
+
* `loadConfig` runs on every tool dispatch — and a line repeated hundreds of
|
|
76
|
+
* times is read as noise and filtered out, which is the same as silence.
|
|
77
|
+
*
|
|
78
|
+
* `console.error`, not `console.log`: on stdio transport stdout carries the
|
|
79
|
+
* JSON-RPC protocol, so anything printed there corrupts the session. Same
|
|
80
|
+
* convention as `skills.ts`.
|
|
81
|
+
*/
|
|
82
|
+
let warnedLegacyConfigDir = false;
|
|
83
|
+
let warnedLegacyLocalPin = false;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Clear both latches. Tests only.
|
|
87
|
+
*
|
|
88
|
+
* A per-process latch is not observable twice, so without this the "said once"
|
|
89
|
+
* half of the contract is untestable — and an untested notice is how the
|
|
90
|
+
* fallback goes silent again without anyone noticing. Exported deliberately
|
|
91
|
+
* rather than left to a test reaching into module state.
|
|
92
|
+
*/
|
|
93
|
+
export function resetLegacyNoticesForTest(): void {
|
|
94
|
+
warnedLegacyConfigDir = false;
|
|
95
|
+
warnedLegacyLocalPin = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function noteLegacyConfigDir(path: string): void {
|
|
99
|
+
if (warnedLegacyConfigDir) return;
|
|
100
|
+
warnedLegacyConfigDir = true;
|
|
101
|
+
console.error(
|
|
102
|
+
`Harmony: reading the pre-#1082 config at ${path}. ` +
|
|
103
|
+
`The current location is ${getConfigPath()}; ` +
|
|
104
|
+
`run the agent daemon once to migrate, or move the file yourself.`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Said once when a repo pin is read under the old name. */
|
|
109
|
+
export function noteLegacyLocalPin(path: string): void {
|
|
110
|
+
if (warnedLegacyLocalPin) return;
|
|
111
|
+
warnedLegacyLocalPin = true;
|
|
112
|
+
console.error(
|
|
113
|
+
`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` +
|
|
114
|
+
`Rename it to ${LOCAL_CONFIG_FILENAME} — the fallback that finds it is temporary.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Said once when a write moves the pin to the current name. */
|
|
119
|
+
function noteLocalPinRename(from: string, to: string): void {
|
|
120
|
+
console.error(
|
|
121
|
+
`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `~/.hmy` — the root the whole `hmy` surface shares. Named separately from
|
|
127
|
+
* `getConfigDir()` because the denylists want the WHOLE tree, not just the
|
|
128
|
+
* daemon's corner of it: `config.yaml` is not a credential, but a future
|
|
129
|
+
* sibling might be, and a directory has no spellings to enumerate.
|
|
130
|
+
*/
|
|
131
|
+
export function getHmyRootDir(): string {
|
|
132
|
+
return join(homedir(), CONFIG_DIR_NAME);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** `~/.hmy/agent` — where every writer writes. Never falls back. */
|
|
36
136
|
export function getConfigDir(): string {
|
|
37
|
-
return join(
|
|
137
|
+
return join(getHmyRootDir(), CONFIG_DIR_SUBDIR);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** `~/.harmony-mcp` — the pre-#1082 location. Read-only, and denied forever. */
|
|
141
|
+
export function getLegacyConfigDir(): string {
|
|
142
|
+
return join(homedir(), LEGACY_CONFIG_DIR_NAME);
|
|
38
143
|
}
|
|
39
144
|
|
|
40
145
|
export function getConfigPath(): string {
|
|
41
146
|
return join(getConfigDir(), "config.json");
|
|
42
147
|
}
|
|
43
148
|
|
|
149
|
+
export function getLegacyConfigPath(): string {
|
|
150
|
+
return join(getLegacyConfigDir(), "config.json");
|
|
151
|
+
}
|
|
152
|
+
|
|
44
153
|
export function getLocalConfigPath(cwd?: string): string {
|
|
45
154
|
return join(cwd || process.cwd(), LOCAL_CONFIG_FILENAME);
|
|
46
155
|
}
|
|
47
156
|
|
|
48
157
|
/**
|
|
49
|
-
* Find the nearest `.
|
|
158
|
+
* Find the nearest `.hmy.json` at or above `cwd` (card #893).
|
|
50
159
|
*
|
|
51
160
|
* The old behaviour looked in the exact cwd and nowhere else, so a server
|
|
52
161
|
* started from a package directory or a git worktree never saw the repo's pin
|
|
@@ -56,9 +165,15 @@ export function getLocalConfigPath(cwd?: string): string {
|
|
|
56
165
|
* Two directories are deliberately skipped, however deep the walk goes:
|
|
57
166
|
*
|
|
58
167
|
* - **the home directory** — a stray file there would capture every session in
|
|
59
|
-
* every repo, and `~/.
|
|
168
|
+
* every repo, and `~/.hmy/agent/config.json` is already the global pin;
|
|
60
169
|
* - **the filesystem root** — same reasoning, machine-wide.
|
|
61
170
|
*
|
|
171
|
+
* **Both names are checked in the SAME directory before walking up** (#1082),
|
|
172
|
+
* which is the only ordering that preserves #893. Checking every ancestor for
|
|
173
|
+
* `.hmy.json` first and only then re-walking for `.harmony-mcp.json` would let
|
|
174
|
+
* a parent repo's new-name pin beat the current repo's own legacy pin — the
|
|
175
|
+
* nearest file must win whichever name it carries.
|
|
176
|
+
*
|
|
62
177
|
* Returns `null` when no ancestor carries one.
|
|
63
178
|
*/
|
|
64
179
|
export function findLocalConfigPath(cwd?: string): string | null {
|
|
@@ -68,8 +183,13 @@ export function findLocalConfigPath(cwd?: string): string | null {
|
|
|
68
183
|
|
|
69
184
|
for (;;) {
|
|
70
185
|
if (dir !== home && dir !== root) {
|
|
71
|
-
const
|
|
72
|
-
if (existsSync(
|
|
186
|
+
const current = join(dir, LOCAL_CONFIG_FILENAME);
|
|
187
|
+
if (existsSync(current)) return current;
|
|
188
|
+
const legacy = join(dir, LEGACY_LOCAL_CONFIG_FILENAME);
|
|
189
|
+
if (existsSync(legacy)) {
|
|
190
|
+
noteLegacyLocalPin(legacy);
|
|
191
|
+
return legacy;
|
|
192
|
+
}
|
|
73
193
|
}
|
|
74
194
|
const parent = dirname(dir);
|
|
75
195
|
if (parent === dir) return null;
|
|
@@ -93,10 +213,14 @@ function emptyConfig(): HarmonyConfig {
|
|
|
93
213
|
}
|
|
94
214
|
|
|
95
215
|
export function loadConfig(): HarmonyConfig {
|
|
96
|
-
|
|
97
|
-
|
|
216
|
+
// Read the new location, fall back to the pre-#1082 one. The fallback is
|
|
217
|
+
// read-only on purpose: `saveConfig` always writes the new path, so the first
|
|
218
|
+
// write after an upgrade lands there and the old file stops being consulted.
|
|
219
|
+
let configPath = getConfigPath();
|
|
98
220
|
if (!existsSync(configPath)) {
|
|
99
|
-
|
|
221
|
+
configPath = getLegacyConfigPath();
|
|
222
|
+
if (!existsSync(configPath)) return emptyConfig();
|
|
223
|
+
noteLegacyConfigDir(configPath);
|
|
100
224
|
}
|
|
101
225
|
|
|
102
226
|
try {
|
|
@@ -157,14 +281,33 @@ export function loadLocalConfig(cwd?: string): LocalConfig | null {
|
|
|
157
281
|
}
|
|
158
282
|
}
|
|
159
283
|
|
|
284
|
+
/** Returns the path actually written, which is NOT always `<cwd>/.hmy.json`. */
|
|
160
285
|
export function saveLocalConfig(
|
|
161
286
|
config: Partial<LocalConfig>,
|
|
162
287
|
cwd?: string,
|
|
163
|
-
):
|
|
164
|
-
// Write back to the
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
288
|
+
): string {
|
|
289
|
+
// Write back to the DIRECTORY we read from, under the CURRENT name (#893,
|
|
290
|
+
// #1082). #893's rule is about the directory: a `set_project_context` issued
|
|
291
|
+
// from a package subdirectory must update the repo's pin rather than strand a
|
|
292
|
+
// second config file the repo root never looks at. The filename was never the
|
|
293
|
+
// point, and writing back to `.harmony-mcp.json` because that is what the read
|
|
294
|
+
// found would make this the one writer still creating the old name — the
|
|
295
|
+
// thing #1082 exists to stop, and a live trap for the card that removes the
|
|
296
|
+
// read fallback, since a pin left under the old name goes invisible and the
|
|
297
|
+
// session falls through to the global context. That silent wrong-workspace
|
|
298
|
+
// failure is exactly what #893 was filed to remove.
|
|
299
|
+
//
|
|
300
|
+
// The legacy file is left on disk rather than deleted, matching
|
|
301
|
+
// `migrateConfigDir`'s doctrine: this code removes nothing an operator wrote.
|
|
302
|
+
// It is inert from the next read on — `findLocalConfigPath` prefers
|
|
303
|
+
// `.hmy.json` in the same directory — and `noteLocalPinRename` says so once.
|
|
304
|
+
const foundPath = findLocalConfigPath(cwd);
|
|
305
|
+
const localConfigPath = foundPath
|
|
306
|
+
? join(dirname(foundPath), LOCAL_CONFIG_FILENAME)
|
|
307
|
+
: getLocalConfigPath(cwd);
|
|
308
|
+
if (foundPath !== null && foundPath !== localConfigPath) {
|
|
309
|
+
noteLocalPinRename(foundPath, localConfigPath);
|
|
310
|
+
}
|
|
168
311
|
|
|
169
312
|
const existingConfig = loadLocalConfig(cwd) || {
|
|
170
313
|
workspaceId: null,
|
|
@@ -178,6 +321,7 @@ export function saveLocalConfig(
|
|
|
178
321
|
if (newConfig.projectId) cleanConfig.projectId = newConfig.projectId;
|
|
179
322
|
|
|
180
323
|
writeFileSync(localConfigPath, JSON.stringify(cleanConfig, null, 2));
|
|
324
|
+
return localConfigPath;
|
|
181
325
|
}
|
|
182
326
|
|
|
183
327
|
export function hasLocalConfig(cwd?: string): boolean {
|
|
@@ -222,10 +366,10 @@ export function setUserEmail(email: string | null): void {
|
|
|
222
366
|
}
|
|
223
367
|
|
|
224
368
|
export interface SetContextOptions {
|
|
225
|
-
/** Force a write to the repo-local `.
|
|
369
|
+
/** Force a write to the repo-local `.hmy.json` (creating it). */
|
|
226
370
|
local?: boolean;
|
|
227
371
|
/**
|
|
228
|
-
* Force a write to the global `~/.
|
|
372
|
+
* Force a write to the global `~/.hmy/agent/config.json`, even when a local
|
|
229
373
|
* file exists. Setup uses this to mirror the chosen context into the global
|
|
230
374
|
* default; without it the mirror would land back in the local file it just
|
|
231
375
|
* wrote, leaving every server started from another directory with no context.
|
|
@@ -318,7 +462,7 @@ export function setActiveWorkspace(
|
|
|
318
462
|
/**
|
|
319
463
|
* The active pair, read from ONE source (card #893).
|
|
320
464
|
*
|
|
321
|
-
* A local `.
|
|
465
|
+
* A local `.hmy.json` wins **as a whole file**, not field by field. The
|
|
322
466
|
* two ids used to fall back to the global config independently, which quietly
|
|
323
467
|
* re-created the very mismatch this card removes: `saveLocalConfig` omits null
|
|
324
468
|
* values, so writing `projectId: null` locally did not clear the project — it
|
package/src/hook-install.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `hooks` is not merely missing from that list — it is the key that BROKE the
|
|
13
13
|
* previous denylist and forced the inversion. A hook block in a run's own
|
|
14
14
|
* worktree executed as the daemon user, outside the sandbox, and read
|
|
15
|
-
* `~/.ssh/config` and `~/.
|
|
15
|
+
* `~/.ssh/config` and `~/.hmy/agent/config.json`. So writing this hook into a
|
|
16
16
|
* project settings file would throw `ProjectSandboxOverrideError` on every
|
|
17
17
|
* contained implement run in that repo — it would not degrade, it would stop
|
|
18
18
|
* the daemon.
|
package/src/oauth-refresh.ts
CHANGED
|
@@ -33,7 +33,7 @@ interface RefreshResponse {
|
|
|
33
33
|
let inFlight: Promise<string | null> | null = null;
|
|
34
34
|
|
|
35
35
|
// Cross-process serialization. Every Claude Code session spawns its own stdio
|
|
36
|
-
// MCP process, and they all share ~/.
|
|
36
|
+
// MCP process, and they all share ~/.hmy/agent/config.json. The refresh
|
|
37
37
|
// token rotates on use, so two processes refreshing concurrently would each
|
|
38
38
|
// POST a refresh: the first consumes the token, the second replays a now-
|
|
39
39
|
// consumed token and trips the server's reuse detection, revoking the whole
|
package/src/run-redaction.ts
CHANGED
|
@@ -65,8 +65,15 @@ export type WithholdReason = "sensitive-path";
|
|
|
65
65
|
* whole segment is sensitive regardless of the file name inside it.
|
|
66
66
|
*
|
|
67
67
|
* Kept in step with `credentialDirectories()` in the harness — see the module
|
|
68
|
-
* doc. `.
|
|
69
|
-
*
|
|
68
|
+
* doc. `.hmy` is here for the same reason it is first there: it holds this
|
|
69
|
+
* product's own API key. `.harmony-mcp` is its pre-#1082 name and stays listed
|
|
70
|
+
* FOREVER — the rename moved the daemon's writes, not the operator's old
|
|
71
|
+
* directory, which keeps that key on disk until they delete it by hand.
|
|
72
|
+
*
|
|
73
|
+
* One segment covers `~/.hmy/agent` and every future sibling: the match below
|
|
74
|
+
* walks segments, so `.hmy` needs no `CONFIG_SCOPED_SEGMENTS` special case.
|
|
75
|
+
* Both names are dot-prefixed and product-specific, so neither can collide with
|
|
76
|
+
* a source directory the way the bare `gh` / `op` entries could.
|
|
70
77
|
*/
|
|
71
78
|
const SENSITIVE_SEGMENTS: readonly string[] = [
|
|
72
79
|
".ssh",
|
|
@@ -76,6 +83,7 @@ const SENSITIVE_SEGMENTS: readonly string[] = [
|
|
|
76
83
|
".gemini",
|
|
77
84
|
".docker",
|
|
78
85
|
".kube",
|
|
86
|
+
".hmy",
|
|
79
87
|
".harmony-mcp",
|
|
80
88
|
".password-store",
|
|
81
89
|
// `~/.claude` holds `.credentials.json`, Claude Code's own OAuth token, and
|
|
@@ -125,7 +133,21 @@ export const HARNESS_CREDENTIAL_LEAVES: readonly {
|
|
|
125
133
|
*/
|
|
126
134
|
readonly kind: "dir" | "file";
|
|
127
135
|
}[] = [
|
|
128
|
-
|
|
136
|
+
// Three entries for one credential directory (#1082). `~/.harmony-mcp` was
|
|
137
|
+
// renamed to `~/.hmy/agent`, and this list does NOT follow `getConfigDir()` —
|
|
138
|
+
// it is hand-transcribed, so a rename here is a decision rather than a
|
|
139
|
+
// consequence. The decision is to ADD, never to swap: an operator's
|
|
140
|
+
// `~/.harmony-mcp` keeps its `config.json` (the Harmony API key) and its run
|
|
141
|
+
// logs until they delete it by hand, so dropping the old name would put the
|
|
142
|
+
// key back on the board.
|
|
143
|
+
//
|
|
144
|
+
// `.hmy` covers the whole tree, which is what `isSensitivePath` wants: it
|
|
145
|
+
// matches on path SEGMENTS, so a single segment needs no `CONFIG_SCOPED_
|
|
146
|
+
// SEGMENTS` special case the way `.config/gh` does. `.hmy/agent` is listed
|
|
147
|
+
// beside it so the mirror test walks the exact path the harness names.
|
|
148
|
+
{ path: ".hmy", kind: "dir" }, // getHmyRootDir()
|
|
149
|
+
{ path: ".hmy/agent", kind: "dir" }, // getConfigDir()
|
|
150
|
+
{ path: ".harmony-mcp", kind: "dir" }, // getLegacyConfigDir()
|
|
129
151
|
{ path: ".claude", kind: "dir" },
|
|
130
152
|
{ path: ".claude.json", kind: "file" },
|
|
131
153
|
{ path: ".ssh", kind: "dir" },
|
package/src/run-state.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* server's memory, so the session identity has to travel **through the
|
|
15
15
|
* filesystem**. That is what this module is.
|
|
16
16
|
*
|
|
17
|
-
* ## Why not `~/.
|
|
17
|
+
* ## Why not `~/.hmy/agent/`
|
|
18
18
|
*
|
|
19
19
|
* `getConfigDir()` is the FIRST entry of `credentialDirectories()`
|
|
20
20
|
* (`packages/harmony-harness/src/run-containment.ts`) and is read-denied to
|
package/src/server.ts
CHANGED
|
@@ -3293,7 +3293,7 @@ export async function handleToolCall(
|
|
|
3293
3293
|
// deliberately-set active project is a hard scope: `found` there means
|
|
3294
3294
|
// the card was already in it, so there is nothing to change — and we
|
|
3295
3295
|
// never let a read silently repoint a context the user chose (which,
|
|
3296
|
-
// on the local stdio MCP, persists to ~/.
|
|
3296
|
+
// on the local stdio MCP, persists to ~/.hmy/agent/config.json
|
|
3297
3297
|
// across sessions). Confirm the target back either way so a
|
|
3298
3298
|
// wrong-context resolve is visible immediately.
|
|
3299
3299
|
const established = activeProjectId == null;
|
package/src/tui/setup.ts
CHANGED
|
@@ -1552,6 +1552,9 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1552
1552
|
}
|
|
1553
1553
|
}
|
|
1554
1554
|
|
|
1555
|
+
/** Set once step 9 writes the repo pin, so the summary names the real file. */
|
|
1556
|
+
let writtenLocalConfigPath: string | null = null;
|
|
1557
|
+
|
|
1555
1558
|
// Step 9: Save context \u2014 both local (cwd-scoped) and global (user default).
|
|
1556
1559
|
// Local config only resolves when the server runs with this repo as cwd;
|
|
1557
1560
|
// remote/OAuth connections and other cwds fall back to the global active
|
|
@@ -1562,9 +1565,15 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1562
1565
|
const localConfig: { workspaceId?: string; projectId?: string } = {};
|
|
1563
1566
|
if (selectedWorkspaceId) localConfig.workspaceId = selectedWorkspaceId;
|
|
1564
1567
|
if (selectedProjectId) localConfig.projectId = selectedProjectId;
|
|
1565
|
-
saveLocalConfig
|
|
1568
|
+
// Print the path `saveLocalConfig` ACTUALLY wrote, not a re-derived guess
|
|
1569
|
+
// (#1082). `getLocalConfigPath` is `<cwd>/.hmy.json` with no upward walk,
|
|
1570
|
+
// while the write targets the directory of the pin already in scope — which
|
|
1571
|
+
// may be an ancestor (#893). Run setup from `packages/foo` in a repo pinned
|
|
1572
|
+
// at its root and the two disagree, so this line reported a file that does
|
|
1573
|
+
// not exist. Returning the path is what removes the second derivation.
|
|
1574
|
+
writtenLocalConfigPath = saveLocalConfig(localConfig, cwd);
|
|
1566
1575
|
console.log(
|
|
1567
|
-
` ${colors.success("\u2713")} ${colors.dim(formatPath(
|
|
1576
|
+
` ${colors.success("\u2713")} ${colors.dim(formatPath(writtenLocalConfigPath, home))} ${colors.dim("(created)")}`,
|
|
1568
1577
|
);
|
|
1569
1578
|
|
|
1570
1579
|
// Mirror the choice into the GLOBAL default, explicitly (#893). The local
|
|
@@ -1629,7 +1638,7 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
1629
1638
|
}
|
|
1630
1639
|
if (selectedWorkspaceId || selectedProjectId) {
|
|
1631
1640
|
console.log(
|
|
1632
|
-
` Context: ${formatPath(getLocalConfigPath(cwd), home)}`,
|
|
1641
|
+
` Context: ${formatPath(writtenLocalConfigPath ?? getLocalConfigPath(cwd), home)}`,
|
|
1633
1642
|
);
|
|
1634
1643
|
}
|
|
1635
1644
|
|
package/src/tui/writer.ts
CHANGED
|
@@ -33,6 +33,16 @@ function ensureDir(dirPath: string): void {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Path fragments that mark a file as living in a Harmony config directory, and
|
|
38
|
+
* therefore as 0o600 rather than 0o644.
|
|
39
|
+
*
|
|
40
|
+
* `.hmy` covers `~/.hmy/agent/config.json` and the repo-local `.hmy.json` in one
|
|
41
|
+
* fragment. `.harmony-mcp` is the pre-#1082 spelling of both, kept so `setup`
|
|
42
|
+
* writing into an un-migrated directory still writes a private file.
|
|
43
|
+
*/
|
|
44
|
+
const CONFIG_DIR_MARKERS: readonly string[] = [".hmy", ".harmony-mcp"];
|
|
45
|
+
|
|
36
46
|
/**
|
|
37
47
|
* Write a file, optionally skipping if exists
|
|
38
48
|
*/
|
|
@@ -49,7 +59,17 @@ export function writeFile(
|
|
|
49
59
|
|
|
50
60
|
try {
|
|
51
61
|
ensureDir(dirname(filePath));
|
|
52
|
-
|
|
62
|
+
// 0o600 for anything under a Harmony config directory — that is where the
|
|
63
|
+
// API key lives. BOTH names are tested (#1082): the rename to `~/.hmy`
|
|
64
|
+
// moved where setup writes, so matching the old name alone would have
|
|
65
|
+
// silently written the credential 0o644 (world-readable) at the new path,
|
|
66
|
+
// and nothing about a successful write would have said so. `setup` is also
|
|
67
|
+
// still able to write into a legacy directory that has not been migrated.
|
|
68
|
+
const defaultMode = CONFIG_DIR_MARKERS.some((marker) =>
|
|
69
|
+
filePath.includes(marker),
|
|
70
|
+
)
|
|
71
|
+
? 0o600
|
|
72
|
+
: 0o644;
|
|
53
73
|
const mode = options.mode ?? defaultMode;
|
|
54
74
|
writeFileSync(filePath, content, { mode });
|
|
55
75
|
if (options.mode !== undefined) {
|