@gethmy/mcp 2.5.6 → 2.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/dist/cli.js +115 -9
- package/dist/index.js +46 -4
- package/dist/lib/api-client.js +45 -0
- package/package.json +1 -1
- package/src/api-client.ts +88 -2
- package/src/auto-session.ts +8 -4
- package/src/cli.ts +8 -3
- package/src/tui/setup.ts +92 -2
- package/src/tui/writer.ts +18 -4
package/dist/cli.js
CHANGED
|
@@ -1479,6 +1479,51 @@ class HarmonyApiClient {
|
|
|
1479
1479
|
async updateMemoryEntity(entityId, updates) {
|
|
1480
1480
|
return this.request("PUT", `/memory/entities/${entityId}`, updates);
|
|
1481
1481
|
}
|
|
1482
|
+
async harmonyRecall(options) {
|
|
1483
|
+
const fetchLimit = Math.max(options.topK ?? 3, 50);
|
|
1484
|
+
let entities = [];
|
|
1485
|
+
if (options.query) {
|
|
1486
|
+
const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
|
|
1487
|
+
project_id: options.projectId,
|
|
1488
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
1489
|
+
limit: fetchLimit
|
|
1490
|
+
});
|
|
1491
|
+
entities = search.entities ?? [];
|
|
1492
|
+
} else {
|
|
1493
|
+
const list = await this.listMemoryEntities({
|
|
1494
|
+
workspace_id: options.workspaceId,
|
|
1495
|
+
project_id: options.projectId,
|
|
1496
|
+
scope: options.scope,
|
|
1497
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
1498
|
+
tags: options.tags,
|
|
1499
|
+
min_confidence: options.minConfidence,
|
|
1500
|
+
limit: fetchLimit
|
|
1501
|
+
});
|
|
1502
|
+
entities = list.entities ?? [];
|
|
1503
|
+
}
|
|
1504
|
+
if (options.type && options.type.length > 1) {
|
|
1505
|
+
const allowed = new Set(options.type);
|
|
1506
|
+
entities = entities.filter((e) => allowed.has(e.type));
|
|
1507
|
+
}
|
|
1508
|
+
if (options.memory_tier) {
|
|
1509
|
+
entities = entities.filter((e) => e.memory_tier === options.memory_tier);
|
|
1510
|
+
}
|
|
1511
|
+
if (options.scope) {
|
|
1512
|
+
entities = entities.filter((e) => e.scope === options.scope);
|
|
1513
|
+
}
|
|
1514
|
+
if (options.tags?.length) {
|
|
1515
|
+
const wanted = new Set(options.tags);
|
|
1516
|
+
entities = entities.filter((e) => (e.tags ?? []).some((t) => wanted.has(t)));
|
|
1517
|
+
}
|
|
1518
|
+
if (typeof options.minConfidence === "number") {
|
|
1519
|
+
const threshold = options.minConfidence;
|
|
1520
|
+
entities = entities.filter((e) => typeof e.confidence === "number" && e.confidence >= threshold);
|
|
1521
|
+
}
|
|
1522
|
+
if (options.topK !== undefined) {
|
|
1523
|
+
entities = entities.slice(0, options.topK);
|
|
1524
|
+
}
|
|
1525
|
+
return { entities };
|
|
1526
|
+
}
|
|
1482
1527
|
async deleteMemoryEntity(entityId) {
|
|
1483
1528
|
return this.request("DELETE", `/memory/entities/${entityId}`);
|
|
1484
1529
|
}
|
|
@@ -1785,11 +1830,8 @@ function resolveAgentIdentity(info) {
|
|
|
1785
1830
|
var AUTO_START_TRIGGERS = new Set([
|
|
1786
1831
|
"harmony_generate_prompt",
|
|
1787
1832
|
"harmony_update_card",
|
|
1788
|
-
"harmony_move_card",
|
|
1789
1833
|
"harmony_create_subtask",
|
|
1790
|
-
"harmony_toggle_subtask"
|
|
1791
|
-
"harmony_add_label_to_card",
|
|
1792
|
-
"harmony_remove_label_from_card"
|
|
1834
|
+
"harmony_toggle_subtask"
|
|
1793
1835
|
]);
|
|
1794
1836
|
var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1795
1837
|
var CHECK_INTERVAL_MS = 60 * 1000;
|
|
@@ -4957,6 +4999,7 @@ async function refreshSkills() {
|
|
|
4957
4999
|
}
|
|
4958
5000
|
|
|
4959
5001
|
// src/tui/setup.ts
|
|
5002
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4960
5003
|
import {
|
|
4961
5004
|
existsSync as existsSync7,
|
|
4962
5005
|
lstatSync,
|
|
@@ -5729,7 +5772,13 @@ async function runDocsStep(cwd) {
|
|
|
5729
5772
|
}
|
|
5730
5773
|
|
|
5731
5774
|
// src/tui/writer.ts
|
|
5732
|
-
import {
|
|
5775
|
+
import {
|
|
5776
|
+
chmodSync,
|
|
5777
|
+
existsSync as existsSync6,
|
|
5778
|
+
mkdirSync as mkdirSync4,
|
|
5779
|
+
readFileSync as readFileSync5,
|
|
5780
|
+
writeFileSync as writeFileSync4
|
|
5781
|
+
} from "node:fs";
|
|
5733
5782
|
import { homedir as homedir3 } from "node:os";
|
|
5734
5783
|
import { dirname as dirname2 } from "node:path";
|
|
5735
5784
|
import * as p2 from "@clack/prompts";
|
|
@@ -5745,8 +5794,12 @@ function writeFile(filePath, content, options = {}) {
|
|
|
5745
5794
|
}
|
|
5746
5795
|
try {
|
|
5747
5796
|
ensureDir(dirname2(filePath));
|
|
5748
|
-
const
|
|
5797
|
+
const defaultMode = filePath.includes(".harmony-mcp") ? 384 : 420;
|
|
5798
|
+
const mode = options.mode ?? defaultMode;
|
|
5749
5799
|
writeFileSync4(filePath, content, { mode });
|
|
5800
|
+
if (options.mode !== undefined) {
|
|
5801
|
+
chmodSync(filePath, options.mode);
|
|
5802
|
+
}
|
|
5750
5803
|
return { path: filePath, action: exists ? "update" : "create" };
|
|
5751
5804
|
} catch (error) {
|
|
5752
5805
|
return {
|
|
@@ -5857,7 +5910,10 @@ async function writeFilesWithProgress(files, options = {}) {
|
|
|
5857
5910
|
} else if (file.type === "toml" && file.tomlSection) {
|
|
5858
5911
|
result = appendToToml(file.path, file.tomlSection, file.content, options);
|
|
5859
5912
|
} else {
|
|
5860
|
-
result = writeFile(file.path, file.content,
|
|
5913
|
+
result = writeFile(file.path, file.content, {
|
|
5914
|
+
...options,
|
|
5915
|
+
mode: file.mode
|
|
5916
|
+
});
|
|
5861
5917
|
}
|
|
5862
5918
|
results.push(result);
|
|
5863
5919
|
await new Promise((resolve2) => setTimeout(resolve2, 50));
|
|
@@ -5993,6 +6049,22 @@ async function fetchProjects(apiKey, workspaceId) {
|
|
|
5993
6049
|
const data = await response.json();
|
|
5994
6050
|
return data.projects || [];
|
|
5995
6051
|
}
|
|
6052
|
+
async function resolveProjectSlug(apiKey, slug) {
|
|
6053
|
+
const response = await fetch(`${API_URL}/v1/projects/resolve/${encodeURIComponent(slug)}`, {
|
|
6054
|
+
method: "GET",
|
|
6055
|
+
headers: {
|
|
6056
|
+
"Content-Type": "application/json",
|
|
6057
|
+
"X-API-Key": apiKey
|
|
6058
|
+
}
|
|
6059
|
+
});
|
|
6060
|
+
if (response.status === 404)
|
|
6061
|
+
return null;
|
|
6062
|
+
if (!response.ok) {
|
|
6063
|
+
throw new Error(`Failed to resolve project slug: ${response.status}`);
|
|
6064
|
+
}
|
|
6065
|
+
const data = await response.json();
|
|
6066
|
+
return { workspaceId: data.workspaceId, projectId: data.projectId };
|
|
6067
|
+
}
|
|
5996
6068
|
async function getAgentFiles(agentId, cwd, installMode = "global") {
|
|
5997
6069
|
const home = homedir4();
|
|
5998
6070
|
const files = [];
|
|
@@ -6035,6 +6107,24 @@ async function getAgentFiles(agentId, cwd, installMode = "global") {
|
|
|
6035
6107
|
throw new Error(`Failed to fetch ${skillFailures.length}/${installableNames.length} skill(s) from /v1/skills:
|
|
6036
6108
|
${summary}`);
|
|
6037
6109
|
}
|
|
6110
|
+
try {
|
|
6111
|
+
const updateCheckFetched = await client3.fetchSkill("hmy-update-check");
|
|
6112
|
+
const actualHash = createHash3("sha256").update(updateCheckFetched.content).digest("hex");
|
|
6113
|
+
if (actualHash !== updateCheckFetched.sha256) {
|
|
6114
|
+
throw new Error(`hmy-update-check integrity check failed: expected ${updateCheckFetched.sha256}, got ${actualHash}`);
|
|
6115
|
+
}
|
|
6116
|
+
files.push({
|
|
6117
|
+
path: join5(home, ".hmy", "bin", "hmy-update-check"),
|
|
6118
|
+
content: updateCheckFetched.content,
|
|
6119
|
+
type: "text",
|
|
6120
|
+
mode: 493
|
|
6121
|
+
});
|
|
6122
|
+
files.push({
|
|
6123
|
+
path: join5(home, ".hmy", "VERSION"),
|
|
6124
|
+
content: versionInfo.version,
|
|
6125
|
+
type: "text"
|
|
6126
|
+
});
|
|
6127
|
+
} catch {}
|
|
6038
6128
|
break;
|
|
6039
6129
|
}
|
|
6040
6130
|
case "codex": {
|
|
@@ -6227,7 +6317,7 @@ async function runSetup(options = {}) {
|
|
|
6227
6317
|
let needsApiKey = !alreadyConfigured;
|
|
6228
6318
|
let needsSkills = !skillsStatus.installed || options.force;
|
|
6229
6319
|
let needsContext = !hasContext && !options.skipContext;
|
|
6230
|
-
if (options.workspaceId || options.projectId) {
|
|
6320
|
+
if (options.workspaceId || options.projectId || options.projectSlug) {
|
|
6231
6321
|
needsContext = true;
|
|
6232
6322
|
}
|
|
6233
6323
|
let apiKey = options.apiKey || existingConfig.apiKey;
|
|
@@ -6449,6 +6539,21 @@ async function runSetup(options = {}) {
|
|
|
6449
6539
|
let selectedProjectId = selectedProjectIdFromSignup || options.projectId;
|
|
6450
6540
|
let selectedWorkspaceName = selectedWorkspaceNameFromSignup;
|
|
6451
6541
|
let selectedProjectName = selectedProjectNameFromSignup;
|
|
6542
|
+
if (options.projectSlug && apiKey && (!selectedWorkspaceId || !selectedProjectId)) {
|
|
6543
|
+
spinner3.start(`Resolving project slug "${options.projectSlug}"...`);
|
|
6544
|
+
try {
|
|
6545
|
+
const resolved = await resolveProjectSlug(apiKey, options.projectSlug);
|
|
6546
|
+
if (resolved) {
|
|
6547
|
+
selectedWorkspaceId = selectedWorkspaceId || resolved.workspaceId;
|
|
6548
|
+
selectedProjectId = selectedProjectId || resolved.projectId;
|
|
6549
|
+
spinner3.stop(colors.success(`Resolved "${options.projectSlug}"`));
|
|
6550
|
+
} else {
|
|
6551
|
+
spinner3.stop(colors.warning(`No project found for slug "${options.projectSlug}"`));
|
|
6552
|
+
}
|
|
6553
|
+
} catch (error) {
|
|
6554
|
+
spinner3.stop(colors.warning(`Could not resolve slug: ${error instanceof Error ? error.message : "unknown error"}`));
|
|
6555
|
+
}
|
|
6556
|
+
}
|
|
6452
6557
|
if (createdNewAccount) {
|
|
6453
6558
|
needsContext = false;
|
|
6454
6559
|
}
|
|
@@ -6775,13 +6880,14 @@ program.command("reset").description("Remove stored configuration").action(() =>
|
|
|
6775
6880
|
console.log(`
|
|
6776
6881
|
To reconfigure, run: npx @gethmy/mcp setup`);
|
|
6777
6882
|
});
|
|
6778
|
-
program.command("setup").description("Smart setup wizard for Harmony MCP (recommended)").option("-f, --force", "Overwrite existing configuration files").option("-k, --api-key <key>", "API key (skips prompt)").option("-e, --email <email>", "Your email for auto-assignment").option("-a, --agents <agents...>", "Agents to configure: claude, codex, cursor, windsurf").option("-l, --local", "Install skills locally in project directory").option("-g, --global", "Install skills globally (recommended)").option("-w, --workspace <id>", "Set workspace context").option("-p, --project <id>", "Set project context").option("--skip-context", "Skip workspace/project selection").option("--skip-docs", "Skip project docs scaffold/verification").option("--new", "Create a new account (skip the choice prompt)").option("-n, --name <name>", "Full name (for account creation)").action(async (options) => {
|
|
6883
|
+
program.command("setup").description("Smart setup wizard for Harmony MCP (recommended)").argument("[slug]", "Project slug — resolves to workspace + project in one step (e.g. harmony-6590761b)").option("-f, --force", "Overwrite existing configuration files").option("-k, --api-key <key>", "API key (skips prompt)").option("-e, --email <email>", "Your email for auto-assignment").option("-a, --agents <agents...>", "Agents to configure: claude, codex, cursor, windsurf").option("-l, --local", "Install skills locally in project directory").option("-g, --global", "Install skills globally (recommended)").option("-w, --workspace <id>", "Set workspace context (UUID)").option("-p, --project <id>", "Set project context (UUID)").option("--skip-context", "Skip workspace/project selection").option("--skip-docs", "Skip project docs scaffold/verification").option("--new", "Create a new account (skip the choice prompt)").option("-n, --name <name>", "Full name (for account creation)").action(async (slug, options) => {
|
|
6779
6884
|
await runSetup({
|
|
6780
6885
|
force: options.force,
|
|
6781
6886
|
apiKey: options.apiKey,
|
|
6782
6887
|
userEmail: options.email,
|
|
6783
6888
|
agents: options.agents,
|
|
6784
6889
|
installMode: options.global ? "global" : options.local ? "local" : undefined,
|
|
6890
|
+
projectSlug: slug,
|
|
6785
6891
|
workspaceId: options.workspace,
|
|
6786
6892
|
projectId: options.project,
|
|
6787
6893
|
skipContext: options.skipContext,
|
package/dist/index.js
CHANGED
|
@@ -1475,6 +1475,51 @@ class HarmonyApiClient {
|
|
|
1475
1475
|
async updateMemoryEntity(entityId, updates) {
|
|
1476
1476
|
return this.request("PUT", `/memory/entities/${entityId}`, updates);
|
|
1477
1477
|
}
|
|
1478
|
+
async harmonyRecall(options) {
|
|
1479
|
+
const fetchLimit = Math.max(options.topK ?? 3, 50);
|
|
1480
|
+
let entities = [];
|
|
1481
|
+
if (options.query) {
|
|
1482
|
+
const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
|
|
1483
|
+
project_id: options.projectId,
|
|
1484
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
1485
|
+
limit: fetchLimit
|
|
1486
|
+
});
|
|
1487
|
+
entities = search.entities ?? [];
|
|
1488
|
+
} else {
|
|
1489
|
+
const list = await this.listMemoryEntities({
|
|
1490
|
+
workspace_id: options.workspaceId,
|
|
1491
|
+
project_id: options.projectId,
|
|
1492
|
+
scope: options.scope,
|
|
1493
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
1494
|
+
tags: options.tags,
|
|
1495
|
+
min_confidence: options.minConfidence,
|
|
1496
|
+
limit: fetchLimit
|
|
1497
|
+
});
|
|
1498
|
+
entities = list.entities ?? [];
|
|
1499
|
+
}
|
|
1500
|
+
if (options.type && options.type.length > 1) {
|
|
1501
|
+
const allowed = new Set(options.type);
|
|
1502
|
+
entities = entities.filter((e) => allowed.has(e.type));
|
|
1503
|
+
}
|
|
1504
|
+
if (options.memory_tier) {
|
|
1505
|
+
entities = entities.filter((e) => e.memory_tier === options.memory_tier);
|
|
1506
|
+
}
|
|
1507
|
+
if (options.scope) {
|
|
1508
|
+
entities = entities.filter((e) => e.scope === options.scope);
|
|
1509
|
+
}
|
|
1510
|
+
if (options.tags?.length) {
|
|
1511
|
+
const wanted = new Set(options.tags);
|
|
1512
|
+
entities = entities.filter((e) => (e.tags ?? []).some((t) => wanted.has(t)));
|
|
1513
|
+
}
|
|
1514
|
+
if (typeof options.minConfidence === "number") {
|
|
1515
|
+
const threshold = options.minConfidence;
|
|
1516
|
+
entities = entities.filter((e) => typeof e.confidence === "number" && e.confidence >= threshold);
|
|
1517
|
+
}
|
|
1518
|
+
if (options.topK !== undefined) {
|
|
1519
|
+
entities = entities.slice(0, options.topK);
|
|
1520
|
+
}
|
|
1521
|
+
return { entities };
|
|
1522
|
+
}
|
|
1478
1523
|
async deleteMemoryEntity(entityId) {
|
|
1479
1524
|
return this.request("DELETE", `/memory/entities/${entityId}`);
|
|
1480
1525
|
}
|
|
@@ -1781,11 +1826,8 @@ function resolveAgentIdentity(info) {
|
|
|
1781
1826
|
var AUTO_START_TRIGGERS = new Set([
|
|
1782
1827
|
"harmony_generate_prompt",
|
|
1783
1828
|
"harmony_update_card",
|
|
1784
|
-
"harmony_move_card",
|
|
1785
1829
|
"harmony_create_subtask",
|
|
1786
|
-
"harmony_toggle_subtask"
|
|
1787
|
-
"harmony_add_label_to_card",
|
|
1788
|
-
"harmony_remove_label_from_card"
|
|
1830
|
+
"harmony_toggle_subtask"
|
|
1789
1831
|
]);
|
|
1790
1832
|
var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1791
1833
|
var CHECK_INTERVAL_MS = 60 * 1000;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1082,6 +1082,51 @@ class HarmonyApiClient {
|
|
|
1082
1082
|
async updateMemoryEntity(entityId, updates) {
|
|
1083
1083
|
return this.request("PUT", `/memory/entities/${entityId}`, updates);
|
|
1084
1084
|
}
|
|
1085
|
+
async harmonyRecall(options) {
|
|
1086
|
+
const fetchLimit = Math.max(options.topK ?? 3, 50);
|
|
1087
|
+
let entities = [];
|
|
1088
|
+
if (options.query) {
|
|
1089
|
+
const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
|
|
1090
|
+
project_id: options.projectId,
|
|
1091
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
1092
|
+
limit: fetchLimit
|
|
1093
|
+
});
|
|
1094
|
+
entities = search.entities ?? [];
|
|
1095
|
+
} else {
|
|
1096
|
+
const list = await this.listMemoryEntities({
|
|
1097
|
+
workspace_id: options.workspaceId,
|
|
1098
|
+
project_id: options.projectId,
|
|
1099
|
+
scope: options.scope,
|
|
1100
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
1101
|
+
tags: options.tags,
|
|
1102
|
+
min_confidence: options.minConfidence,
|
|
1103
|
+
limit: fetchLimit
|
|
1104
|
+
});
|
|
1105
|
+
entities = list.entities ?? [];
|
|
1106
|
+
}
|
|
1107
|
+
if (options.type && options.type.length > 1) {
|
|
1108
|
+
const allowed = new Set(options.type);
|
|
1109
|
+
entities = entities.filter((e) => allowed.has(e.type));
|
|
1110
|
+
}
|
|
1111
|
+
if (options.memory_tier) {
|
|
1112
|
+
entities = entities.filter((e) => e.memory_tier === options.memory_tier);
|
|
1113
|
+
}
|
|
1114
|
+
if (options.scope) {
|
|
1115
|
+
entities = entities.filter((e) => e.scope === options.scope);
|
|
1116
|
+
}
|
|
1117
|
+
if (options.tags?.length) {
|
|
1118
|
+
const wanted = new Set(options.tags);
|
|
1119
|
+
entities = entities.filter((e) => (e.tags ?? []).some((t) => wanted.has(t)));
|
|
1120
|
+
}
|
|
1121
|
+
if (typeof options.minConfidence === "number") {
|
|
1122
|
+
const threshold = options.minConfidence;
|
|
1123
|
+
entities = entities.filter((e) => typeof e.confidence === "number" && e.confidence >= threshold);
|
|
1124
|
+
}
|
|
1125
|
+
if (options.topK !== undefined) {
|
|
1126
|
+
entities = entities.slice(0, options.topK);
|
|
1127
|
+
}
|
|
1128
|
+
return { entities };
|
|
1129
|
+
}
|
|
1085
1130
|
async deleteMemoryEntity(entityId) {
|
|
1086
1131
|
return this.request("DELETE", `/memory/entities/${entityId}`);
|
|
1087
1132
|
}
|
package/package.json
CHANGED
package/src/api-client.ts
CHANGED
|
@@ -743,15 +743,101 @@ export class HarmonyApiClient {
|
|
|
743
743
|
scope?: string;
|
|
744
744
|
type?: string;
|
|
745
745
|
memory_tier?: string;
|
|
746
|
-
//
|
|
747
|
-
//
|
|
746
|
+
// Supersede semantics — used by Phase 1.5 review-reject back-fill to
|
|
747
|
+
// tombstone the original implement episode without hard-deleting it.
|
|
748
|
+
// The backend sets superseded_at automatically when superseded_by lands.
|
|
748
749
|
superseded_by?: string | null;
|
|
750
|
+
superseded_at?: string | null;
|
|
749
751
|
version?: number;
|
|
750
752
|
},
|
|
751
753
|
): Promise<{ entity: unknown; warnings?: string[] }> {
|
|
752
754
|
return this.request("PUT", `/memory/entities/${entityId}`, updates);
|
|
753
755
|
}
|
|
754
756
|
|
|
757
|
+
/**
|
|
758
|
+
* Retrieve memories filtered by type/tier/scope, optionally ranked by a
|
|
759
|
+
* free-text query. Wraps `searchMemoryEntities` (when a query is given)
|
|
760
|
+
* or `listMemoryEntities` and applies client-side filters that the REST
|
|
761
|
+
* surface doesn't natively expose (multi-type, memory_tier).
|
|
762
|
+
*
|
|
763
|
+
* Used by the agent daemon's read hook to surface similar past episodes
|
|
764
|
+
* before building a new task prompt (Phase 1.5).
|
|
765
|
+
*/
|
|
766
|
+
async harmonyRecall(options: {
|
|
767
|
+
workspaceId: string;
|
|
768
|
+
projectId?: string;
|
|
769
|
+
query?: string;
|
|
770
|
+
type?: string[];
|
|
771
|
+
memory_tier?: string;
|
|
772
|
+
scope?: string;
|
|
773
|
+
tags?: string[];
|
|
774
|
+
minConfidence?: number;
|
|
775
|
+
topK?: number;
|
|
776
|
+
}): Promise<{ entities: unknown[] }> {
|
|
777
|
+
// Over-fetch beyond topK so client-side filters (multi-type, memory_tier,
|
|
778
|
+
// tags) have headroom — matches the MCP server's recall path (server.ts).
|
|
779
|
+
const fetchLimit = Math.max(options.topK ?? 3, 50);
|
|
780
|
+
let entities: Array<Record<string, unknown>> = [];
|
|
781
|
+
|
|
782
|
+
if (options.query) {
|
|
783
|
+
// searchMemoryEntities accepts a single type — refine client-side
|
|
784
|
+
// when the caller passed multiple.
|
|
785
|
+
const search = await this.searchMemoryEntities(
|
|
786
|
+
options.workspaceId,
|
|
787
|
+
options.query,
|
|
788
|
+
{
|
|
789
|
+
project_id: options.projectId,
|
|
790
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
791
|
+
limit: fetchLimit,
|
|
792
|
+
},
|
|
793
|
+
);
|
|
794
|
+
entities = (search.entities ?? []) as Array<Record<string, unknown>>;
|
|
795
|
+
} else {
|
|
796
|
+
const list = await this.listMemoryEntities({
|
|
797
|
+
workspace_id: options.workspaceId,
|
|
798
|
+
project_id: options.projectId,
|
|
799
|
+
scope: options.scope,
|
|
800
|
+
type: options.type?.length === 1 ? options.type[0] : undefined,
|
|
801
|
+
tags: options.tags,
|
|
802
|
+
min_confidence: options.minConfidence,
|
|
803
|
+
limit: fetchLimit,
|
|
804
|
+
});
|
|
805
|
+
entities = (list.entities ?? []) as Array<Record<string, unknown>>;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Client-side filters: REST surface lacks multi-type and memory_tier.
|
|
809
|
+
if (options.type && options.type.length > 1) {
|
|
810
|
+
const allowed = new Set(options.type);
|
|
811
|
+
entities = entities.filter((e) => allowed.has(e.type as string));
|
|
812
|
+
}
|
|
813
|
+
if (options.memory_tier) {
|
|
814
|
+
entities = entities.filter((e) => e.memory_tier === options.memory_tier);
|
|
815
|
+
}
|
|
816
|
+
if (options.scope) {
|
|
817
|
+
entities = entities.filter((e) => e.scope === options.scope);
|
|
818
|
+
}
|
|
819
|
+
if (options.tags?.length) {
|
|
820
|
+
const wanted = new Set(options.tags);
|
|
821
|
+
entities = entities.filter((e) =>
|
|
822
|
+
((e.tags as string[]) ?? []).some((t) => wanted.has(t)),
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
if (typeof options.minConfidence === "number") {
|
|
826
|
+
const threshold = options.minConfidence;
|
|
827
|
+
entities = entities.filter(
|
|
828
|
+
(e) =>
|
|
829
|
+
typeof e.confidence === "number" &&
|
|
830
|
+
(e.confidence as number) >= threshold,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (options.topK !== undefined) {
|
|
835
|
+
entities = entities.slice(0, options.topK);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return { entities };
|
|
839
|
+
}
|
|
840
|
+
|
|
755
841
|
async deleteMemoryEntity(entityId: string): Promise<{ success: boolean }> {
|
|
756
842
|
return this.request("DELETE", `/memory/entities/${entityId}`);
|
|
757
843
|
}
|
package/src/auto-session.ts
CHANGED
|
@@ -65,15 +65,19 @@ export function resolveAgentIdentity(info: ClientInfo | null): {
|
|
|
65
65
|
return { agentIdentifier: key, agentName: displayName };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Tools that trigger auto-start of a session.
|
|
70
|
+
*
|
|
71
|
+
* Restricted to tools that signal real work on a card. Board-management ops
|
|
72
|
+
* (move, label add/remove) are excluded — they're routinely used for triage
|
|
73
|
+
* and would create false-positive sessions whose side effect (the auto-added
|
|
74
|
+
* `agent` label on the card) confuses both UI and humans.
|
|
75
|
+
*/
|
|
69
76
|
export const AUTO_START_TRIGGERS = new Set([
|
|
70
77
|
"harmony_generate_prompt",
|
|
71
78
|
"harmony_update_card",
|
|
72
|
-
"harmony_move_card",
|
|
73
79
|
"harmony_create_subtask",
|
|
74
80
|
"harmony_toggle_subtask",
|
|
75
|
-
"harmony_add_label_to_card",
|
|
76
|
-
"harmony_remove_label_from_card",
|
|
77
81
|
]);
|
|
78
82
|
|
|
79
83
|
export const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
package/src/cli.ts
CHANGED
|
@@ -139,6 +139,10 @@ program
|
|
|
139
139
|
program
|
|
140
140
|
.command("setup")
|
|
141
141
|
.description("Smart setup wizard for Harmony MCP (recommended)")
|
|
142
|
+
.argument(
|
|
143
|
+
"[slug]",
|
|
144
|
+
"Project slug — resolves to workspace + project in one step (e.g. harmony-6590761b)",
|
|
145
|
+
)
|
|
142
146
|
.option("-f, --force", "Overwrite existing configuration files")
|
|
143
147
|
.option("-k, --api-key <key>", "API key (skips prompt)")
|
|
144
148
|
.option("-e, --email <email>", "Your email for auto-assignment")
|
|
@@ -148,13 +152,13 @@ program
|
|
|
148
152
|
)
|
|
149
153
|
.option("-l, --local", "Install skills locally in project directory")
|
|
150
154
|
.option("-g, --global", "Install skills globally (recommended)")
|
|
151
|
-
.option("-w, --workspace <id>", "Set workspace context")
|
|
152
|
-
.option("-p, --project <id>", "Set project context")
|
|
155
|
+
.option("-w, --workspace <id>", "Set workspace context (UUID)")
|
|
156
|
+
.option("-p, --project <id>", "Set project context (UUID)")
|
|
153
157
|
.option("--skip-context", "Skip workspace/project selection")
|
|
154
158
|
.option("--skip-docs", "Skip project docs scaffold/verification")
|
|
155
159
|
.option("--new", "Create a new account (skip the choice prompt)")
|
|
156
160
|
.option("-n, --name <name>", "Full name (for account creation)")
|
|
157
|
-
.action(async (options) => {
|
|
161
|
+
.action(async (slug, options) => {
|
|
158
162
|
await runSetup({
|
|
159
163
|
force: options.force,
|
|
160
164
|
apiKey: options.apiKey,
|
|
@@ -165,6 +169,7 @@ program
|
|
|
165
169
|
: options.local
|
|
166
170
|
? "local"
|
|
167
171
|
: undefined,
|
|
172
|
+
projectSlug: slug,
|
|
168
173
|
workspaceId: options.workspace,
|
|
169
174
|
projectId: options.project,
|
|
170
175
|
skipContext: options.skipContext,
|
package/src/tui/setup.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import {
|
|
2
3
|
existsSync,
|
|
3
4
|
lstatSync,
|
|
@@ -38,6 +39,7 @@ export interface SetupOptions {
|
|
|
38
39
|
installMode?: InstallMode;
|
|
39
40
|
workspaceId?: string;
|
|
40
41
|
projectId?: string;
|
|
42
|
+
projectSlug?: string;
|
|
41
43
|
skipContext?: boolean;
|
|
42
44
|
skipDocs?: boolean;
|
|
43
45
|
newAccount?: boolean;
|
|
@@ -211,11 +213,40 @@ async function fetchProjects(
|
|
|
211
213
|
return data.projects || [];
|
|
212
214
|
}
|
|
213
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Resolve a project slug to {workspaceId, projectId}. Used by
|
|
218
|
+
* `npx @gethmy/mcp setup <slug>` so users don't have to copy raw UUIDs.
|
|
219
|
+
*/
|
|
220
|
+
async function resolveProjectSlug(
|
|
221
|
+
apiKey: string,
|
|
222
|
+
slug: string,
|
|
223
|
+
): Promise<{ workspaceId: string; projectId: string } | null> {
|
|
224
|
+
const response = await fetch(
|
|
225
|
+
`${API_URL}/v1/projects/resolve/${encodeURIComponent(slug)}`,
|
|
226
|
+
{
|
|
227
|
+
method: "GET",
|
|
228
|
+
headers: {
|
|
229
|
+
"Content-Type": "application/json",
|
|
230
|
+
"X-API-Key": apiKey,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (response.status === 404) return null;
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
throw new Error(`Failed to resolve project slug: ${response.status}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const data = await response.json();
|
|
241
|
+
return { workspaceId: data.workspaceId, projectId: data.projectId };
|
|
242
|
+
}
|
|
243
|
+
|
|
214
244
|
export interface FileToWrite {
|
|
215
245
|
path: string;
|
|
216
246
|
content: string;
|
|
217
247
|
type: "text" | "json" | "toml";
|
|
218
248
|
tomlSection?: string;
|
|
249
|
+
mode?: number;
|
|
219
250
|
}
|
|
220
251
|
|
|
221
252
|
export interface SymlinkToCreate {
|
|
@@ -288,6 +319,36 @@ async function getAgentFiles(
|
|
|
288
319
|
);
|
|
289
320
|
}
|
|
290
321
|
|
|
322
|
+
// Pre-populate ~/.hmy/VERSION and ~/.hmy/bin/hmy-update-check so the
|
|
323
|
+
// lazy bootstrap inside the skill preamble is bypassed on first run.
|
|
324
|
+
// Without this, the bootstrap writes "1.0.0" as a fallback whenever the
|
|
325
|
+
// version fetch times out (edge function cold start) — triggering a
|
|
326
|
+
// spurious "upgrade to v6" prompt the moment the skill is first invoked.
|
|
327
|
+
try {
|
|
328
|
+
const updateCheckFetched = await client.fetchSkill("hmy-update-check");
|
|
329
|
+
const actualHash = createHash("sha256")
|
|
330
|
+
.update(updateCheckFetched.content)
|
|
331
|
+
.digest("hex");
|
|
332
|
+
if (actualHash !== updateCheckFetched.sha256) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`hmy-update-check integrity check failed: expected ${updateCheckFetched.sha256}, got ${actualHash}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
files.push({
|
|
338
|
+
path: join(home, ".hmy", "bin", "hmy-update-check"),
|
|
339
|
+
content: updateCheckFetched.content,
|
|
340
|
+
type: "text",
|
|
341
|
+
mode: 0o755,
|
|
342
|
+
});
|
|
343
|
+
files.push({
|
|
344
|
+
path: join(home, ".hmy", "VERSION"),
|
|
345
|
+
content: versionInfo.version,
|
|
346
|
+
type: "text",
|
|
347
|
+
});
|
|
348
|
+
} catch {
|
|
349
|
+
// Non-fatal — bootstrap will install both on first skill invocation.
|
|
350
|
+
}
|
|
351
|
+
|
|
291
352
|
// Note: MCP server registration is handled separately via `claude mcp add` CLI
|
|
292
353
|
// in runSetup() after file writing, with fallback to settings.json if CLI unavailable
|
|
293
354
|
break;
|
|
@@ -524,8 +585,8 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
524
585
|
let needsSkills = !skillsStatus.installed || options.force;
|
|
525
586
|
let needsContext = !hasContext && !options.skipContext;
|
|
526
587
|
|
|
527
|
-
// If workspace/project provided via flags, we'll set context
|
|
528
|
-
if (options.workspaceId || options.projectId) {
|
|
588
|
+
// If workspace/project/slug provided via flags or argument, we'll set context
|
|
589
|
+
if (options.workspaceId || options.projectId || options.projectSlug) {
|
|
529
590
|
needsContext = true;
|
|
530
591
|
}
|
|
531
592
|
|
|
@@ -806,6 +867,35 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
|
|
|
806
867
|
selectedWorkspaceNameFromSignup;
|
|
807
868
|
let selectedProjectName: string | undefined = selectedProjectNameFromSignup;
|
|
808
869
|
|
|
870
|
+
// Resolve project slug shorthand (e.g. `npx @gethmy/mcp setup harmony-6590761b`).
|
|
871
|
+
// Slug wins over --workspace/--project flags only when those aren't already set
|
|
872
|
+
// from signup or explicit flags.
|
|
873
|
+
if (
|
|
874
|
+
options.projectSlug &&
|
|
875
|
+
apiKey &&
|
|
876
|
+
(!selectedWorkspaceId || !selectedProjectId)
|
|
877
|
+
) {
|
|
878
|
+
spinner.start(`Resolving project slug "${options.projectSlug}"...`);
|
|
879
|
+
try {
|
|
880
|
+
const resolved = await resolveProjectSlug(apiKey, options.projectSlug);
|
|
881
|
+
if (resolved) {
|
|
882
|
+
selectedWorkspaceId = selectedWorkspaceId || resolved.workspaceId;
|
|
883
|
+
selectedProjectId = selectedProjectId || resolved.projectId;
|
|
884
|
+
spinner.stop(colors.success(`Resolved "${options.projectSlug}"`));
|
|
885
|
+
} else {
|
|
886
|
+
spinner.stop(
|
|
887
|
+
colors.warning(`No project found for slug "${options.projectSlug}"`),
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
} catch (error) {
|
|
891
|
+
spinner.stop(
|
|
892
|
+
colors.warning(
|
|
893
|
+
`Could not resolve slug: ${error instanceof Error ? error.message : "unknown error"}`,
|
|
894
|
+
),
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
809
899
|
// Skip context selection if we just created a new account
|
|
810
900
|
if (createdNewAccount) {
|
|
811
901
|
needsContext = false;
|
package/src/tui/writer.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
2
8
|
import { homedir } from "node:os";
|
|
3
9
|
import { dirname } from "node:path";
|
|
4
10
|
import * as p from "@clack/prompts";
|
|
@@ -15,6 +21,7 @@ export interface FileResult {
|
|
|
15
21
|
interface WriteOptions {
|
|
16
22
|
force?: boolean;
|
|
17
23
|
merge?: boolean;
|
|
24
|
+
mode?: number;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
/**
|
|
@@ -42,9 +49,12 @@ export function writeFile(
|
|
|
42
49
|
|
|
43
50
|
try {
|
|
44
51
|
ensureDir(dirname(filePath));
|
|
45
|
-
|
|
46
|
-
const mode =
|
|
52
|
+
const defaultMode = filePath.includes(".harmony-mcp") ? 0o600 : 0o644;
|
|
53
|
+
const mode = options.mode ?? defaultMode;
|
|
47
54
|
writeFileSync(filePath, content, { mode });
|
|
55
|
+
if (options.mode !== undefined) {
|
|
56
|
+
chmodSync(filePath, options.mode);
|
|
57
|
+
}
|
|
48
58
|
return { path: filePath, action: exists ? "update" : "create" };
|
|
49
59
|
} catch (error) {
|
|
50
60
|
return {
|
|
@@ -183,6 +193,7 @@ export async function writeFilesWithProgress(
|
|
|
183
193
|
type: "text" | "json" | "toml";
|
|
184
194
|
jsonKey?: string;
|
|
185
195
|
tomlSection?: string;
|
|
196
|
+
mode?: number;
|
|
186
197
|
}>,
|
|
187
198
|
options: WriteOptions = {},
|
|
188
199
|
): Promise<FileResult[]> {
|
|
@@ -201,7 +212,10 @@ export async function writeFilesWithProgress(
|
|
|
201
212
|
} else if (file.type === "toml" && file.tomlSection) {
|
|
202
213
|
result = appendToToml(file.path, file.tomlSection, file.content, options);
|
|
203
214
|
} else {
|
|
204
|
-
result = writeFile(file.path, file.content,
|
|
215
|
+
result = writeFile(file.path, file.content, {
|
|
216
|
+
...options,
|
|
217
|
+
mode: file.mode,
|
|
218
|
+
});
|
|
205
219
|
}
|
|
206
220
|
|
|
207
221
|
results.push(result);
|