@gethmy/mcp 3.4.1 → 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 +78 -13
- package/dist/index.js +72 -9
- 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/api-client.ts +17 -0
- 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 +73 -2
- 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
|
|
@@ -5693,7 +5730,7 @@ var TOOLS = {
|
|
|
5693
5730
|
}
|
|
5694
5731
|
},
|
|
5695
5732
|
harmony_update_agent_progress: {
|
|
5696
|
-
description: "Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes. Check the reply: `session: null` with `stopped: true` means a human stopped this card's run, so reporting progress will no longer open a session here (card #770) — stop work if you were mid-run, and read `recovery` for the one legitimate way back. Otherwise a session is always returned: this call opens one if none is live.",
|
|
5733
|
+
description: "Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes. Check the reply: `session: null` with `stopped: true` means a human stopped this card's run, so reporting progress will no longer open a session here (card #770) — stop work if you were mid-run, and read `recovery` for the one legitimate way back. Otherwise a session is always returned: this call opens one if none is live. `sessionStale: true` means it opened one IN PLACE OF a session the inactivity sweep had closed (card #1067): nobody stopped you and the new row keeps the old one's steering channel and driver, so carry on — but the id changed, so poll harmony_get_pending_messages with the new `session.id` from this reply, not the one you were holding. `liveSessionIdChanged: true` is the weaker cousin: the live session is simply not the one you started, so use the new id, but do NOT assume steering survived — read `instruction`.",
|
|
5697
5734
|
inputSchema: {
|
|
5698
5735
|
type: "object",
|
|
5699
5736
|
properties: {
|
|
@@ -7654,6 +7691,32 @@ ${options}
|
|
|
7654
7691
|
if (result.session === null) {
|
|
7655
7692
|
untrack(cardId, deps.getScopeId?.());
|
|
7656
7693
|
}
|
|
7694
|
+
const scopeId = deps.getScopeId?.();
|
|
7695
|
+
const ownSession = memSession && memSession.scopeId === scopeId ? memSession : undefined;
|
|
7696
|
+
const knownSessionId = ownSession?.agentSessionId;
|
|
7697
|
+
const liveSessionId = result.session?.id;
|
|
7698
|
+
const idChangedUnderUs = knownSessionId !== undefined && liveSessionId !== undefined && liveSessionId !== knownSessionId;
|
|
7699
|
+
if (idChangedUnderUs && ownSession && liveSessionId) {
|
|
7700
|
+
ownSession.agentSessionId = liveSessionId;
|
|
7701
|
+
}
|
|
7702
|
+
const newSessionId = liveSessionId ? ` (${liveSessionId})` : "";
|
|
7703
|
+
if (result.sessionStale) {
|
|
7704
|
+
return {
|
|
7705
|
+
success: true,
|
|
7706
|
+
midSessionLearnings: 0,
|
|
7707
|
+
...result,
|
|
7708
|
+
instruction: "Your previous session had gone stale, so this report opened a new one in its place — nobody stopped you, and it kept the steering channel and driver of the run it replaced. Do NOT abandon the work. Poll harmony_get_pending_messages with the NEW session id" + newSessionId + ", not the one you were holding."
|
|
7709
|
+
};
|
|
7710
|
+
}
|
|
7711
|
+
if (idChangedUnderUs) {
|
|
7712
|
+
return {
|
|
7713
|
+
success: true,
|
|
7714
|
+
midSessionLearnings: 0,
|
|
7715
|
+
...result,
|
|
7716
|
+
liveSessionIdChanged: true,
|
|
7717
|
+
instruction: "The live session on this card is no longer the one you started — it may have been reopened, or deliberately ended and replaced. Nobody stopped you, so carry on, but poll harmony_get_pending_messages with the session id from this reply" + newSessionId + ". If steering matters to this run, do not assume it survived: start a session deliberately with harmony_start_agent_session and steerable: true."
|
|
7718
|
+
};
|
|
7719
|
+
}
|
|
7657
7720
|
return { success: true, midSessionLearnings: 0, ...result };
|
|
7658
7721
|
}
|
|
7659
7722
|
case "harmony_end_agent_session": {
|
|
@@ -9410,6 +9473,7 @@ function ensureDir(dirPath) {
|
|
|
9410
9473
|
mkdirSync5(dirPath, { recursive: true, mode: 493 });
|
|
9411
9474
|
}
|
|
9412
9475
|
}
|
|
9476
|
+
var CONFIG_DIR_MARKERS = [".hmy", ".harmony-mcp"];
|
|
9413
9477
|
function writeFile(filePath, content, options = {}) {
|
|
9414
9478
|
const exists = existsSync8(filePath);
|
|
9415
9479
|
if (exists && !options.force) {
|
|
@@ -9417,7 +9481,7 @@ function writeFile(filePath, content, options = {}) {
|
|
|
9417
9481
|
}
|
|
9418
9482
|
try {
|
|
9419
9483
|
ensureDir(dirname3(filePath));
|
|
9420
|
-
const defaultMode = filePath.includes(
|
|
9484
|
+
const defaultMode = CONFIG_DIR_MARKERS.some((marker) => filePath.includes(marker)) ? 384 : 420;
|
|
9421
9485
|
const mode = options.mode ?? defaultMode;
|
|
9422
9486
|
writeFileSync5(filePath, content, { mode });
|
|
9423
9487
|
if (options.mode !== undefined) {
|
|
@@ -10586,14 +10650,15 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
10586
10650
|
console.log(` ${colors.dim("Skipped tool allowlist — you'll be prompted per tool, or run /permissions in Claude Code later.")}`);
|
|
10587
10651
|
}
|
|
10588
10652
|
}
|
|
10653
|
+
let writtenLocalConfigPath = null;
|
|
10589
10654
|
if (selectedWorkspaceId || selectedProjectId) {
|
|
10590
10655
|
const localConfig = {};
|
|
10591
10656
|
if (selectedWorkspaceId)
|
|
10592
10657
|
localConfig.workspaceId = selectedWorkspaceId;
|
|
10593
10658
|
if (selectedProjectId)
|
|
10594
10659
|
localConfig.projectId = selectedProjectId;
|
|
10595
|
-
saveLocalConfig(localConfig, cwd);
|
|
10596
|
-
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)")}`);
|
|
10597
10662
|
if (selectedWorkspaceId || selectedProjectId) {
|
|
10598
10663
|
setActiveContext({
|
|
10599
10664
|
workspaceId: selectedWorkspaceId ?? null,
|
|
@@ -10623,7 +10688,7 @@ Specify the workspace with --workspace <id>, or select one below.`);
|
|
|
10623
10688
|
console.log(` Skills: ${installMode === "global" ? "~/.agents/skills/ (global)" : ".claude/skills/ (local)"}`);
|
|
10624
10689
|
}
|
|
10625
10690
|
if (selectedWorkspaceId || selectedProjectId) {
|
|
10626
|
-
console.log(` Context: ${formatPath(getLocalConfigPath(cwd), home)}`);
|
|
10691
|
+
console.log(` Context: ${formatPath(writtenLocalConfigPath ?? getLocalConfigPath(cwd), home)}`);
|
|
10627
10692
|
}
|
|
10628
10693
|
console.log("");
|
|
10629
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
|
|
@@ -5476,7 +5513,7 @@ var TOOLS = {
|
|
|
5476
5513
|
}
|
|
5477
5514
|
},
|
|
5478
5515
|
harmony_update_agent_progress: {
|
|
5479
|
-
description: "Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes. Check the reply: `session: null` with `stopped: true` means a human stopped this card's run, so reporting progress will no longer open a session here (card #770) — stop work if you were mid-run, and read `recovery` for the one legitimate way back. Otherwise a session is always returned: this call opens one if none is live.",
|
|
5516
|
+
description: "Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes. Check the reply: `session: null` with `stopped: true` means a human stopped this card's run, so reporting progress will no longer open a session here (card #770) — stop work if you were mid-run, and read `recovery` for the one legitimate way back. Otherwise a session is always returned: this call opens one if none is live. `sessionStale: true` means it opened one IN PLACE OF a session the inactivity sweep had closed (card #1067): nobody stopped you and the new row keeps the old one's steering channel and driver, so carry on — but the id changed, so poll harmony_get_pending_messages with the new `session.id` from this reply, not the one you were holding. `liveSessionIdChanged: true` is the weaker cousin: the live session is simply not the one you started, so use the new id, but do NOT assume steering survived — read `instruction`.",
|
|
5480
5517
|
inputSchema: {
|
|
5481
5518
|
type: "object",
|
|
5482
5519
|
properties: {
|
|
@@ -7437,6 +7474,32 @@ ${options}
|
|
|
7437
7474
|
if (result.session === null) {
|
|
7438
7475
|
untrack(cardId, deps.getScopeId?.());
|
|
7439
7476
|
}
|
|
7477
|
+
const scopeId = deps.getScopeId?.();
|
|
7478
|
+
const ownSession = memSession && memSession.scopeId === scopeId ? memSession : undefined;
|
|
7479
|
+
const knownSessionId = ownSession?.agentSessionId;
|
|
7480
|
+
const liveSessionId = result.session?.id;
|
|
7481
|
+
const idChangedUnderUs = knownSessionId !== undefined && liveSessionId !== undefined && liveSessionId !== knownSessionId;
|
|
7482
|
+
if (idChangedUnderUs && ownSession && liveSessionId) {
|
|
7483
|
+
ownSession.agentSessionId = liveSessionId;
|
|
7484
|
+
}
|
|
7485
|
+
const newSessionId = liveSessionId ? ` (${liveSessionId})` : "";
|
|
7486
|
+
if (result.sessionStale) {
|
|
7487
|
+
return {
|
|
7488
|
+
success: true,
|
|
7489
|
+
midSessionLearnings: 0,
|
|
7490
|
+
...result,
|
|
7491
|
+
instruction: "Your previous session had gone stale, so this report opened a new one in its place — nobody stopped you, and it kept the steering channel and driver of the run it replaced. Do NOT abandon the work. Poll harmony_get_pending_messages with the NEW session id" + newSessionId + ", not the one you were holding."
|
|
7492
|
+
};
|
|
7493
|
+
}
|
|
7494
|
+
if (idChangedUnderUs) {
|
|
7495
|
+
return {
|
|
7496
|
+
success: true,
|
|
7497
|
+
midSessionLearnings: 0,
|
|
7498
|
+
...result,
|
|
7499
|
+
liveSessionIdChanged: true,
|
|
7500
|
+
instruction: "The live session on this card is no longer the one you started — it may have been reopened, or deliberately ended and replaced. Nobody stopped you, so carry on, but poll harmony_get_pending_messages with the session id from this reply" + newSessionId + ". If steering matters to this run, do not assume it survived: start a session deliberately with harmony_start_agent_session and steerable: true."
|
|
7501
|
+
};
|
|
7502
|
+
}
|
|
7440
7503
|
return { success: true, midSessionLearnings: 0, ...result };
|
|
7441
7504
|
}
|
|
7442
7505
|
case "harmony_end_agent_session": {
|
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/api-client.ts
CHANGED
|
@@ -1335,6 +1335,23 @@ export class HarmonyApiClient {
|
|
|
1335
1335
|
stopReason?: "human_stopped";
|
|
1336
1336
|
/** How to legitimately work this card again (an explicit start). */
|
|
1337
1337
|
recovery?: string;
|
|
1338
|
+
/**
|
|
1339
|
+
* This write REPLACED a session the silence sweep had closed (#1067): the
|
|
1340
|
+
* returned session is a new row under a new id, carrying forward the
|
|
1341
|
+
* `steerable`/`driver` of the one it replaced. Not a stop — the run
|
|
1342
|
+
* continues — but every later poll must use the NEW `session.id`.
|
|
1343
|
+
*
|
|
1344
|
+
* Same word `getPendingMessages` answers with, deliberately: one flag name
|
|
1345
|
+
* for one fact across both calls. Absent on an ordinary update, on a
|
|
1346
|
+
* deliberate start, and from a pre-#1067 server.
|
|
1347
|
+
*/
|
|
1348
|
+
sessionStale?: boolean;
|
|
1349
|
+
/**
|
|
1350
|
+
* The swept session that was replaced — never the new one's id. Present
|
|
1351
|
+
* exactly when `sessionStale` is, so it is also the discriminator between
|
|
1352
|
+
* the server's own verdict and the MCP tool's weaker `liveSessionIdChanged`.
|
|
1353
|
+
*/
|
|
1354
|
+
replacedSessionId?: string;
|
|
1338
1355
|
}> {
|
|
1339
1356
|
return this.request("POST", `/cards/${cardId}/agent-context`, data);
|
|
1340
1357
|
}
|
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
|
@@ -1610,7 +1610,7 @@ export const TOOLS = {
|
|
|
1610
1610
|
},
|
|
1611
1611
|
harmony_update_agent_progress: {
|
|
1612
1612
|
description:
|
|
1613
|
-
"Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes. Check the reply: `session: null` with `stopped: true` means a human stopped this card's run, so reporting progress will no longer open a session here (card #770) — stop work if you were mid-run, and read `recovery` for the one legitimate way back. Otherwise a session is always returned: this call opens one if none is live.",
|
|
1613
|
+
"Update progress on an active agent session. Use to report progress percentage, current task, blockers, or status changes. Check the reply: `session: null` with `stopped: true` means a human stopped this card's run, so reporting progress will no longer open a session here (card #770) — stop work if you were mid-run, and read `recovery` for the one legitimate way back. Otherwise a session is always returned: this call opens one if none is live. `sessionStale: true` means it opened one IN PLACE OF a session the inactivity sweep had closed (card #1067): nobody stopped you and the new row keeps the old one's steering channel and driver, so carry on — but the id changed, so poll harmony_get_pending_messages with the new `session.id` from this reply, not the one you were holding. `liveSessionIdChanged: true` is the weaker cousin: the live session is simply not the one you started, so use the new id, but do NOT assume steering survived — read `instruction`.",
|
|
1614
1614
|
inputSchema: {
|
|
1615
1615
|
type: "object",
|
|
1616
1616
|
properties: {
|
|
@@ -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;
|
|
@@ -4379,6 +4379,77 @@ export async function handleToolCall(
|
|
|
4379
4379
|
untrack(cardId, deps.getScopeId?.());
|
|
4380
4380
|
}
|
|
4381
4381
|
|
|
4382
|
+
// Two DIFFERENT facts, and the earlier cut of #1067 merged them into one
|
|
4383
|
+
// sentence that was false in three reachable cases.
|
|
4384
|
+
//
|
|
4385
|
+
// `result.sessionStale` is the server's own verdict: it replaced a
|
|
4386
|
+
// recently-swept session AND carried its `steerable`/`driver` forward. Only
|
|
4387
|
+
// that verdict licenses saying the steering channel survived.
|
|
4388
|
+
//
|
|
4389
|
+
// The id comparison establishes strictly less — that the live session is
|
|
4390
|
+
// not the one this run recorded at start. It catches the case the flag
|
|
4391
|
+
// cannot: `flushMemoryActions` is fire-and-forget, sends
|
|
4392
|
+
// `implicitCreate: true` and discards its result, so when the sweep has
|
|
4393
|
+
// closed the row that bookkeeping write is what mints the replacement and
|
|
4394
|
+
// consumes the server's one `sessionStale`; this checkpoint then lands on a
|
|
4395
|
+
// live row under a new id with `created: false`. But an id change alone is
|
|
4396
|
+
// ALSO what `harmony_move_card` leaves behind — it ends the session and
|
|
4397
|
+
// untracks the card without calling `cleanupMemorySession`, so the recorded
|
|
4398
|
+
// id outlives a deliberate `completed` end — and what a lapsed recency
|
|
4399
|
+
// window leaves behind, where the new row genuinely is NOT steerable.
|
|
4400
|
+
// Claiming inheritance there tells a run its steering survived when it
|
|
4401
|
+
// did not, which suppresses the one recovery that works (an explicit
|
|
4402
|
+
// `harmony_start_agent_session` with `steerable: true`). Worse than
|
|
4403
|
+
// silence, so the two paths say different things.
|
|
4404
|
+
//
|
|
4405
|
+
// The RESYNC is unconditional and is a fix in its own right: this field
|
|
4406
|
+
// scopes working-memory writes (`sessionScopeFor`) and feeds comment
|
|
4407
|
+
// attribution, so a stale id there is wrong regardless of what is reported.
|
|
4408
|
+
const scopeId = deps.getScopeId?.();
|
|
4409
|
+
// `memorySessions` is keyed by card id ALONE at module scope, so on the
|
|
4410
|
+
// hosted transport a lookup can hand back another user's session for the
|
|
4411
|
+
// same card. Anything that CLAIMS the tracked session compares the scope
|
|
4412
|
+
// first — `chooseCommentSession` does, and #1035 is why.
|
|
4413
|
+
const ownSession =
|
|
4414
|
+
memSession && memSession.scopeId === scopeId ? memSession : undefined;
|
|
4415
|
+
const knownSessionId = ownSession?.agentSessionId;
|
|
4416
|
+
const liveSessionId = (result.session as { id?: string } | null)?.id;
|
|
4417
|
+
const idChangedUnderUs =
|
|
4418
|
+
knownSessionId !== undefined &&
|
|
4419
|
+
liveSessionId !== undefined &&
|
|
4420
|
+
liveSessionId !== knownSessionId;
|
|
4421
|
+
// Report once: the run now knows this id, so a later checkpoint on the
|
|
4422
|
+
// same session is silent again rather than crying change forever.
|
|
4423
|
+
if (idChangedUnderUs && ownSession && liveSessionId) {
|
|
4424
|
+
ownSession.agentSessionId = liveSessionId;
|
|
4425
|
+
}
|
|
4426
|
+
const newSessionId = liveSessionId ? ` (${liveSessionId})` : "";
|
|
4427
|
+
if (result.sessionStale) {
|
|
4428
|
+
return {
|
|
4429
|
+
success: true,
|
|
4430
|
+
midSessionLearnings: 0,
|
|
4431
|
+
...result,
|
|
4432
|
+
instruction:
|
|
4433
|
+
"Your previous session had gone stale, so this report opened a new one in its place — nobody stopped you, and it kept the steering channel and driver of the run it replaced. Do NOT abandon the work. Poll harmony_get_pending_messages with the NEW session id" +
|
|
4434
|
+
newSessionId +
|
|
4435
|
+
", not the one you were holding.",
|
|
4436
|
+
};
|
|
4437
|
+
}
|
|
4438
|
+
if (idChangedUnderUs) {
|
|
4439
|
+
// Everything this branch knows, and nothing it does not: no claim that
|
|
4440
|
+
// anything went stale, and none that any capability was preserved.
|
|
4441
|
+
return {
|
|
4442
|
+
success: true,
|
|
4443
|
+
midSessionLearnings: 0,
|
|
4444
|
+
...result,
|
|
4445
|
+
liveSessionIdChanged: true,
|
|
4446
|
+
instruction:
|
|
4447
|
+
"The live session on this card is no longer the one you started — it may have been reopened, or deliberately ended and replaced. Nobody stopped you, so carry on, but poll harmony_get_pending_messages with the session id from this reply" +
|
|
4448
|
+
newSessionId +
|
|
4449
|
+
". If steering matters to this run, do not assume it survived: start a session deliberately with harmony_start_agent_session and steerable: true.",
|
|
4450
|
+
};
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4382
4453
|
// Phase 0 (memory architecture v2): mid-session learning extraction removed.
|
|
4383
4454
|
return { success: true, midSessionLearnings: 0, ...result };
|
|
4384
4455
|
}
|
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) {
|