@codazen/harmonica-mcp 0.23.0 → 0.24.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/index.js +61 -43
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24696,24 +24696,35 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
24696
24696
|
title: external_exports.string().describe("Title for this revision"),
|
|
24697
24697
|
description: external_exports.string().describe("What this revision delivers"),
|
|
24698
24698
|
changeSummary: external_exports.string().describe("Brief summary of the change"),
|
|
24699
|
+
beatVersionId: external_exports.string().min(1).optional().describe("Optional Beat Version ID to associate this revision with"),
|
|
24699
24700
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
24700
24701
|
priority: external_exports.coerce.number().optional().describe("Priority (lower = higher priority)"),
|
|
24701
24702
|
estimatedEffort: external_exports.string().optional().describe("Estimated effort (e.g., S, M, L, XL)"),
|
|
24702
24703
|
humanAssignee: humanAssigneeSchema.optional().describe('Human assignee (e.g. { email: "mmerchant@codazen.com", name: "Mike Merchant" })'),
|
|
24703
24704
|
agentAssignee: agentAssigneeSchema.optional().describe('Agent assignee (e.g. { agentId: "maya", name: "Maya" })')
|
|
24704
24705
|
},
|
|
24705
|
-
async ({ beatId, title, description, changeSummary, tags, priority, estimatedEffort, humanAssignee, agentAssignee }) => {
|
|
24706
|
+
async ({ beatId, title, description, changeSummary, beatVersionId, tags, priority, estimatedEffort, humanAssignee, agentAssignee }) => {
|
|
24706
24707
|
try {
|
|
24707
24708
|
await assertBeatInOrg(client, beatId, ctx.orgId);
|
|
24708
24709
|
const beat = await client.getBeat(beatId);
|
|
24709
24710
|
if (!beat) {
|
|
24710
24711
|
return { content: [{ type: "text", text: `Beat not found: "${beatId}"` }], isError: true };
|
|
24711
24712
|
}
|
|
24713
|
+
if (beatVersionId !== void 0) {
|
|
24714
|
+
const bv = await client.getBeatVersion(beatVersionId);
|
|
24715
|
+
if (!bv) {
|
|
24716
|
+
return { content: [{ type: "text", text: `Beat Version not found: "${beatVersionId}"` }], isError: true };
|
|
24717
|
+
}
|
|
24718
|
+
if (bv.beatId !== beatId || bv.projectId !== beat.projectId) {
|
|
24719
|
+
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" belongs to a different Beat.` }], isError: true };
|
|
24720
|
+
}
|
|
24721
|
+
}
|
|
24712
24722
|
const { revision, warnings } = await client.createRevision(beat.projectId, beatId, {
|
|
24713
24723
|
title,
|
|
24714
24724
|
description,
|
|
24715
24725
|
changeSummary,
|
|
24716
24726
|
changeImportance: "moderate",
|
|
24727
|
+
beatVersionId,
|
|
24717
24728
|
tags,
|
|
24718
24729
|
priority,
|
|
24719
24730
|
estimatedEffort,
|
|
@@ -24750,23 +24761,34 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
24750
24761
|
changeSummary: external_exports.string().describe("Why this change is being made (recorded in audit trail)"),
|
|
24751
24762
|
title: external_exports.string().optional().describe("New title"),
|
|
24752
24763
|
description: external_exports.string().optional().describe("New description (what this revision delivers)"),
|
|
24764
|
+
beatVersionId: external_exports.string().min(1).nullable().optional().describe("Beat Version ID to associate \u2014 set null to clear"),
|
|
24753
24765
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
24754
24766
|
priority: external_exports.coerce.number().optional().describe("Priority (lower = higher priority)"),
|
|
24755
24767
|
estimatedEffort: external_exports.string().optional().describe("Effort estimate (e.g., S, M, L, XL)"),
|
|
24756
24768
|
humanAssignee: humanAssigneeSchema.nullable().optional().describe("Human assignee \u2014 set null to clear"),
|
|
24757
24769
|
agentAssignee: agentAssigneeSchema.nullable().optional().describe("Agent assignee \u2014 set null to clear")
|
|
24758
24770
|
},
|
|
24759
|
-
async ({ revisionId, changeSummary, ...changes }) => {
|
|
24771
|
+
async ({ revisionId, changeSummary, beatVersionId, ...changes }) => {
|
|
24760
24772
|
try {
|
|
24761
24773
|
const revision = await client.getRevision(revisionId);
|
|
24762
24774
|
if (!revision) {
|
|
24763
24775
|
return { content: [{ type: "text", text: `Revision not found: "${revisionId}"` }], isError: true };
|
|
24764
24776
|
}
|
|
24765
24777
|
await assertBeatInOrg(client, revision.beatId, ctx.orgId);
|
|
24778
|
+
if (typeof beatVersionId === "string") {
|
|
24779
|
+
const bv = await client.getBeatVersion(beatVersionId);
|
|
24780
|
+
if (!bv) {
|
|
24781
|
+
return { content: [{ type: "text", text: `Beat Version not found: "${beatVersionId}"` }], isError: true };
|
|
24782
|
+
}
|
|
24783
|
+
if (bv.beatId !== revision.beatId || bv.projectId !== revision.projectId) {
|
|
24784
|
+
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" belongs to a different Beat.` }], isError: true };
|
|
24785
|
+
}
|
|
24786
|
+
}
|
|
24766
24787
|
const updates = { changeSummary };
|
|
24767
24788
|
for (const [k, v] of Object.entries(changes)) {
|
|
24768
24789
|
if (v !== void 0) updates[k] = v;
|
|
24769
24790
|
}
|
|
24791
|
+
if (beatVersionId !== void 0) updates.beatVersionId = beatVersionId;
|
|
24770
24792
|
if (Object.keys(updates).length <= 1) {
|
|
24771
24793
|
return { content: [{ type: "text", text: "No fields provided to update." }], isError: true };
|
|
24772
24794
|
}
|
|
@@ -24910,37 +24932,29 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
24910
24932
|
isError: true
|
|
24911
24933
|
};
|
|
24912
24934
|
}
|
|
24935
|
+
const trimmed = snapshotInput.trim();
|
|
24936
|
+
const importOptions = { targetOrgId: ctx.orgId, targetUserId: ctx.user.userId };
|
|
24937
|
+
if (trimmed.startsWith("https://")) {
|
|
24938
|
+
if (isPrivateHost(trimmed)) {
|
|
24939
|
+
throw new Error("Snapshot URL must point to a public host \u2014 private, link-local, and loopback addresses are not permitted.");
|
|
24940
|
+
}
|
|
24941
|
+
const result2 = await client.importProjectSnapshotFromUrl(trimmed, importOptions);
|
|
24942
|
+
return { content: [{ type: "text", text: formatImportSummary(result2) }] };
|
|
24943
|
+
}
|
|
24944
|
+
if (trimmed.startsWith("http://")) {
|
|
24945
|
+
throw new Error("Snapshot URL must use https:// \u2014 http:// is not permitted.");
|
|
24946
|
+
}
|
|
24913
24947
|
const snapshotJson = await resolveSnapshotInput(snapshotInput);
|
|
24914
24948
|
let parsed;
|
|
24915
24949
|
try {
|
|
24916
24950
|
parsed = JSON.parse(snapshotJson);
|
|
24917
24951
|
} catch {
|
|
24918
|
-
|
|
24919
|
-
throw new Error(`Snapshot is not valid JSON${urlHint}. The URL may have returned an error page instead of snapshot data.`);
|
|
24952
|
+
throw new Error("Snapshot is not valid JSON. The content may be corrupted.");
|
|
24920
24953
|
}
|
|
24921
|
-
assertSnapshotShape(parsed,
|
|
24954
|
+
assertSnapshotShape(parsed, "");
|
|
24922
24955
|
const normalized = normalizeSnapshot(parsed);
|
|
24923
|
-
const result = await client.importProjectSnapshot(normalized,
|
|
24924
|
-
|
|
24925
|
-
targetUserId: ctx.user.userId
|
|
24926
|
-
});
|
|
24927
|
-
const summary = [
|
|
24928
|
-
`Imported project: ${result.projectId}`,
|
|
24929
|
-
` Beats: ${result.counts.beats}`,
|
|
24930
|
-
` Proposals: ${result.counts.proposals}`,
|
|
24931
|
-
` Revisions: ${result.counts.revisions}`,
|
|
24932
|
-
` Activities: ${result.counts.activities}`,
|
|
24933
|
-
` Questions: ${result.counts.questions}`,
|
|
24934
|
-
` Notes: ${result.counts.notes}`,
|
|
24935
|
-
` Tasks: ${result.counts.tasks}`,
|
|
24936
|
-
` Submissions: ${result.counts.submissions}`,
|
|
24937
|
-
` PromptLogs: ${result.counts.promptLogs}`
|
|
24938
|
-
];
|
|
24939
|
-
if (result.errors.length > 0) {
|
|
24940
|
-
summary.push("", `Errors (${result.errors.length}):`);
|
|
24941
|
-
result.errors.forEach((e) => summary.push(` - ${e}`));
|
|
24942
|
-
}
|
|
24943
|
-
return { content: [{ type: "text", text: summary.join("\n") }] };
|
|
24956
|
+
const result = await client.importProjectSnapshot(normalized, importOptions);
|
|
24957
|
+
return { content: [{ type: "text", text: formatImportSummary(result) }] };
|
|
24944
24958
|
}
|
|
24945
24959
|
);
|
|
24946
24960
|
}
|
|
@@ -24969,27 +24983,30 @@ function normalizeSnapshot(raw) {
|
|
|
24969
24983
|
}
|
|
24970
24984
|
async function resolveSnapshotInput(input) {
|
|
24971
24985
|
const trimmed = input.trim();
|
|
24972
|
-
if (trimmed.startsWith("https://")) {
|
|
24973
|
-
if (isPrivateHost(trimmed)) {
|
|
24974
|
-
throw new Error("Snapshot URL must point to a public host \u2014 private, link-local, and loopback addresses are not permitted.");
|
|
24975
|
-
}
|
|
24976
|
-
let res;
|
|
24977
|
-
try {
|
|
24978
|
-
res = await fetch(trimmed, { redirect: "error", signal: AbortSignal.timeout(3e4) });
|
|
24979
|
-
} catch (err) {
|
|
24980
|
-
throw new Error(`Failed to fetch snapshot URL: ${err instanceof Error ? err.message : String(err)}`);
|
|
24981
|
-
}
|
|
24982
|
-
if (!res.ok) throw new Error(`Failed to fetch snapshot from URL: ${res.status}`);
|
|
24983
|
-
return res.text();
|
|
24984
|
-
}
|
|
24985
|
-
if (trimmed.startsWith("http://")) {
|
|
24986
|
-
throw new Error("Snapshot URL must use https:// \u2014 http:// is not permitted.");
|
|
24987
|
-
}
|
|
24988
24986
|
if (trimmed.startsWith("/") || trimmed.startsWith("~")) {
|
|
24989
24987
|
return (0, import_promises.readFile)(trimmed, "utf-8");
|
|
24990
24988
|
}
|
|
24991
24989
|
return trimmed;
|
|
24992
24990
|
}
|
|
24991
|
+
function formatImportSummary(result) {
|
|
24992
|
+
const lines = [
|
|
24993
|
+
`Imported project: ${result.projectId}`,
|
|
24994
|
+
` Beats: ${result.counts.beats}`,
|
|
24995
|
+
` Proposals: ${result.counts.proposals}`,
|
|
24996
|
+
` Revisions: ${result.counts.revisions}`,
|
|
24997
|
+
` Activities: ${result.counts.activities}`,
|
|
24998
|
+
` Questions: ${result.counts.questions}`,
|
|
24999
|
+
` Notes: ${result.counts.notes}`,
|
|
25000
|
+
` Tasks: ${result.counts.tasks}`,
|
|
25001
|
+
` Submissions: ${result.counts.submissions}`,
|
|
25002
|
+
` PromptLogs: ${result.counts.promptLogs}`
|
|
25003
|
+
];
|
|
25004
|
+
if (result.errors.length > 0) {
|
|
25005
|
+
lines.push("", `Errors (${result.errors.length}):`);
|
|
25006
|
+
result.errors.forEach((e) => lines.push(` - ${e}`));
|
|
25007
|
+
}
|
|
25008
|
+
return lines.join("\n");
|
|
25009
|
+
}
|
|
24993
25010
|
function isPrivateHost(url) {
|
|
24994
25011
|
let hostname2;
|
|
24995
25012
|
try {
|
|
@@ -26418,6 +26435,7 @@ function createHttpClient(config2) {
|
|
|
26418
26435
|
return download;
|
|
26419
26436
|
},
|
|
26420
26437
|
importProjectSnapshot: (snapshot, options) => request("POST", "/api/projects/import", { ...snapshot, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
|
|
26438
|
+
importProjectSnapshotFromUrl: (url2, options) => request("POST", "/api/projects/import", { snapshotUrl: url2, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
|
|
26421
26439
|
// Embedding similarity
|
|
26422
26440
|
embedProjectEntities: (projectId) => request("POST", `/api/projects/${encodeURIComponent(projectId)}/embeddings/generate`),
|
|
26423
26441
|
findSimilarNotes: async (noteId, projectId, options) => {
|
|
@@ -26725,7 +26743,7 @@ function loadConfig() {
|
|
|
26725
26743
|
};
|
|
26726
26744
|
}
|
|
26727
26745
|
async function main() {
|
|
26728
|
-
console.error(`[harmonica-mcp] v${"0.
|
|
26746
|
+
console.error(`[harmonica-mcp] v${"0.24.0"} starting\u2026`);
|
|
26729
26747
|
const config2 = loadConfig();
|
|
26730
26748
|
const client = createHttpClient({
|
|
26731
26749
|
apiBaseUrl: config2.apiBaseUrl,
|