@cfio/cohort-sync 0.34.0 → 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
@@ -12183,7 +12183,9 @@ async function callPublishWireFromPlugin(apiKey2, args) {
12183
12183
  ...args.filename !== void 0 ? { filename: args.filename } : {},
12184
12184
  ...args.altText !== void 0 ? { altText: args.altText } : {},
12185
12185
  ...args.roomId !== void 0 ? { roomId: args.roomId } : {},
12186
- ...args.taskId !== void 0 ? { taskId: args.taskId } : {}
12186
+ ...args.taskId !== void 0 ? { taskId: args.taskId } : {},
12187
+ ...args.inResponseToSteerId !== void 0 ? { inResponseToSteerId: args.inResponseToSteerId } : {},
12188
+ ...args.deliveryNote !== void 0 ? { deliveryNote: args.deliveryNote } : {}
12187
12189
  });
12188
12190
  } catch (err) {
12189
12191
  if (isUnauthorizedError(err)) {
@@ -14117,7 +14119,7 @@ function dumpEvent(event) {
14117
14119
  function positiveNumber(value) {
14118
14120
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
14119
14121
  }
14120
- var PLUGIN_VERSION = true ? "0.34.0" : "unknown";
14122
+ var PLUGIN_VERSION = true ? "0.34.2" : "unknown";
14121
14123
  function resolveGatewayToken(api) {
14122
14124
  const token2 = api.config?.gateway?.auth?.token;
14123
14125
  return typeof token2 === "string" ? token2 : null;
@@ -15144,6 +15146,7 @@ var WIRE_FILENAME_MAX = 200;
15144
15146
  var WIRE_MIME_TYPE_MAX = 100;
15145
15147
  var WIRE_ALT_TEXT_MAX = 1e3;
15146
15148
  var WIRE_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
15149
+ var WIRE_DELIVERY_NOTE_MAX = 1e3;
15147
15150
  function wireInvalid(message) {
15148
15151
  return textResult(`Cannot publish to Wire: ${message}.`);
15149
15152
  }
@@ -15209,9 +15212,87 @@ function getStorageIdFromUploadResponse(body) {
15209
15212
  function formatMetric(metric) {
15210
15213
  return `${metric.current}/${metric.target} ${metric.unit}`;
15211
15214
  }
15215
+ function isRecord2(value) {
15216
+ return typeof value === "object" && value !== null;
15217
+ }
15212
15218
  function redactSecrets(text) {
15213
15219
  return text.replace(/ch_(?:live|test)_[A-Za-z0-9_-]+/g, "[redacted]");
15214
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
+ }
15215
15296
  async function safeHttpError(response) {
15216
15297
  try {
15217
15298
  const body = await response.text();
@@ -15486,7 +15567,9 @@ Do not attempt more comments until tomorrow.`);
15486
15567
  filename: Type.Optional(Type.String({ description: `Original image filename, max ${WIRE_FILENAME_MAX} characters. Used with kind=image.` })),
15487
15568
  alt_text: Type.Optional(Type.String({ description: `Accessible image description, max ${WIRE_ALT_TEXT_MAX} characters. Used with kind=image.` })),
15488
15569
  room_id: Type.Optional(Type.String({ description: "Optional Cohort Room ID to link this Wire item to." })),
15489
- task_id: Type.Optional(Type.String({ description: "Optional Cohort task ID to link this Wire item to." }))
15570
+ task_id: Type.Optional(Type.String({ description: "Optional Cohort task ID to link this Wire item to." })),
15571
+ in_response_to_steer_id: Type.Optional(Type.String({ description: "Wire request ID when delivering a requested revision or follow-up." })),
15572
+ delivery_note: Type.Optional(Type.String({ description: `Short note for the requester, max ${WIRE_DELIVERY_NOTE_MAX} characters.` }))
15490
15573
  }),
15491
15574
  async execute(_toolCallId, params) {
15492
15575
  const rt = getToolRuntime();
@@ -15501,6 +15584,10 @@ Do not attempt more comments until tomorrow.`);
15501
15584
  if (!roomId.ok) return wireInvalid(roomId.message);
15502
15585
  const taskId = optionalWireString(params.task_id, "task_id", WIRE_TITLE_MAX);
15503
15586
  if (!taskId.ok) return wireInvalid(taskId.message);
15587
+ const inResponseToSteerId = optionalWireString(params.in_response_to_steer_id, "in_response_to_steer_id", WIRE_TITLE_MAX);
15588
+ if (!inResponseToSteerId.ok) return wireInvalid(inResponseToSteerId.message);
15589
+ const deliveryNote = optionalWireString(params.delivery_note, "delivery_note", WIRE_DELIVERY_NOTE_MAX);
15590
+ if (!deliveryNote.ok) return wireInvalid(deliveryNote.message);
15504
15591
  const agentName = rt.resolveAgentName(agentId);
15505
15592
  if (params.kind === "report") {
15506
15593
  const body = optionalWireString(params.body, "body", WIRE_BODY_MAX);
@@ -15513,7 +15600,9 @@ Do not attempt more comments until tomorrow.`);
15513
15600
  brief: brief.value,
15514
15601
  ...body.value !== void 0 ? { body: body.value } : {},
15515
15602
  ...roomId.value !== void 0 ? { roomId: roomId.value } : {},
15516
- ...taskId.value !== void 0 ? { taskId: taskId.value } : {}
15603
+ ...taskId.value !== void 0 ? { taskId: taskId.value } : {},
15604
+ ...inResponseToSteerId.value !== void 0 ? { inResponseToSteerId: inResponseToSteerId.value } : {},
15605
+ ...deliveryNote.value !== void 0 ? { deliveryNote: deliveryNote.value } : {}
15517
15606
  });
15518
15607
  return textResult(`Published report to Wire.
15519
15608
  Wire item: ${result.wireItemId}`, result);
@@ -15562,7 +15651,9 @@ Wire item: ${result.wireItemId}`, result);
15562
15651
  filename: filename.value,
15563
15652
  altText: altText.value,
15564
15653
  ...roomId.value !== void 0 ? { roomId: roomId.value } : {},
15565
- ...taskId.value !== void 0 ? { taskId: taskId.value } : {}
15654
+ ...taskId.value !== void 0 ? { taskId: taskId.value } : {},
15655
+ ...inResponseToSteerId.value !== void 0 ? { inResponseToSteerId: inResponseToSteerId.value } : {},
15656
+ ...deliveryNote.value !== void 0 ? { deliveryNote: deliveryNote.value } : {}
15566
15657
  });
15567
15658
  return textResult(`Published image to Wire.
15568
15659
  Wire item: ${result.wireItemId}`, result);
@@ -15829,10 +15920,17 @@ ${body}`,
15829
15920
  `**Assigned to:** ${task.assignedTo ?? "unassigned"}`,
15830
15921
  `**Created:** ${task.createdAt}`
15831
15922
  ];
15923
+ if (task.blocked === true) {
15924
+ lines.push("**Blocked:** yes");
15925
+ }
15832
15926
  const contextBlock = renderTaskContext(task.context);
15833
15927
  if (contextBlock) {
15834
15928
  lines.push("", contextBlock);
15835
15929
  }
15930
+ const relationsSummary = formatTaskRelationsSummary(task.relations);
15931
+ if (relationsSummary.length > 0) {
15932
+ lines.push("", "## Relations", ...relationsSummary);
15933
+ }
15836
15934
  lines.push("", "## Description", task.description || "(no description)");
15837
15935
  if (params.include_comments !== false) {
15838
15936
  const limit = params.comment_limit ?? 10;
@@ -15948,7 +16046,92 @@ ${renderGoal(goal)}`, goal);
15948
16046
  });
15949
16047
  return textResult(`Task #${params.task_number} transitioned to "${params.status}".`);
15950
16048
  } catch (err) {
15951
- 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"}`);
15952
16135
  }
15953
16136
  }
15954
16137
  };
@@ -82,5 +82,5 @@
82
82
  }
83
83
  }
84
84
  },
85
- "version": "0.34.0"
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.0",
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.0",
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",