@gethmy/mcp 3.5.0 → 3.7.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 +258 -12
- package/dist/index.js +252 -8
- package/dist/lib/api-client.js +117 -8
- package/dist/lib/config.js +56 -9
- package/dist/lib/oauth-refresh.js +49 -8
- package/dist/run-hook-cli.js +264 -8
- package/package.json +3 -2
- package/src/config.ts +162 -18
- package/src/hook-install.ts +1 -1
- package/src/oauth-refresh.ts +1 -1
- package/src/run-hook.ts +1 -1
- 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/src/run-redaction.ts +0 -461
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 `@harmony/shared`'s `runRedaction.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-hook.ts
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* This module is pure. The process that calls it does the I/O.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import { redactToolCall } from "
|
|
35
|
+
import { redactToolCall } from "@harmony/shared";
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* The subset of the harness's `PostToolUse` stdin payload this reads.
|
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) {
|