@cfio/cohort-sync 0.34.1 → 0.34.2

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 CHANGED
@@ -14119,7 +14119,7 @@ function dumpEvent(event) {
14119
14119
  function positiveNumber(value) {
14120
14120
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
14121
14121
  }
14122
- var PLUGIN_VERSION = true ? "0.34.1" : "unknown";
14122
+ var PLUGIN_VERSION = true ? "0.34.2" : "unknown";
14123
14123
  function resolveGatewayToken(api) {
14124
14124
  const token2 = api.config?.gateway?.auth?.token;
14125
14125
  return typeof token2 === "string" ? token2 : null;
@@ -15212,9 +15212,87 @@ function getStorageIdFromUploadResponse(body) {
15212
15212
  function formatMetric(metric) {
15213
15213
  return `${metric.current}/${metric.target} ${metric.unit}`;
15214
15214
  }
15215
+ function isRecord2(value) {
15216
+ return typeof value === "object" && value !== null;
15217
+ }
15215
15218
  function redactSecrets(text) {
15216
15219
  return text.replace(/ch_(?:live|test)_[A-Za-z0-9_-]+/g, "[redacted]");
15217
15220
  }
15221
+ function isTaskRelationSummary(value) {
15222
+ if (!isRecord2(value) || !isRecord2(value.task)) return false;
15223
+ return typeof value.id === "string" && (value.type === "blocks" || value.type === "related" || value.type === "duplicate_of") && (value.direction === "incoming" || value.direction === "outgoing") && (value.task.taskNumber === void 0 || typeof value.task.taskNumber === "number") && (value.task.title === void 0 || typeof value.task.title === "string") && typeof value.task.status === "string" && (typeof value.task.assignedTo === "string" || value.task.assignedTo === null);
15224
+ }
15225
+ function relationTaskLabel(relation) {
15226
+ const id = relation.task.taskNumber !== void 0 ? `#${relation.task.taskNumber}` : relation.task.title ?? "untitled task";
15227
+ return `${id} (${relation.task.status}, ${relation.task.assignedTo ?? "unassigned"})`;
15228
+ }
15229
+ function formatTaskRelationsSummary(relations) {
15230
+ if (!Array.isArray(relations)) return [];
15231
+ const rows = relations.filter(isTaskRelationSummary);
15232
+ if (rows.length === 0) return [];
15233
+ const groups = [
15234
+ { label: "Blocked by", rows: rows.filter((r) => r.type === "blocks" && r.direction === "incoming") },
15235
+ { label: "Blocks", rows: rows.filter((r) => r.type === "blocks" && r.direction === "outgoing") },
15236
+ { label: "Related", rows: rows.filter((r) => r.type === "related") },
15237
+ { label: "Duplicate of", rows: rows.filter((r) => r.type === "duplicate_of" && r.direction === "outgoing") },
15238
+ { label: "Duplicated by", rows: rows.filter((r) => r.type === "duplicate_of" && r.direction === "incoming") }
15239
+ ];
15240
+ return groups.filter((group) => group.rows.length > 0).map((group) => `${group.label}: ${group.rows.map(relationTaskLabel).join(", ")}`);
15241
+ }
15242
+ function blockerLine(blocker) {
15243
+ const id = blocker.taskNumber !== void 0 ? `#${blocker.taskNumber}` : "Unnumbered task";
15244
+ return `${id} ${blocker.title} (${blocker.status}, ${blocker.assignedTo ?? "unassigned"})`;
15245
+ }
15246
+ function getTaskBlockedErrorData(error) {
15247
+ if (!isRecord2(error) || !isRecord2(error.data)) return null;
15248
+ const data = error.data;
15249
+ if (data.code !== "CONFLICT" || data.subcode !== "TASK_BLOCKED" || typeof data.message !== "string") {
15250
+ return null;
15251
+ }
15252
+ const blockers = Array.isArray(data.blockers) ? data.blockers.flatMap((blocker) => {
15253
+ if (!isRecord2(blocker) || typeof blocker.title !== "string" || typeof blocker.status !== "string") {
15254
+ return [];
15255
+ }
15256
+ const assignedTo = typeof blocker.assignedTo === "string" ? blocker.assignedTo : null;
15257
+ return [{
15258
+ ...typeof blocker.taskNumber === "number" ? { taskNumber: blocker.taskNumber } : {},
15259
+ title: blocker.title,
15260
+ status: blocker.status,
15261
+ assignedTo
15262
+ }];
15263
+ }) : void 0;
15264
+ return {
15265
+ code: "CONFLICT",
15266
+ subcode: "TASK_BLOCKED",
15267
+ message: data.message,
15268
+ ...blockers ? { blockers } : {}
15269
+ };
15270
+ }
15271
+ function formatTaskBlockedError(error) {
15272
+ const data = getTaskBlockedErrorData(error);
15273
+ if (!data) return null;
15274
+ const lines = [data.message];
15275
+ if (data.blockers && data.blockers.length > 0) {
15276
+ lines.push("", "Open blockers:");
15277
+ for (const blocker of data.blockers) {
15278
+ lines.push(`- ${blockerLine(blocker)}`);
15279
+ }
15280
+ }
15281
+ return lines.join("\n");
15282
+ }
15283
+ function relationMatchesVerb(relation, relationVerb, otherTaskNumber) {
15284
+ if (relation.task.taskNumber !== otherTaskNumber) return false;
15285
+ switch (relationVerb) {
15286
+ case "blocked_by":
15287
+ return relation.type === "blocks" && relation.direction === "incoming";
15288
+ case "blocks":
15289
+ return relation.type === "blocks" && relation.direction === "outgoing";
15290
+ case "related":
15291
+ return relation.type === "related";
15292
+ case "duplicate_of":
15293
+ return relation.type === "duplicate_of" && relation.direction === "outgoing";
15294
+ }
15295
+ }
15218
15296
  async function safeHttpError(response) {
15219
15297
  try {
15220
15298
  const body = await response.text();
@@ -15842,10 +15920,17 @@ ${body}`,
15842
15920
  `**Assigned to:** ${task.assignedTo ?? "unassigned"}`,
15843
15921
  `**Created:** ${task.createdAt}`
15844
15922
  ];
15923
+ if (task.blocked === true) {
15924
+ lines.push("**Blocked:** yes");
15925
+ }
15845
15926
  const contextBlock = renderTaskContext(task.context);
15846
15927
  if (contextBlock) {
15847
15928
  lines.push("", contextBlock);
15848
15929
  }
15930
+ const relationsSummary = formatTaskRelationsSummary(task.relations);
15931
+ if (relationsSummary.length > 0) {
15932
+ lines.push("", "## Relations", ...relationsSummary);
15933
+ }
15849
15934
  lines.push("", "## Description", task.description || "(no description)");
15850
15935
  if (params.include_comments !== false) {
15851
15936
  const limit = params.comment_limit ?? 10;
@@ -15961,7 +16046,92 @@ ${renderGoal(goal)}`, goal);
15961
16046
  });
15962
16047
  return textResult(`Task #${params.task_number} transitioned to "${params.status}".`);
15963
16048
  } catch (err) {
15964
- return textResult(`Failed to transition task #${params.task_number}: ${err instanceof Error ? err.message : String(err)}`);
16049
+ const blockedMessage = formatTaskBlockedError(err);
16050
+ if (blockedMessage) {
16051
+ return textResult(blockedMessage, getTaskBlockedErrorData(err));
16052
+ }
16053
+ const appMessage = getConvexAppErrorMessage(err);
16054
+ return textResult(`Failed to transition task #${params.task_number}: ${appMessage ?? (err instanceof Error ? err.message : String(err))}`);
16055
+ }
16056
+ }
16057
+ };
16058
+ });
16059
+ api.registerTool(() => {
16060
+ return {
16061
+ name: "cohort_relate",
16062
+ label: "cohort_relate",
16063
+ description: "Create or remove Cohort task relations by task number. This is THE way to decompose work into ordered blockers: create blocker tasks, then relate them with blocked_by or blocks so Cohort can sequence handoffs.",
16064
+ parameters: Type.Object({
16065
+ task_number: Type.Number({ description: "Primary task number (e.g. 214)." }),
16066
+ relation: Type.Union([
16067
+ Type.Literal("blocked_by"),
16068
+ Type.Literal("blocks"),
16069
+ Type.Literal("related"),
16070
+ Type.Literal("duplicate_of")
16071
+ ], { description: "Relation from task_number to other_task_number." }),
16072
+ other_task_number: Type.Number({ description: "Other task number to relate to the primary task." }),
16073
+ remove: Type.Optional(Type.Boolean({ description: "Set true to remove the matching relation instead of creating it." }))
16074
+ }),
16075
+ async execute(_toolCallId, params) {
16076
+ const rt = getToolRuntime();
16077
+ if (!rt.isReady) {
16078
+ return textResult("cohort_relate is not ready yet \u2014 the plugin is still starting up.");
16079
+ }
16080
+ if (params.task_number === params.other_task_number) {
16081
+ return textResult("Cannot relate a task to itself.");
16082
+ }
16083
+ try {
16084
+ if (!params.remove) {
16085
+ const response = await fetch(`${rt.apiUrl}/api/v1/tasks/${params.task_number}/relations`, {
16086
+ method: "POST",
16087
+ headers: {
16088
+ "Authorization": `Bearer ${rt.apiKey}`,
16089
+ "Content-Type": "application/json"
16090
+ },
16091
+ body: JSON.stringify({
16092
+ type: params.relation,
16093
+ taskNumber: params.other_task_number
16094
+ }),
16095
+ signal: AbortSignal.timeout(1e4)
16096
+ });
16097
+ if (!response.ok) {
16098
+ const message = await safeHttpError(response);
16099
+ return textResult(`Failed to relate task #${params.task_number}: ${response.status}${message}`);
16100
+ }
16101
+ const relation2 = await response.json();
16102
+ return textResult(`Related task #${params.task_number} ${params.relation} #${params.other_task_number}.`, relation2);
16103
+ }
16104
+ const taskResponse = await fetch(`${rt.apiUrl}/api/v1/tasks/${params.task_number}`, {
16105
+ headers: { "Authorization": `Bearer ${rt.apiKey}` },
16106
+ signal: AbortSignal.timeout(1e4)
16107
+ });
16108
+ if (!taskResponse.ok) {
16109
+ const message = await safeHttpError(taskResponse);
16110
+ return textResult(`Failed to inspect task #${params.task_number}: ${taskResponse.status}${message}`);
16111
+ }
16112
+ const task = await taskResponse.json();
16113
+ const relations = Array.isArray(task.relations) ? task.relations.filter(isTaskRelationSummary) : [];
16114
+ const relation = relations.find(
16115
+ (candidate) => relationMatchesVerb(candidate, params.relation, params.other_task_number)
16116
+ );
16117
+ if (!relation) {
16118
+ return textResult(`No ${params.relation} relation found between task #${params.task_number} and #${params.other_task_number}.`);
16119
+ }
16120
+ const deleteResponse = await fetch(
16121
+ `${rt.apiUrl}/api/v1/tasks/${params.task_number}/relations/${encodeURIComponent(relation.id)}`,
16122
+ {
16123
+ method: "DELETE",
16124
+ headers: { "Authorization": `Bearer ${rt.apiKey}` },
16125
+ signal: AbortSignal.timeout(1e4)
16126
+ }
16127
+ );
16128
+ if (!deleteResponse.ok) {
16129
+ const message = await safeHttpError(deleteResponse);
16130
+ return textResult(`Failed to remove relation from task #${params.task_number}: ${deleteResponse.status}${message}`);
16131
+ }
16132
+ return textResult(`Removed ${params.relation} relation between task #${params.task_number} and #${params.other_task_number}.`);
16133
+ } catch (err) {
16134
+ return textResult(`Failed to relate task #${params.task_number}: ${err instanceof Error ? redactSecrets(err.message) : "Unknown error"}`);
15965
16135
  }
15966
16136
  }
15967
16137
  };
@@ -82,5 +82,5 @@
82
82
  }
83
83
  }
84
84
  },
85
- "version": "0.34.1"
85
+ "version": "0.34.2"
86
86
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.34.1",
3
+ "version": "0.34.2",
4
4
  "description": "OpenClaw plugin — syncs agent telemetry, sessions, and activity to the Cohort dashboard",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.34.1",
3
+ "version": "0.34.2",
4
4
  "description": "OpenClaw plugin — syncs agent telemetry, sessions, and activity to the Cohort dashboard",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.cohort.bot/gateway",