@codazen/harmonica-mcp 0.24.0 → 0.25.1

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.
Files changed (2) hide show
  1. package/dist/index.js +298 -3
  2. 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(
@@ -24862,11 +24983,141 @@ function registerRevisionLifecycleTools(server, ctx, client) {
24862
24983
  );
24863
24984
  }
24864
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
+
24865
25112
  // ../../libs/harmonica-services/src/mcp/tools/snapshot-tools.ts
24866
25113
  var import_promises = require("node:fs/promises");
24867
25114
  var import_node_os = require("node:os");
24868
25115
  var import_node_path = require("node:path");
24869
- var SNAPSHOT_VERSION = 1;
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
24870
25121
  function registerSnapshotTools(server, ctx, client) {
24871
25122
  server.tool(
24872
25123
  "export_project_snapshot",
@@ -24978,7 +25229,16 @@ function normalizeSnapshot(raw) {
24978
25229
  taskIndex: raw.taskIndex ?? [],
24979
25230
  submissions: raw.submissions ?? [],
24980
25231
  submissionIndex: raw.submissionIndex ?? [],
24981
- 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 ?? []
24982
25242
  };
24983
25243
  }
24984
25244
  async function resolveSnapshotInput(input) {
@@ -25667,6 +25927,7 @@ function registerAllTools(server, ctx, client) {
25667
25927
  registerTeamspaceTools(server, ctx, client);
25668
25928
  registerDeliverableTools(server, ctx, client);
25669
25929
  registerDropTools(server, ctx, client);
25930
+ registerSessionTools(server, ctx, client);
25670
25931
  registerOnboardingTools(server, ctx, client);
25671
25932
  }
25672
25933
 
@@ -26202,6 +26463,40 @@ function createHttpClient(config2) {
26202
26463
  }
26203
26464
  return { success: true };
26204
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 }),
26205
26500
  // Tasks
26206
26501
  getTask: async (taskId) => {
26207
26502
  return request("GET", `/api/tasks/${encodeURIComponent(taskId)}`);
@@ -26743,7 +27038,7 @@ function loadConfig() {
26743
27038
  };
26744
27039
  }
26745
27040
  async function main() {
26746
- console.error(`[harmonica-mcp] v${"0.24.0"} starting\u2026`);
27041
+ console.error(`[harmonica-mcp] v${"0.25.1"} starting\u2026`);
26747
27042
  const config2 = loadConfig();
26748
27043
  const client = createHttpClient({
26749
27044
  apiBaseUrl: config2.apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "0.24.0",
3
+ "version": "0.25.1",
4
4
  "description": "MCP server for Harmonica — connect Claude to your Harmonica projects",
5
5
  "license": "MIT",
6
6
  "bin": {