@codazen/harmonica-mcp 0.23.0 → 0.25.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 +358 -45
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22214,6 +22214,127 @@ function formatNoteDetail(note) {
|
|
|
22214
22214
|
return parts.join("\n");
|
|
22215
22215
|
}
|
|
22216
22216
|
|
|
22217
|
+
// ../../libs/harmonica-services/src/mcp/formatters/session-formatter.ts
|
|
22218
|
+
var TYPE_LABELS2 = {
|
|
22219
|
+
planning: "Planning",
|
|
22220
|
+
implementation: "Implementation",
|
|
22221
|
+
triage: "Triage",
|
|
22222
|
+
assessment: "Assessment",
|
|
22223
|
+
composition: "Composition",
|
|
22224
|
+
baseline: "Baseline"
|
|
22225
|
+
};
|
|
22226
|
+
var STATUS_LABELS = {
|
|
22227
|
+
running: "Running",
|
|
22228
|
+
succeeded: "Succeeded",
|
|
22229
|
+
failed: "Failed",
|
|
22230
|
+
timed_out: "Timed Out",
|
|
22231
|
+
cancelled: "Cancelled"
|
|
22232
|
+
};
|
|
22233
|
+
var EVENT_TAIL_LIMIT = 10;
|
|
22234
|
+
function formatSessionSummary(session) {
|
|
22235
|
+
const type = TYPE_LABELS2[session.sessionType] ?? session.sessionType;
|
|
22236
|
+
const status = STATUS_LABELS[session.status] ?? session.status;
|
|
22237
|
+
const duration3 = session.endedAt ? ` \xB7 ${formatRelativeWindow(session.startedAt, session.endedAt)}` : "";
|
|
22238
|
+
const scope = formatScopeSuffix(session);
|
|
22239
|
+
const actor = session.actorPersona ? ` \xB7 ${session.actorPersona}` : "";
|
|
22240
|
+
return `- **${type}** (${session.sessionId}) [${status}]${actor}${duration3}${scope}`;
|
|
22241
|
+
}
|
|
22242
|
+
function formatSessionList(sessions, scopeLabel) {
|
|
22243
|
+
if (sessions.length === 0) {
|
|
22244
|
+
return `No sessions found for ${scopeLabel}.`;
|
|
22245
|
+
}
|
|
22246
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
22247
|
+
for (const s of sessions) {
|
|
22248
|
+
const bucket = grouped.get(s.status) ?? [];
|
|
22249
|
+
bucket.push(s);
|
|
22250
|
+
grouped.set(s.status, bucket);
|
|
22251
|
+
}
|
|
22252
|
+
const parts = [`**${sessions.length} session${sessions.length === 1 ? "" : "s"}** (${scopeLabel})`, ""];
|
|
22253
|
+
for (const [status, items] of grouped) {
|
|
22254
|
+
parts.push(`### ${STATUS_LABELS[status] ?? status} (${items.length})`);
|
|
22255
|
+
for (const item of items) {
|
|
22256
|
+
parts.push(formatSessionSummary(item));
|
|
22257
|
+
}
|
|
22258
|
+
parts.push("");
|
|
22259
|
+
}
|
|
22260
|
+
return parts.join("\n").trimEnd();
|
|
22261
|
+
}
|
|
22262
|
+
function formatSessionDetail(session) {
|
|
22263
|
+
const type = TYPE_LABELS2[session.sessionType] ?? session.sessionType;
|
|
22264
|
+
const status = STATUS_LABELS[session.status] ?? session.status;
|
|
22265
|
+
const parts = [
|
|
22266
|
+
`# Session: ${session.sessionId}`,
|
|
22267
|
+
"",
|
|
22268
|
+
`**Type:** ${type}`,
|
|
22269
|
+
`**Status:** ${status}`,
|
|
22270
|
+
`**Project:** ${session.projectId}`,
|
|
22271
|
+
`**Actor:** ${session.actor.agentPersona}${session.actor.agentRole ? ` (${session.actor.agentRole})` : ""}`
|
|
22272
|
+
];
|
|
22273
|
+
if (session.actor.initiator) {
|
|
22274
|
+
const i = session.actor.initiator;
|
|
22275
|
+
parts.push(`**Initiated by:** ${i.type}/${i.id}${i.name ? ` (${i.name})` : ""}`);
|
|
22276
|
+
}
|
|
22277
|
+
if (session.beatId) parts.push(`**Beat:** ${session.beatId}`);
|
|
22278
|
+
if (session.beatVersionId) parts.push(`**Beat Version:** ${session.beatVersionId}`);
|
|
22279
|
+
if (session.revisionId) parts.push(`**Revision:** ${session.revisionId}`);
|
|
22280
|
+
if (session.triggerSource) parts.push(`**Trigger:** ${session.triggerSource}`);
|
|
22281
|
+
parts.push("");
|
|
22282
|
+
parts.push(`**Started:** ${session.startedAt}`);
|
|
22283
|
+
if (session.endedAt) parts.push(`**Ended:** ${session.endedAt}`);
|
|
22284
|
+
if (session.metrics) {
|
|
22285
|
+
parts.push("", "### Metrics");
|
|
22286
|
+
const m = session.metrics;
|
|
22287
|
+
if (m.durationMs != null) parts.push(`- Duration: ${formatDurationMs(m.durationMs)}`);
|
|
22288
|
+
if (m.toolCallCount != null) parts.push(`- Tool calls: ${m.toolCallCount}`);
|
|
22289
|
+
if (m.inputTokens != null) parts.push(`- Input tokens: ${m.inputTokens}`);
|
|
22290
|
+
if (m.outputTokens != null) parts.push(`- Output tokens: ${m.outputTokens}`);
|
|
22291
|
+
if (m.totalTokens != null) parts.push(`- Total tokens: ${m.totalTokens}`);
|
|
22292
|
+
if (m.estimatedCostUsd != null) parts.push(`- Est. cost: $${m.estimatedCostUsd.toFixed(4)}`);
|
|
22293
|
+
}
|
|
22294
|
+
if (session.errorMessage) {
|
|
22295
|
+
parts.push("", "### Error");
|
|
22296
|
+
parts.push(session.errorMessage);
|
|
22297
|
+
}
|
|
22298
|
+
const toolEvents = session.toolEvents ?? [];
|
|
22299
|
+
if (toolEvents.length > 0) {
|
|
22300
|
+
parts.push("", `### Tool Events (showing last ${Math.min(toolEvents.length, EVENT_TAIL_LIMIT)} of ${toolEvents.length})`);
|
|
22301
|
+
for (const ev of toolEvents.slice(-EVENT_TAIL_LIMIT)) {
|
|
22302
|
+
const dur = ev.durationMs != null ? ` (${ev.durationMs}ms)` : "";
|
|
22303
|
+
const result = ev.resultStatus ? ` \u2014 ${ev.resultStatus}` : "";
|
|
22304
|
+
parts.push(`- [${ev.index}] ${ev.toolName}${result}${dur}`);
|
|
22305
|
+
}
|
|
22306
|
+
}
|
|
22307
|
+
const textEvents = session.textEvents ?? [];
|
|
22308
|
+
if (textEvents.length > 0) {
|
|
22309
|
+
parts.push("", `### Text Events (${textEvents.length})`);
|
|
22310
|
+
for (const ev of textEvents.slice(-EVENT_TAIL_LIMIT)) {
|
|
22311
|
+
const preview = ev.content.length > 160 ? `${ev.content.slice(0, 160)}\u2026` : ev.content;
|
|
22312
|
+
parts.push(`- [${ev.index}] ${preview}`);
|
|
22313
|
+
}
|
|
22314
|
+
}
|
|
22315
|
+
parts.push("", `_Created: ${session.createdAt}_`);
|
|
22316
|
+
return parts.join("\n");
|
|
22317
|
+
}
|
|
22318
|
+
function formatScopeSuffix(session) {
|
|
22319
|
+
const parts = [];
|
|
22320
|
+
if ("beatId" in session && session.beatId) parts.push(`Beat: ${session.beatId}`);
|
|
22321
|
+
if ("revisionId" in session && session.revisionId) parts.push(`Rev: ${session.revisionId}`);
|
|
22322
|
+
return parts.length > 0 ? ` [${parts.join(" \xB7 ")}]` : "";
|
|
22323
|
+
}
|
|
22324
|
+
function formatRelativeWindow(startedAt, endedAt) {
|
|
22325
|
+
const ms = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
22326
|
+
if (!Number.isFinite(ms) || ms < 0) return "";
|
|
22327
|
+
return formatDurationMs(ms);
|
|
22328
|
+
}
|
|
22329
|
+
function formatDurationMs(ms) {
|
|
22330
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
22331
|
+
const seconds = Math.round(ms / 1e3);
|
|
22332
|
+
if (seconds < 60) return `${seconds}s`;
|
|
22333
|
+
const minutes = Math.floor(seconds / 60);
|
|
22334
|
+
const remSec = seconds % 60;
|
|
22335
|
+
return remSec === 0 ? `${minutes}m` : `${minutes}m ${remSec}s`;
|
|
22336
|
+
}
|
|
22337
|
+
|
|
22217
22338
|
// ../../libs/harmonica-services/src/mcp/tools/beat-tools.ts
|
|
22218
22339
|
function registerBeatTools(server, ctx, client) {
|
|
22219
22340
|
server.tool(
|
|
@@ -24696,24 +24817,35 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
24696
24817
|
title: external_exports.string().describe("Title for this revision"),
|
|
24697
24818
|
description: external_exports.string().describe("What this revision delivers"),
|
|
24698
24819
|
changeSummary: external_exports.string().describe("Brief summary of the change"),
|
|
24820
|
+
beatVersionId: external_exports.string().min(1).optional().describe("Optional Beat Version ID to associate this revision with"),
|
|
24699
24821
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
24700
24822
|
priority: external_exports.coerce.number().optional().describe("Priority (lower = higher priority)"),
|
|
24701
24823
|
estimatedEffort: external_exports.string().optional().describe("Estimated effort (e.g., S, M, L, XL)"),
|
|
24702
24824
|
humanAssignee: humanAssigneeSchema.optional().describe('Human assignee (e.g. { email: "mmerchant@codazen.com", name: "Mike Merchant" })'),
|
|
24703
24825
|
agentAssignee: agentAssigneeSchema.optional().describe('Agent assignee (e.g. { agentId: "maya", name: "Maya" })')
|
|
24704
24826
|
},
|
|
24705
|
-
async ({ beatId, title, description, changeSummary, tags, priority, estimatedEffort, humanAssignee, agentAssignee }) => {
|
|
24827
|
+
async ({ beatId, title, description, changeSummary, beatVersionId, tags, priority, estimatedEffort, humanAssignee, agentAssignee }) => {
|
|
24706
24828
|
try {
|
|
24707
24829
|
await assertBeatInOrg(client, beatId, ctx.orgId);
|
|
24708
24830
|
const beat = await client.getBeat(beatId);
|
|
24709
24831
|
if (!beat) {
|
|
24710
24832
|
return { content: [{ type: "text", text: `Beat not found: "${beatId}"` }], isError: true };
|
|
24711
24833
|
}
|
|
24834
|
+
if (beatVersionId !== void 0) {
|
|
24835
|
+
const bv = await client.getBeatVersion(beatVersionId);
|
|
24836
|
+
if (!bv) {
|
|
24837
|
+
return { content: [{ type: "text", text: `Beat Version not found: "${beatVersionId}"` }], isError: true };
|
|
24838
|
+
}
|
|
24839
|
+
if (bv.beatId !== beatId || bv.projectId !== beat.projectId) {
|
|
24840
|
+
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" belongs to a different Beat.` }], isError: true };
|
|
24841
|
+
}
|
|
24842
|
+
}
|
|
24712
24843
|
const { revision, warnings } = await client.createRevision(beat.projectId, beatId, {
|
|
24713
24844
|
title,
|
|
24714
24845
|
description,
|
|
24715
24846
|
changeSummary,
|
|
24716
24847
|
changeImportance: "moderate",
|
|
24848
|
+
beatVersionId,
|
|
24717
24849
|
tags,
|
|
24718
24850
|
priority,
|
|
24719
24851
|
estimatedEffort,
|
|
@@ -24750,23 +24882,34 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
24750
24882
|
changeSummary: external_exports.string().describe("Why this change is being made (recorded in audit trail)"),
|
|
24751
24883
|
title: external_exports.string().optional().describe("New title"),
|
|
24752
24884
|
description: external_exports.string().optional().describe("New description (what this revision delivers)"),
|
|
24885
|
+
beatVersionId: external_exports.string().min(1).nullable().optional().describe("Beat Version ID to associate \u2014 set null to clear"),
|
|
24753
24886
|
tags: external_exports.array(external_exports.string()).optional().describe("Tags for categorization"),
|
|
24754
24887
|
priority: external_exports.coerce.number().optional().describe("Priority (lower = higher priority)"),
|
|
24755
24888
|
estimatedEffort: external_exports.string().optional().describe("Effort estimate (e.g., S, M, L, XL)"),
|
|
24756
24889
|
humanAssignee: humanAssigneeSchema.nullable().optional().describe("Human assignee \u2014 set null to clear"),
|
|
24757
24890
|
agentAssignee: agentAssigneeSchema.nullable().optional().describe("Agent assignee \u2014 set null to clear")
|
|
24758
24891
|
},
|
|
24759
|
-
async ({ revisionId, changeSummary, ...changes }) => {
|
|
24892
|
+
async ({ revisionId, changeSummary, beatVersionId, ...changes }) => {
|
|
24760
24893
|
try {
|
|
24761
24894
|
const revision = await client.getRevision(revisionId);
|
|
24762
24895
|
if (!revision) {
|
|
24763
24896
|
return { content: [{ type: "text", text: `Revision not found: "${revisionId}"` }], isError: true };
|
|
24764
24897
|
}
|
|
24765
24898
|
await assertBeatInOrg(client, revision.beatId, ctx.orgId);
|
|
24899
|
+
if (typeof beatVersionId === "string") {
|
|
24900
|
+
const bv = await client.getBeatVersion(beatVersionId);
|
|
24901
|
+
if (!bv) {
|
|
24902
|
+
return { content: [{ type: "text", text: `Beat Version not found: "${beatVersionId}"` }], isError: true };
|
|
24903
|
+
}
|
|
24904
|
+
if (bv.beatId !== revision.beatId || bv.projectId !== revision.projectId) {
|
|
24905
|
+
return { content: [{ type: "text", text: `Beat Version "${beatVersionId}" belongs to a different Beat.` }], isError: true };
|
|
24906
|
+
}
|
|
24907
|
+
}
|
|
24766
24908
|
const updates = { changeSummary };
|
|
24767
24909
|
for (const [k, v] of Object.entries(changes)) {
|
|
24768
24910
|
if (v !== void 0) updates[k] = v;
|
|
24769
24911
|
}
|
|
24912
|
+
if (beatVersionId !== void 0) updates.beatVersionId = beatVersionId;
|
|
24770
24913
|
if (Object.keys(updates).length <= 1) {
|
|
24771
24914
|
return { content: [{ type: "text", text: "No fields provided to update." }], isError: true };
|
|
24772
24915
|
}
|
|
@@ -24840,11 +24983,141 @@ function registerRevisionLifecycleTools(server, ctx, client) {
|
|
|
24840
24983
|
);
|
|
24841
24984
|
}
|
|
24842
24985
|
|
|
24986
|
+
// ../../libs/harmonica-services/src/mcp/tools/session-tools.ts
|
|
24987
|
+
var SESSION_STATUSES = ["running", "succeeded", "failed", "timed_out", "cancelled"];
|
|
24988
|
+
var SESSION_TYPES = ["planning", "implementation", "triage", "assessment", "composition", "baseline"];
|
|
24989
|
+
function registerSessionTools(server, ctx, client) {
|
|
24990
|
+
server.tool(
|
|
24991
|
+
"get_session",
|
|
24992
|
+
'Get full details of an agent Session by ID \u2014 type, status, actor, scoping links (beat/revision), metrics, structured result, and the recent tool/text events. Use this when the user asks "what did that session do?" or to inspect a specific agent run.',
|
|
24993
|
+
{
|
|
24994
|
+
sessionId: external_exports.string().min(1).describe("The Session ID (sess-<uuid>)")
|
|
24995
|
+
},
|
|
24996
|
+
async ({ sessionId }) => {
|
|
24997
|
+
const session = await client.getSession(sessionId);
|
|
24998
|
+
if (!session) {
|
|
24999
|
+
return {
|
|
25000
|
+
content: [{ type: "text", text: `Session not found: ${sessionId}` }],
|
|
25001
|
+
isError: true
|
|
25002
|
+
};
|
|
25003
|
+
}
|
|
25004
|
+
return { content: [{ type: "text", text: formatSessionDetail(session) }] };
|
|
25005
|
+
}
|
|
25006
|
+
);
|
|
25007
|
+
server.tool(
|
|
25008
|
+
"list_project_sessions",
|
|
25009
|
+
'List agent Sessions in a project, optionally filtered by status, type, or whether terminal sessions are included. Use this to answer "show me sessions in this project" or to find recent runs across all beats.',
|
|
25010
|
+
{
|
|
25011
|
+
projectId: external_exports.string().min(1).describe("The project ID"),
|
|
25012
|
+
status: external_exports.enum(SESSION_STATUSES).optional().describe("Filter by status: running, succeeded, failed, timed_out, cancelled"),
|
|
25013
|
+
sessionType: external_exports.enum(SESSION_TYPES).optional().describe("Filter by session type"),
|
|
25014
|
+
includeTerminal: external_exports.boolean().optional().describe("Include terminal sessions (succeeded/failed/timed_out/cancelled). Default false.")
|
|
25015
|
+
},
|
|
25016
|
+
async ({ projectId, status, sessionType, includeTerminal }) => {
|
|
25017
|
+
const sessions = await client.listProjectSessions(projectId, {
|
|
25018
|
+
status,
|
|
25019
|
+
sessionType,
|
|
25020
|
+
includeTerminal
|
|
25021
|
+
});
|
|
25022
|
+
return {
|
|
25023
|
+
content: [
|
|
25024
|
+
{ type: "text", text: formatSessionList(sessions, `project ${projectId}`) }
|
|
25025
|
+
]
|
|
25026
|
+
};
|
|
25027
|
+
}
|
|
25028
|
+
);
|
|
25029
|
+
server.tool(
|
|
25030
|
+
"list_beat_sessions",
|
|
25031
|
+
'List agent Sessions scoped to a Beat. Use this to see every agent run that touched a specific capability \u2014 useful for "what work has been done on this beat?" investigations.',
|
|
25032
|
+
{
|
|
25033
|
+
beatId: external_exports.string().min(1).describe("The Beat ID (e.g., PROJ-B-001)"),
|
|
25034
|
+
status: external_exports.enum(SESSION_STATUSES).optional().describe("Filter by status"),
|
|
25035
|
+
sessionType: external_exports.enum(SESSION_TYPES).optional().describe("Filter by session type"),
|
|
25036
|
+
includeTerminal: external_exports.boolean().optional().describe("Include terminal sessions. Default false.")
|
|
25037
|
+
},
|
|
25038
|
+
async ({ beatId, status, sessionType, includeTerminal }) => {
|
|
25039
|
+
const sessions = await client.listBeatSessions(beatId, {
|
|
25040
|
+
status,
|
|
25041
|
+
sessionType,
|
|
25042
|
+
includeTerminal
|
|
25043
|
+
});
|
|
25044
|
+
return {
|
|
25045
|
+
content: [
|
|
25046
|
+
{ type: "text", text: formatSessionList(sessions, `beat ${beatId}`) }
|
|
25047
|
+
]
|
|
25048
|
+
};
|
|
25049
|
+
}
|
|
25050
|
+
);
|
|
25051
|
+
server.tool(
|
|
25052
|
+
"list_revision_sessions",
|
|
25053
|
+
'List agent Sessions scoped to a specific Revision. Use this when a user asks "what did the agent do on this PR/revision?" \u2014 typically planning and implementation sessions.',
|
|
25054
|
+
{
|
|
25055
|
+
revisionId: external_exports.string().min(1).describe("The Revision ID (rev-<uuid>)"),
|
|
25056
|
+
status: external_exports.enum(SESSION_STATUSES).optional().describe("Filter by status"),
|
|
25057
|
+
sessionType: external_exports.enum(SESSION_TYPES).optional().describe("Filter by session type"),
|
|
25058
|
+
includeTerminal: external_exports.boolean().optional().describe("Include terminal sessions. Default false.")
|
|
25059
|
+
},
|
|
25060
|
+
async ({ revisionId, status, sessionType, includeTerminal }) => {
|
|
25061
|
+
const sessions = await client.listRevisionSessions(revisionId, {
|
|
25062
|
+
status,
|
|
25063
|
+
sessionType,
|
|
25064
|
+
includeTerminal
|
|
25065
|
+
});
|
|
25066
|
+
return {
|
|
25067
|
+
content: [
|
|
25068
|
+
{ type: "text", text: formatSessionList(sessions, `revision ${revisionId}`) }
|
|
25069
|
+
]
|
|
25070
|
+
};
|
|
25071
|
+
}
|
|
25072
|
+
);
|
|
25073
|
+
server.tool(
|
|
25074
|
+
"cancel_session",
|
|
25075
|
+
"Cancel a running agent Session. Idempotent \u2014 calling cancel on a session that is already terminal (succeeded, failed, timed_out, cancelled) returns the existing record unchanged. Use this to stop a runaway agent or free up budget.",
|
|
25076
|
+
{
|
|
25077
|
+
sessionId: external_exports.string().min(1).describe("The Session ID to cancel"),
|
|
25078
|
+
reason: external_exports.string().optional().describe("Why the session is being cancelled (audit trail)")
|
|
25079
|
+
},
|
|
25080
|
+
async ({ sessionId, reason }) => {
|
|
25081
|
+
try {
|
|
25082
|
+
const session = await client.cancelSession(sessionId, {
|
|
25083
|
+
actor: {
|
|
25084
|
+
type: "human",
|
|
25085
|
+
id: ctx.user.userId,
|
|
25086
|
+
...ctx.user.name && { name: ctx.user.name },
|
|
25087
|
+
...ctx.user.email && { email: ctx.user.email }
|
|
25088
|
+
},
|
|
25089
|
+
reason
|
|
25090
|
+
});
|
|
25091
|
+
if (!session) {
|
|
25092
|
+
return {
|
|
25093
|
+
content: [{ type: "text", text: `Session not found: ${sessionId}` }],
|
|
25094
|
+
isError: true
|
|
25095
|
+
};
|
|
25096
|
+
}
|
|
25097
|
+
return {
|
|
25098
|
+
content: [{ type: "text", text: formatSessionDetail(session) }]
|
|
25099
|
+
};
|
|
25100
|
+
} catch (err) {
|
|
25101
|
+
return {
|
|
25102
|
+
content: [
|
|
25103
|
+
{ type: "text", text: `Failed to cancel session: ${err instanceof Error ? err.message : String(err)}` }
|
|
25104
|
+
],
|
|
25105
|
+
isError: true
|
|
25106
|
+
};
|
|
25107
|
+
}
|
|
25108
|
+
}
|
|
25109
|
+
);
|
|
25110
|
+
}
|
|
25111
|
+
|
|
24843
25112
|
// ../../libs/harmonica-services/src/mcp/tools/snapshot-tools.ts
|
|
24844
25113
|
var import_promises = require("node:fs/promises");
|
|
24845
25114
|
var import_node_os = require("node:os");
|
|
24846
25115
|
var import_node_path = require("node:path");
|
|
24847
|
-
|
|
25116
|
+
|
|
25117
|
+
// ../../libs/harmonica-services/src/project-snapshot.constants.ts
|
|
25118
|
+
var SNAPSHOT_VERSION = 2;
|
|
25119
|
+
|
|
25120
|
+
// ../../libs/harmonica-services/src/mcp/tools/snapshot-tools.ts
|
|
24848
25121
|
function registerSnapshotTools(server, ctx, client) {
|
|
24849
25122
|
server.tool(
|
|
24850
25123
|
"export_project_snapshot",
|
|
@@ -24910,37 +25183,29 @@ function registerSnapshotTools(server, ctx, client) {
|
|
|
24910
25183
|
isError: true
|
|
24911
25184
|
};
|
|
24912
25185
|
}
|
|
25186
|
+
const trimmed = snapshotInput.trim();
|
|
25187
|
+
const importOptions = { targetOrgId: ctx.orgId, targetUserId: ctx.user.userId };
|
|
25188
|
+
if (trimmed.startsWith("https://")) {
|
|
25189
|
+
if (isPrivateHost(trimmed)) {
|
|
25190
|
+
throw new Error("Snapshot URL must point to a public host \u2014 private, link-local, and loopback addresses are not permitted.");
|
|
25191
|
+
}
|
|
25192
|
+
const result2 = await client.importProjectSnapshotFromUrl(trimmed, importOptions);
|
|
25193
|
+
return { content: [{ type: "text", text: formatImportSummary(result2) }] };
|
|
25194
|
+
}
|
|
25195
|
+
if (trimmed.startsWith("http://")) {
|
|
25196
|
+
throw new Error("Snapshot URL must use https:// \u2014 http:// is not permitted.");
|
|
25197
|
+
}
|
|
24913
25198
|
const snapshotJson = await resolveSnapshotInput(snapshotInput);
|
|
24914
25199
|
let parsed;
|
|
24915
25200
|
try {
|
|
24916
25201
|
parsed = JSON.parse(snapshotJson);
|
|
24917
25202
|
} catch {
|
|
24918
|
-
|
|
24919
|
-
throw new Error(`Snapshot is not valid JSON${urlHint}. The URL may have returned an error page instead of snapshot data.`);
|
|
25203
|
+
throw new Error("Snapshot is not valid JSON. The content may be corrupted.");
|
|
24920
25204
|
}
|
|
24921
|
-
assertSnapshotShape(parsed,
|
|
25205
|
+
assertSnapshotShape(parsed, "");
|
|
24922
25206
|
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") }] };
|
|
25207
|
+
const result = await client.importProjectSnapshot(normalized, importOptions);
|
|
25208
|
+
return { content: [{ type: "text", text: formatImportSummary(result) }] };
|
|
24944
25209
|
}
|
|
24945
25210
|
);
|
|
24946
25211
|
}
|
|
@@ -24964,32 +25229,44 @@ function normalizeSnapshot(raw) {
|
|
|
24964
25229
|
taskIndex: raw.taskIndex ?? [],
|
|
24965
25230
|
submissions: raw.submissions ?? [],
|
|
24966
25231
|
submissionIndex: raw.submissionIndex ?? [],
|
|
24967
|
-
promptLogs: raw.promptLogs ?? []
|
|
25232
|
+
promptLogs: raw.promptLogs ?? [],
|
|
25233
|
+
beatVersions: raw.beatVersions ?? [],
|
|
25234
|
+
beatBeatVersionIndex: raw.beatBeatVersionIndex ?? [],
|
|
25235
|
+
projectBeatVersionIndex: raw.projectBeatVersionIndex ?? [],
|
|
25236
|
+
beatVersionDropAssignments: raw.beatVersionDropAssignments ?? [],
|
|
25237
|
+
drops: raw.drops ?? [],
|
|
25238
|
+
teamspaceDropIndex: raw.teamspaceDropIndex ?? [],
|
|
25239
|
+
deliverables: raw.deliverables ?? [],
|
|
25240
|
+
teamspaceDeliverableIndex: raw.teamspaceDeliverableIndex ?? [],
|
|
25241
|
+
projectDeliverableIndex: raw.projectDeliverableIndex ?? []
|
|
24968
25242
|
};
|
|
24969
25243
|
}
|
|
24970
25244
|
async function resolveSnapshotInput(input) {
|
|
24971
25245
|
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
25246
|
if (trimmed.startsWith("/") || trimmed.startsWith("~")) {
|
|
24989
25247
|
return (0, import_promises.readFile)(trimmed, "utf-8");
|
|
24990
25248
|
}
|
|
24991
25249
|
return trimmed;
|
|
24992
25250
|
}
|
|
25251
|
+
function formatImportSummary(result) {
|
|
25252
|
+
const lines = [
|
|
25253
|
+
`Imported project: ${result.projectId}`,
|
|
25254
|
+
` Beats: ${result.counts.beats}`,
|
|
25255
|
+
` Proposals: ${result.counts.proposals}`,
|
|
25256
|
+
` Revisions: ${result.counts.revisions}`,
|
|
25257
|
+
` Activities: ${result.counts.activities}`,
|
|
25258
|
+
` Questions: ${result.counts.questions}`,
|
|
25259
|
+
` Notes: ${result.counts.notes}`,
|
|
25260
|
+
` Tasks: ${result.counts.tasks}`,
|
|
25261
|
+
` Submissions: ${result.counts.submissions}`,
|
|
25262
|
+
` PromptLogs: ${result.counts.promptLogs}`
|
|
25263
|
+
];
|
|
25264
|
+
if (result.errors.length > 0) {
|
|
25265
|
+
lines.push("", `Errors (${result.errors.length}):`);
|
|
25266
|
+
result.errors.forEach((e) => lines.push(` - ${e}`));
|
|
25267
|
+
}
|
|
25268
|
+
return lines.join("\n");
|
|
25269
|
+
}
|
|
24993
25270
|
function isPrivateHost(url) {
|
|
24994
25271
|
let hostname2;
|
|
24995
25272
|
try {
|
|
@@ -25650,6 +25927,7 @@ function registerAllTools(server, ctx, client) {
|
|
|
25650
25927
|
registerTeamspaceTools(server, ctx, client);
|
|
25651
25928
|
registerDeliverableTools(server, ctx, client);
|
|
25652
25929
|
registerDropTools(server, ctx, client);
|
|
25930
|
+
registerSessionTools(server, ctx, client);
|
|
25653
25931
|
registerOnboardingTools(server, ctx, client);
|
|
25654
25932
|
}
|
|
25655
25933
|
|
|
@@ -26185,6 +26463,40 @@ function createHttpClient(config2) {
|
|
|
26185
26463
|
}
|
|
26186
26464
|
return { success: true };
|
|
26187
26465
|
},
|
|
26466
|
+
// Sessions (B-150). All three list methods point at the same flat
|
|
26467
|
+
// `GET /api/sessions?<scope>=` endpoint — the DataClient interface
|
|
26468
|
+
// surfaces three methods because the data layer has three DDB index
|
|
26469
|
+
// partitions, but the REST surface is unified per the project-level
|
|
26470
|
+
// "avoid nested/duplicate routes" guidance.
|
|
26471
|
+
getSession: (sessionId) => request("GET", `/api/sessions/${encodeURIComponent(sessionId)}`),
|
|
26472
|
+
listProjectSessions: async (projectId, options) => {
|
|
26473
|
+
const params = new URLSearchParams();
|
|
26474
|
+
params.set("projectId", projectId);
|
|
26475
|
+
if (options?.status) params.set("status", options.status);
|
|
26476
|
+
if (options?.sessionType) params.set("sessionType", options.sessionType);
|
|
26477
|
+
if (options?.includeTerminal) params.set("includeTerminal", "true");
|
|
26478
|
+
const result = await request("GET", `/api/sessions?${params.toString()}`);
|
|
26479
|
+
return result ?? [];
|
|
26480
|
+
},
|
|
26481
|
+
listBeatSessions: async (beatId, options) => {
|
|
26482
|
+
const params = new URLSearchParams();
|
|
26483
|
+
params.set("beatId", beatId);
|
|
26484
|
+
if (options?.status) params.set("status", options.status);
|
|
26485
|
+
if (options?.sessionType) params.set("sessionType", options.sessionType);
|
|
26486
|
+
if (options?.includeTerminal) params.set("includeTerminal", "true");
|
|
26487
|
+
const result = await request("GET", `/api/sessions?${params.toString()}`);
|
|
26488
|
+
return result ?? [];
|
|
26489
|
+
},
|
|
26490
|
+
listRevisionSessions: async (revisionId, options) => {
|
|
26491
|
+
const params = new URLSearchParams();
|
|
26492
|
+
params.set("revisionId", revisionId);
|
|
26493
|
+
if (options?.status) params.set("status", options.status);
|
|
26494
|
+
if (options?.sessionType) params.set("sessionType", options.sessionType);
|
|
26495
|
+
if (options?.includeTerminal) params.set("includeTerminal", "true");
|
|
26496
|
+
const result = await request("GET", `/api/sessions?${params.toString()}`);
|
|
26497
|
+
return result ?? [];
|
|
26498
|
+
},
|
|
26499
|
+
cancelSession: (sessionId, options) => request("POST", `/api/sessions/${encodeURIComponent(sessionId)}/cancel`, { reason: options.reason }),
|
|
26188
26500
|
// Tasks
|
|
26189
26501
|
getTask: async (taskId) => {
|
|
26190
26502
|
return request("GET", `/api/tasks/${encodeURIComponent(taskId)}`);
|
|
@@ -26418,6 +26730,7 @@ function createHttpClient(config2) {
|
|
|
26418
26730
|
return download;
|
|
26419
26731
|
},
|
|
26420
26732
|
importProjectSnapshot: (snapshot, options) => request("POST", "/api/projects/import", { ...snapshot, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
|
|
26733
|
+
importProjectSnapshotFromUrl: (url2, options) => request("POST", "/api/projects/import", { snapshotUrl: url2, importOptions: options }, LONG_RUNNING_TIMEOUT_MS),
|
|
26421
26734
|
// Embedding similarity
|
|
26422
26735
|
embedProjectEntities: (projectId) => request("POST", `/api/projects/${encodeURIComponent(projectId)}/embeddings/generate`),
|
|
26423
26736
|
findSimilarNotes: async (noteId, projectId, options) => {
|
|
@@ -26725,7 +27038,7 @@ function loadConfig() {
|
|
|
26725
27038
|
};
|
|
26726
27039
|
}
|
|
26727
27040
|
async function main() {
|
|
26728
|
-
console.error(`[harmonica-mcp] v${"0.
|
|
27041
|
+
console.error(`[harmonica-mcp] v${"0.25.0"} starting\u2026`);
|
|
26729
27042
|
const config2 = loadConfig();
|
|
26730
27043
|
const client = createHttpClient({
|
|
26731
27044
|
apiBaseUrl: config2.apiBaseUrl,
|