@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.
Files changed (2) hide show
  1. package/dist/index.js +358 -45
  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(
@@ -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
- 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
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
- const urlHint = snapshotInput.trim().startsWith("https://") ? ` fetched from ${snapshotInput.trim().split("?")[0]}` : "";
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, snapshotInput.trim().startsWith("https://") ? ` fetched from ${snapshotInput.trim().split("?")[0]}` : "");
25205
+ assertSnapshotShape(parsed, "");
24922
25206
  const normalized = normalizeSnapshot(parsed);
24923
- const result = await client.importProjectSnapshot(normalized, {
24924
- targetOrgId: ctx.orgId,
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.23.0"} starting\u2026`);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codazen/harmonica-mcp",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "MCP server for Harmonica — connect Claude to your Harmonica projects",
5
5
  "license": "MIT",
6
6
  "bin": {