@cfio/cohort-sync 0.32.0 → 0.33.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 CHANGED
@@ -2615,6 +2615,9 @@ __export(type_exports2, {
2615
2615
  // ../../node_modules/.pnpm/@sinclair+typebox@0.34.48/node_modules/@sinclair/typebox/build/esm/type/type/index.mjs
2616
2616
  var Type = type_exports2;
2617
2617
 
2618
+ // index.ts
2619
+ import { Buffer as Buffer2 } from "node:buffer";
2620
+
2618
2621
  // src/hooks.ts
2619
2622
  import fs2 from "node:fs";
2620
2623
  import os2 from "node:os";
@@ -12000,6 +12003,8 @@ var failCommandRef = makeFunctionReference("gatewayCommands:failCommand");
12000
12003
  var getChannelsForPlugin = makeFunctionReference("cloudGatewayChannels:listForPlugin");
12001
12004
  var addCommentFromPluginRef = makeFunctionReference("comments:addCommentFromPlugin");
12002
12005
  var addRoomMessageFromPluginRef = makeFunctionReference("rooms:addMessageFromPlugin");
12006
+ var generateWireUploadUrlFromPluginRef = makeFunctionReference("wire:generateUploadUrl");
12007
+ var publishWireFromPluginRef = makeFunctionReference("wire:publish");
12003
12008
  var startModerationSessionFromPluginRef = makeFunctionReference("moderationSessions:startFromPlugin");
12004
12009
  var advanceModerationSessionFromPluginRef = makeFunctionReference("moderationSessions:advanceFromPlugin");
12005
12010
  var cancelModerationSessionFromPluginRef = makeFunctionReference("moderationSessions:cancelFromPlugin");
@@ -12133,6 +12138,60 @@ async function callAddRoomMessageFromPlugin(apiKey2, args) {
12133
12138
  throw err;
12134
12139
  }
12135
12140
  }
12141
+ async function callGenerateWireUploadUrlFromPlugin(apiKey2, args) {
12142
+ if (authCircuitOpen) {
12143
+ throw new Error(
12144
+ 'cohort-sync: API key rejected \u2014 all outbound mutations disabled until gateway restart.\n 1. Create a new key at https://my.cohort.bot/settings/api-keys\n 2. Run: openclaw config set plugins.entries.cohort-sync.config.apiKey "ch_live_..."'
12145
+ );
12146
+ }
12147
+ const c = getClient();
12148
+ if (!c) {
12149
+ throw new Error("Convex client not initialized \u2014 subscription may not be active");
12150
+ }
12151
+ try {
12152
+ return await c.mutation(generateWireUploadUrlFromPluginRef, {
12153
+ apiKeyHash: hashApiKey(apiKey2),
12154
+ agentName: args.agentName
12155
+ });
12156
+ } catch (err) {
12157
+ if (isUnauthorizedError(err)) {
12158
+ tripAuthCircuit();
12159
+ }
12160
+ throw err;
12161
+ }
12162
+ }
12163
+ async function callPublishWireFromPlugin(apiKey2, args) {
12164
+ if (authCircuitOpen) {
12165
+ throw new Error(
12166
+ 'cohort-sync: API key rejected \u2014 all outbound mutations disabled until gateway restart.\n 1. Create a new key at https://my.cohort.bot/settings/api-keys\n 2. Run: openclaw config set plugins.entries.cohort-sync.config.apiKey "ch_live_..."'
12167
+ );
12168
+ }
12169
+ const c = getClient();
12170
+ if (!c) {
12171
+ throw new Error("Convex client not initialized \u2014 subscription may not be active");
12172
+ }
12173
+ try {
12174
+ return await c.mutation(publishWireFromPluginRef, {
12175
+ apiKeyHash: hashApiKey(apiKey2),
12176
+ agentName: args.agentName,
12177
+ kind: args.kind,
12178
+ title: args.title,
12179
+ brief: args.brief,
12180
+ ...args.body !== void 0 ? { body: args.body } : {},
12181
+ ...args.storageId !== void 0 ? { storageId: args.storageId } : {},
12182
+ ...args.mimeType !== void 0 ? { mimeType: args.mimeType } : {},
12183
+ ...args.filename !== void 0 ? { filename: args.filename } : {},
12184
+ ...args.altText !== void 0 ? { altText: args.altText } : {},
12185
+ ...args.roomId !== void 0 ? { roomId: args.roomId } : {},
12186
+ ...args.taskId !== void 0 ? { taskId: args.taskId } : {}
12187
+ });
12188
+ } catch (err) {
12189
+ if (isUnauthorizedError(err)) {
12190
+ tripAuthCircuit();
12191
+ }
12192
+ throw err;
12193
+ }
12194
+ }
12136
12195
  async function callStartModerationSessionFromPlugin(apiKey2, args) {
12137
12196
  if (authCircuitOpen) {
12138
12197
  throw new Error(
@@ -12279,21 +12338,24 @@ var ATMENTION_RESPONSE_PROMPT = `YOU WERE DIRECTLY @-MENTIONED. RESPOND.
12279
12338
  - Do not stop because Vercel CLI is not authenticated; Cohort deploys through GitHub Actions after merge.`;
12280
12339
  var ROOM_MESSAGE_RESPONSE_PROMPT = `YOU WERE DIRECTLY ASKED IN A COHORT ROOM. RESPOND.
12281
12340
 
12282
- - If you are the Room moderator and a human asks for a roundtable, standup, report-in, or panel response, FIRST use cohort_room_start_moderation_session(room_id, mode, objective, participant_names) to run it as a managed session, then drive it with cohort_room_advance_moderation_session; do not only name agents in prose.
12341
+ - If you are the Room moderator and a human asks for a question to be routed, FIRST use cohort_room_start_moderation_session with mode: "moderated_qna", one best participant, and question_text; use cohort_room_advance_moderation_session(action: "followup") for additional experts one at a time, then confirm with the asker before complete.
12342
+ - If you are the Room moderator and a human asks for a roundtable, standup, report-in, or panel response, FIRST use cohort_room_start_moderation_session with mode: "round_robin" to run it as a managed session, then drive it with cohort_room_advance_moderation_session; do not only name agents in prose.
12283
12343
  - Otherwise, use the cohort_room_message tool to post a reply in the Room. Do NOT just think silently and exit.
12284
12344
  - Reply in your own voice (see your persona in IDENTITY.md).
12285
- - If the human asks for a roundtable, standup, report-in, or panel response, contribute your own concise update.
12345
+ - If the human asks for an answer, answer it. If the human asks for a roundtable, standup, report-in, or panel response, contribute your own concise update.
12286
12346
  - If the moderator asks you to answer, answer. If another agent asks for your report, provide it. If your prompt includes a turn_id, echo it in cohort_room_message.
12287
12347
  - A brief, honest reply is better than no reply. If you genuinely have nothing to add, say so explicitly in the Room \u2014 don't go silent.`;
12288
12348
  var TOOLS_REFERENCE = `
12289
12349
  TOOLS: Use these \u2014 do NOT call the REST API directly.
12290
12350
  - cohort_comment(task_number, comment) \u2014 post a comment
12291
12351
  - cohort_room_message(room_id, message, turn_id?) \u2014 post a message in a Cohort Room; pass turn_id when your prompt included one so the reply is attributed to that moderation turn
12292
- - cohort_room_start_moderation_session(room_id, mode, objective, participant_names) \u2014 moderator only: start a managed round robin or Q&A session
12293
- - cohort_room_advance_moderation_session(session_id, action, ...) \u2014 moderator only: advance, retry, skip, accept, followup, or complete the session
12352
+ - cohort_room_start_moderation_session(room_id, mode, objective, participant_names) \u2014 moderator only: start a managed session; round_robin = ask every participant to report in, moderated_qna = route a question to one selected agent
12353
+ - cohort_room_advance_moderation_session(session_id, action, ...) \u2014 moderator only: advance, retry, skip, accept, followup, or complete the session; followup asks one additional Q&A expert after the first answer
12294
12354
  - cohort_room_cancel_moderation_session(session_id, reason?) \u2014 moderator only: cancel an in-flight session
12295
12355
  - cohort_room_prompt_agent(roomId, agentName, prompt) \u2014 moderator only, low-level: ask one Room agent to respond (prefer moderation sessions for multi-step workflows)
12296
12356
  - cohort_room_prompt_agents(roomId, agentNames, prompt) \u2014 moderator only, low-level: ask multiple Room agents to respond (prefer moderation sessions for multi-step workflows)
12357
+ - wire_publish(kind, title, brief, body?, image_base64?, mime_type?, filename?, alt_text?, room_id?, task_id?, in_response_to_steer_id?, delivery_note?) - publish a report or image to Cohort Wire; include in_response_to_steer_id when a WIRE REQUEST block asks for it
12358
+ - wire_decline(steer_id, reason) - decline a Wire request you cannot complete
12297
12359
  - cohort_task(task_number) \u2014 fetch full task details + comments
12298
12360
  - cohort_transition(task_number, status) \u2014 change status
12299
12361
  - cohort_assign(task_number, assignee) \u2014 assign/unassign
@@ -12322,6 +12384,16 @@ var HANDOFF_ACTION_TERMS = [
12322
12384
  "gather",
12323
12385
  "ask"
12324
12386
  ];
12387
+ var QUESTION_ROUTING_TERMS = [
12388
+ "question for the team",
12389
+ "route it",
12390
+ "route this",
12391
+ "ask whoever",
12392
+ "whoever's best suited",
12393
+ "whoever is best suited",
12394
+ "who's best suited",
12395
+ "who is best suited"
12396
+ ];
12325
12397
  function sanitizePreview(raw) {
12326
12398
  return raw.replace(/<\/?user_comment>/gi, "");
12327
12399
  }
@@ -12337,11 +12409,24 @@ function isHumanHandoffRequest(n) {
12337
12409
  const hasAction = HANDOFF_ACTION_TERMS.some((term) => preview.includes(term));
12338
12410
  return hasTopic && hasAction;
12339
12411
  }
12412
+ function isHumanQuestionRoutingRequest(n) {
12413
+ if (n.actorType !== "human") {
12414
+ return false;
12415
+ }
12416
+ const preview = (n.preview ?? "").toLowerCase();
12417
+ return QUESTION_ROUTING_TERMS.some((term) => preview.includes(term)) || /\bhave\s+[a-z][a-z0-9_-]*\s+answer\b/.test(preview);
12418
+ }
12340
12419
  function roomMessageCta(n) {
12341
12420
  const roomId = n.roomId ?? "unknown";
12421
+ if (n.wireSteerContext != null && renderWireSteerContext(n.wireSteerContext) !== "") {
12422
+ return "Follow the WIRE REQUEST block in this message \u2014 it is authoritative.";
12423
+ }
12342
12424
  if (n.moderationContext != null && renderModerationContext(n.moderationContext) !== "") {
12343
12425
  return "Follow the MODERATION SESSION block in this message \u2014 it is authoritative.";
12344
12426
  }
12427
+ if (isHumanQuestionRoutingRequest(n)) {
12428
+ return `You are moderating a Room Q&A. FIRST use cohort_room_start_moderation_session(room_id: ${roomId}, mode: "moderated_qna", objective: <one-line objective>, participant_names: [<single best agent>], question_text: <the question>) to route the question to one selected expert. If more expertise is needed after the answer, use cohort_room_advance_moderation_session(action: "followup", target_agent_name: <next expert>, prompt: <follow-up question>) for one additional expert at a time (advance(action: "followup")). Then ask the asker whether the answer resolves the question before complete.`;
12429
+ }
12345
12430
  if (isHumanHandoffRequest(n)) {
12346
12431
  return `You are moderating a Room round robin. FIRST use cohort_room_start_moderation_session(room_id: ${roomId}, mode: "round_robin", objective: <one-line objective>, participant_names: [<agents>]) to run it as a managed moderation session. Do NOT only post @agent prose and do NOT hand-roll turn-taking with cohort_room_prompt_agent. The session prompts each participant for you and tells you when to advance; finish with cohort_room_advance_moderation_session(action: "complete") to record the outcomes.`;
12347
12432
  }
@@ -12384,6 +12469,47 @@ function renderModerationContext(ctx) {
12384
12469
  lines.push("[/MODERATION SESSION]");
12385
12470
  return lines.join("\n");
12386
12471
  }
12472
+ function renderWireSteerContext(ctx) {
12473
+ if (ctx.version !== 1) return "";
12474
+ const artifact = ctx.artifact;
12475
+ const lines = [
12476
+ `[WIRE REQUEST ${ctx.steerId}]`,
12477
+ `Mode: ${ctx.mode} | Contract: ${ctx.contract} | Source: ${artifact.type} ${ctx.wireItemId}`,
12478
+ `Title: ${artifact.title}`,
12479
+ `Brief: ${artifact.brief}`
12480
+ ];
12481
+ if (artifact.body) {
12482
+ lines.push("Artifact body:", artifact.body);
12483
+ }
12484
+ if (artifact.imageUrl) {
12485
+ lines.push(`Artifact URL: ${artifact.imageUrl}`);
12486
+ }
12487
+ lines.push(`Instruction: ${ctx.instruction}`);
12488
+ if (ctx.comments.length > 0) {
12489
+ lines.push(
12490
+ "Recent thread:",
12491
+ ...ctx.comments.map(
12492
+ (comment) => `- ${comment.authorName} (${comment.authorType}): ${comment.body}`
12493
+ )
12494
+ );
12495
+ } else {
12496
+ lines.push("Recent thread: (none)");
12497
+ }
12498
+ const provenance = [
12499
+ ctx.provenance.roomId ? `roomId=${ctx.provenance.roomId}` : "",
12500
+ ctx.provenance.taskId ? `taskId=${ctx.provenance.taskId}` : "",
12501
+ ctx.provenance.sessionId ? `sessionId=${ctx.provenance.sessionId}` : "",
12502
+ ctx.provenance.revisesItemId ? `revisesItemId=${ctx.provenance.revisesItemId}` : "",
12503
+ ctx.provenance.derivedFromItemId ? `derivedFromItemId=${ctx.provenance.derivedFromItemId}` : ""
12504
+ ].filter(Boolean);
12505
+ lines.push(`Provenance: ${provenance.length > 0 ? provenance.join(" | ") : "(none)"}`);
12506
+ lines.push(`NEXT ACTION: ${ctx.deliveryInstruction}`);
12507
+ lines.push(
12508
+ "Do NOT deliver by commenting. Use wire_publish with inResponseToSteerId so Cohort can link the result.",
12509
+ "[/WIRE REQUEST]"
12510
+ );
12511
+ return lines.join("\n");
12512
+ }
12387
12513
  function buildNotificationMessage(n) {
12388
12514
  let header;
12389
12515
  let cta;
@@ -12441,8 +12567,9 @@ Scope: ${truncated}`;
12441
12567
  }
12442
12568
  let prompt;
12443
12569
  if (n.type === "room_message") {
12444
- const moderationBlock = n.moderationContext != null ? renderModerationContext(n.moderationContext) : "";
12445
- const preamble = moderationBlock || n.behavioralPrompt;
12570
+ const wireSteerBlock = n.wireSteerContext != null ? renderWireSteerContext(n.wireSteerContext) : "";
12571
+ const moderationBlock = !wireSteerBlock && n.moderationContext != null ? renderModerationContext(n.moderationContext) : "";
12572
+ const preamble = wireSteerBlock || moderationBlock || n.behavioralPrompt;
12446
12573
  prompt = preamble ? `${preamble}
12447
12574
 
12448
12575
  ${ROOM_MESSAGE_RESPONSE_PROMPT}` : ROOM_MESSAGE_RESPONSE_PROMPT;
@@ -13990,7 +14117,7 @@ function dumpEvent(event) {
13990
14117
  function positiveNumber(value) {
13991
14118
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
13992
14119
  }
13993
- var PLUGIN_VERSION = true ? "0.32.0" : "unknown";
14120
+ var PLUGIN_VERSION = true ? "0.33.0" : "unknown";
13994
14121
  function resolveGatewayToken(api) {
13995
14122
  const token2 = api.config?.gateway?.auth?.token;
13996
14123
  return typeof token2 === "string" ? token2 : null;
@@ -15010,6 +15137,143 @@ var POCKET_GUIDE = `# Cohort Agent Guide (Pocket Version)
15010
15137
  function textResult(text, details) {
15011
15138
  return { content: [{ type: "text", text }], details: details ?? void 0 };
15012
15139
  }
15140
+ var WIRE_TITLE_MAX = 200;
15141
+ var WIRE_BRIEF_MAX = 2e3;
15142
+ var WIRE_BODY_MAX = 32e3;
15143
+ var WIRE_FILENAME_MAX = 200;
15144
+ var WIRE_MIME_TYPE_MAX = 100;
15145
+ var WIRE_ALT_TEXT_MAX = 1e3;
15146
+ var WIRE_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
15147
+ function wireInvalid(message) {
15148
+ return textResult(`Cannot publish to Wire: ${message}.`);
15149
+ }
15150
+ function requiredWireString(value, field, maxLength) {
15151
+ if (typeof value !== "string" || value.trim().length === 0) {
15152
+ return { ok: false, message: `${field} is required` };
15153
+ }
15154
+ const trimmed = value.trim();
15155
+ if (trimmed.length > maxLength) {
15156
+ return { ok: false, message: `${field} too long (max ${maxLength} characters)` };
15157
+ }
15158
+ return { ok: true, value: trimmed };
15159
+ }
15160
+ function optionalWireString(value, field, maxLength) {
15161
+ if (value === void 0 || value === null) {
15162
+ return { ok: true };
15163
+ }
15164
+ if (typeof value !== "string") {
15165
+ return { ok: false, message: `${field} must be a string` };
15166
+ }
15167
+ const trimmed = value.trim();
15168
+ if (!trimmed) {
15169
+ return { ok: true };
15170
+ }
15171
+ if (trimmed.length > maxLength) {
15172
+ return { ok: false, message: `${field} too long (max ${maxLength} characters)` };
15173
+ }
15174
+ return { ok: true, value: trimmed };
15175
+ }
15176
+ function decodeWireImageBase64(value) {
15177
+ if (typeof value !== "string" || value.trim().length === 0) {
15178
+ return { ok: false, message: "image_base64 is required" };
15179
+ }
15180
+ const dataUrlMatch = /^data:[^;,]+;base64,([\s\S]*)$/i.exec(value.trim());
15181
+ const normalized = (dataUrlMatch ? dataUrlMatch[1] : value).replace(/\s/g, "");
15182
+ if (!normalized) {
15183
+ return { ok: false, message: "image_base64 is required" };
15184
+ }
15185
+ if (normalized.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
15186
+ return { ok: false, message: "image_base64 must be valid base64" };
15187
+ }
15188
+ const bytes = Buffer2.from(normalized, "base64");
15189
+ if (bytes.byteLength === 0) {
15190
+ return { ok: false, message: "image_base64 is required" };
15191
+ }
15192
+ if (bytes.byteLength > WIRE_IMAGE_MAX_BYTES) {
15193
+ return { ok: false, message: `image too large (max ${WIRE_IMAGE_MAX_BYTES} bytes)` };
15194
+ }
15195
+ return { ok: true, bytes };
15196
+ }
15197
+ function getStorageIdFromUploadResponse(body) {
15198
+ try {
15199
+ const parsed = JSON.parse(body);
15200
+ if (typeof parsed !== "object" || parsed === null || !("storageId" in parsed)) {
15201
+ return null;
15202
+ }
15203
+ const storageId = parsed.storageId;
15204
+ return typeof storageId === "string" && storageId.trim() ? storageId.trim() : null;
15205
+ } catch {
15206
+ return null;
15207
+ }
15208
+ }
15209
+ function formatMetric(metric) {
15210
+ return `${metric.current}/${metric.target} ${metric.unit}`;
15211
+ }
15212
+ function redactSecrets(text) {
15213
+ return text.replace(/ch_(?:live|test)_[A-Za-z0-9_-]+/g, "[redacted]");
15214
+ }
15215
+ async function safeHttpError(response) {
15216
+ try {
15217
+ const body = await response.text();
15218
+ const parsed = JSON.parse(body);
15219
+ if (parsed && typeof parsed === "object") {
15220
+ const record = parsed;
15221
+ const message = typeof record.message === "string" ? record.message : record.error && typeof record.error === "object" && typeof record.error.message === "string" ? record.error.message : typeof record.error === "string" ? record.error : null;
15222
+ return message ? `: ${redactSecrets(message).slice(0, 300)}` : "";
15223
+ }
15224
+ } catch {
15225
+ }
15226
+ return "";
15227
+ }
15228
+ function renderTaskContext(context) {
15229
+ if (!context) return "";
15230
+ const lines = [];
15231
+ if (context.initiative) {
15232
+ const suffix = context.initiative.description ? ` \u2014 ${context.initiative.description}` : "";
15233
+ lines.push(`**Initiative:** ${context.initiative.name}${suffix}`);
15234
+ }
15235
+ if (context.goal) {
15236
+ const metric = context.goal.metric ? ` \u2014 ${formatMetric(context.goal.metric)}` : "";
15237
+ lines.push(`**Goal:** ${context.goal.title} (${context.goal.status})${metric}`);
15238
+ if (context.goal.test) {
15239
+ lines.push(`**Test:** ${context.goal.test}`);
15240
+ }
15241
+ }
15242
+ if (context.project) {
15243
+ const projectDetails = [context.project.status];
15244
+ if (context.project.progress) {
15245
+ projectDetails.push(`${context.project.progress.done}/${context.project.progress.total} tasks done`);
15246
+ }
15247
+ if (context.project.targetDate) {
15248
+ projectDetails.push(`target ${context.project.targetDate}`);
15249
+ }
15250
+ lines.push(`**Project:** ${context.project.title} (${projectDetails.join(", ")})`);
15251
+ }
15252
+ return lines.length > 0 ? ["## Why this matters", ...lines].join("\n") : "";
15253
+ }
15254
+ function renderGoal(goal) {
15255
+ const lines = [
15256
+ `# Goal: ${goal.title}`,
15257
+ `**Status:** ${goal.status}`
15258
+ ];
15259
+ if (goal.description) {
15260
+ lines.push("", "## Description", goal.description);
15261
+ }
15262
+ if (goal.test) {
15263
+ lines.push("", `**Test:** ${goal.test}`);
15264
+ }
15265
+ if (goal.metric) {
15266
+ lines.push(`**Metric:** ${formatMetric(goal.metric)}`);
15267
+ }
15268
+ if (goal.lastVerification) {
15269
+ const result = goal.lastVerification.passed ? "passed" : "failed";
15270
+ lines.push(
15271
+ `**Last verification:** ${result} by ${goal.lastVerification.byName} (${goal.lastVerification.byType}) at ${goal.lastVerification.at}`,
15272
+ `**Evidence:** ${goal.lastVerification.evidence}`
15273
+ );
15274
+ }
15275
+ return lines.join("\n");
15276
+ }
15013
15277
  var sharedHookState = null;
15014
15278
  var plugin = definePluginEntry({
15015
15279
  id: "cohort-sync",
@@ -15203,21 +15467,127 @@ Do not attempt more comments until tomorrow.`);
15203
15467
  }
15204
15468
  };
15205
15469
  });
15470
+ api.registerTool((toolCtx) => {
15471
+ const agentId = toolCtx.agentId ?? "main";
15472
+ return {
15473
+ name: "wire_publish",
15474
+ label: "wire_publish",
15475
+ description: "Publish a report or image to Cohort Wire. For images, pass base64 bytes plus image metadata; the tool uploads the file before publishing.",
15476
+ parameters: Type.Object({
15477
+ kind: Type.Union([
15478
+ Type.Literal("report"),
15479
+ Type.Literal("image")
15480
+ ], { description: 'Publish kind: "report" for text documents, "image" for image files.' }),
15481
+ title: Type.String({ description: `Title shown in Wire, max ${WIRE_TITLE_MAX} characters.` }),
15482
+ brief: Type.String({ description: `Short summary shown in Wire, max ${WIRE_BRIEF_MAX} characters.` }),
15483
+ body: Type.Optional(Type.String({ description: `Report markdown/body text, max ${WIRE_BODY_MAX} characters. Used with kind=report.` })),
15484
+ image_base64: Type.Optional(Type.String({ description: `Base64 image bytes, max ${WIRE_IMAGE_MAX_BYTES} decoded bytes. Used with kind=image.` })),
15485
+ mime_type: Type.Optional(Type.String({ description: `Image MIME type such as image/png, max ${WIRE_MIME_TYPE_MAX} characters. Used with kind=image.` })),
15486
+ filename: Type.Optional(Type.String({ description: `Original image filename, max ${WIRE_FILENAME_MAX} characters. Used with kind=image.` })),
15487
+ alt_text: Type.Optional(Type.String({ description: `Accessible image description, max ${WIRE_ALT_TEXT_MAX} characters. Used with kind=image.` })),
15488
+ 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." }))
15490
+ }),
15491
+ async execute(_toolCallId, params) {
15492
+ const rt = getToolRuntime();
15493
+ if (!rt.isReady) {
15494
+ return textResult("wire_publish is not ready yet - the plugin is still starting up. Try again in a few seconds.");
15495
+ }
15496
+ const title = requiredWireString(params.title, "title", WIRE_TITLE_MAX);
15497
+ if (!title.ok) return wireInvalid(title.message);
15498
+ const brief = requiredWireString(params.brief, "brief", WIRE_BRIEF_MAX);
15499
+ if (!brief.ok) return wireInvalid(brief.message);
15500
+ const roomId = optionalWireString(params.room_id, "room_id", WIRE_TITLE_MAX);
15501
+ if (!roomId.ok) return wireInvalid(roomId.message);
15502
+ const taskId = optionalWireString(params.task_id, "task_id", WIRE_TITLE_MAX);
15503
+ if (!taskId.ok) return wireInvalid(taskId.message);
15504
+ const agentName = rt.resolveAgentName(agentId);
15505
+ if (params.kind === "report") {
15506
+ const body = optionalWireString(params.body, "body", WIRE_BODY_MAX);
15507
+ if (!body.ok) return wireInvalid(body.message);
15508
+ try {
15509
+ const result = await callPublishWireFromPlugin(rt.apiKey, {
15510
+ agentName,
15511
+ kind: "report",
15512
+ title: title.value,
15513
+ brief: brief.value,
15514
+ ...body.value !== void 0 ? { body: body.value } : {},
15515
+ ...roomId.value !== void 0 ? { roomId: roomId.value } : {},
15516
+ ...taskId.value !== void 0 ? { taskId: taskId.value } : {}
15517
+ });
15518
+ return textResult(`Published report to Wire.
15519
+ Wire item: ${result.wireItemId}`, result);
15520
+ } catch (err) {
15521
+ const msg = getConvexAppErrorMessage(err) ?? (err instanceof Error ? err.message : String(err));
15522
+ return textResult(`Failed to publish to Wire: ${msg}`);
15523
+ }
15524
+ }
15525
+ if (params.kind !== "image") {
15526
+ return wireInvalid('kind must be "report" or "image"');
15527
+ }
15528
+ const mimeType = requiredWireString(params.mime_type, "mime_type", WIRE_MIME_TYPE_MAX);
15529
+ if (!mimeType.ok) return wireInvalid(mimeType.message);
15530
+ if (!mimeType.value.startsWith("image/")) {
15531
+ return wireInvalid("mime_type must start with image/");
15532
+ }
15533
+ const filename = requiredWireString(params.filename, "filename", WIRE_FILENAME_MAX);
15534
+ if (!filename.ok) return wireInvalid(filename.message);
15535
+ const altText = requiredWireString(params.alt_text, "alt_text", WIRE_ALT_TEXT_MAX);
15536
+ if (!altText.ok) return wireInvalid(altText.message);
15537
+ const image = decodeWireImageBase64(params.image_base64);
15538
+ if (!image.ok) return wireInvalid(image.message);
15539
+ try {
15540
+ const uploadUrl = await callGenerateWireUploadUrlFromPlugin(rt.apiKey, { agentName });
15541
+ const uploadResponse = await fetch(uploadUrl, {
15542
+ method: "POST",
15543
+ headers: { "Content-Type": mimeType.value },
15544
+ body: new Blob([image.bytes], { type: mimeType.value }),
15545
+ signal: AbortSignal.timeout(3e4)
15546
+ });
15547
+ const uploadBody = await uploadResponse.text();
15548
+ if (!uploadResponse.ok) {
15549
+ return textResult(`Failed to upload image to Wire: ${uploadResponse.status} ${uploadBody.slice(0, 300)}`);
15550
+ }
15551
+ const storageId = getStorageIdFromUploadResponse(uploadBody);
15552
+ if (!storageId) {
15553
+ return textResult("Failed to upload image to Wire: upload response did not include storageId.");
15554
+ }
15555
+ const result = await callPublishWireFromPlugin(rt.apiKey, {
15556
+ agentName,
15557
+ kind: "image",
15558
+ title: title.value,
15559
+ brief: brief.value,
15560
+ storageId,
15561
+ mimeType: mimeType.value,
15562
+ filename: filename.value,
15563
+ altText: altText.value,
15564
+ ...roomId.value !== void 0 ? { roomId: roomId.value } : {},
15565
+ ...taskId.value !== void 0 ? { taskId: taskId.value } : {}
15566
+ });
15567
+ return textResult(`Published image to Wire.
15568
+ Wire item: ${result.wireItemId}`, result);
15569
+ } catch (err) {
15570
+ const msg = getConvexAppErrorMessage(err) ?? (err instanceof Error ? err.message : String(err));
15571
+ return textResult(`Failed to publish to Wire: ${msg}`);
15572
+ }
15573
+ }
15574
+ };
15575
+ });
15206
15576
  api.registerTool((toolCtx) => {
15207
15577
  const agentId = toolCtx.agentId ?? "main";
15208
15578
  return {
15209
15579
  name: "cohort_room_start_moderation_session",
15210
15580
  label: "cohort_room_start_moderation_session",
15211
- description: "Start a managed moderation session in a Cohort Room. This is THE way to run round robins and moderated Q&A as the Room moderator: the backend prompts each participant in turn, tracks who responded, and tells you when to advance. Returns the session state block. The returned block includes the session_id \u2014 pass it to cohort_room_advance_moderation_session for every subsequent action.",
15581
+ description: 'Start a managed moderation session in a Cohort Room. Use round_robin = ask every participant to report in; use moderated_qna = route a question to one selected agent, wait for the answer, ask the asker whether it resolves the question, then complete. For multiple experts in Q&A, start with one best agent and use action: "followup" for additional experts one at a time. Returns the session state block. The returned block includes the session_id \u2014 pass it to cohort_room_advance_moderation_session for every subsequent action.',
15212
15582
  parameters: Type.Object({
15213
15583
  room_id: Type.String({ description: "Room ID supplied by Cohort, e.g. rooms:abc123" }),
15214
15584
  mode: Type.Union([
15215
15585
  Type.Literal("round_robin"),
15216
15586
  Type.Literal("moderated_qna")
15217
- ], { description: 'Session mode: "round_robin" (each participant responds in turn) or "moderated_qna" (one participant answers a question)' }),
15587
+ ], { description: 'Session mode: "round_robin" means every participant reports in; "moderated_qna" means one selected participant answers a question, with serial followups for more experts.' }),
15218
15588
  objective: Type.String({ description: "One-line objective for the session, e.g. 'Daily standup: blockers and progress'" }),
15219
- participant_names: Type.Array(Type.String(), { description: "Cohort agent names to include, in speaking order" }),
15220
- question_text: Type.Optional(Type.String({ description: "moderated_qna only: the question to ask the participant" })),
15589
+ participant_names: Type.Array(Type.String(), { description: "Cohort agent names to include, in speaking order; moderated_qna should include exactly one best agent" }),
15590
+ question_text: Type.Optional(Type.String({ description: "moderated_qna only: the question to ask the selected participant" })),
15221
15591
  auto_advance: Type.Optional(Type.Boolean({ description: "round_robin only: automatically prompt the next participant after each matched response" }))
15222
15592
  }),
15223
15593
  async execute(_toolCallId, params) {
@@ -15249,7 +15619,7 @@ Do not attempt more comments until tomorrow.`);
15249
15619
  return {
15250
15620
  name: "cohort_room_advance_moderation_session",
15251
15621
  label: "cohort_room_advance_moderation_session",
15252
- description: 'Advance a moderation session you are running: "next" prompts the next participant, "retry"/"skip"/"accept" resolve a mismatched turn, "followup" asks a target a follow-up question, and "complete" ends the session. Outcomes passed with action "complete" (summary, decisions, proposed_tasks, follow_ups) become the session recap; proposed_tasks surface for human approval. Returns the updated session state block.',
15622
+ description: 'Advance a moderation session you are running: "next" prompts the next round-robin participant, "retry"/"skip"/"accept" resolve a mismatched turn, followup asks one additional Q&A expert after the first answer, and "complete" ends the session. For moderated_qna, confirm with the asker before complete. Outcomes passed with action "complete" (summary, decisions, proposed_tasks, follow_ups) become the session recap; proposed_tasks surface for human approval. Returns the updated session state block.',
15253
15623
  parameters: Type.Object({
15254
15624
  session_id: Type.String({ description: "Moderation session ID returned by cohort_room_start_moderation_session" }),
15255
15625
  action: Type.Union([
@@ -15259,7 +15629,7 @@ Do not attempt more comments until tomorrow.`);
15259
15629
  Type.Literal("accept"),
15260
15630
  Type.Literal("followup"),
15261
15631
  Type.Literal("complete")
15262
- ], { description: "next = prompt the next participant (from waiting); retry = re-prompt the same agent; skip = skip the expected agent and move on; accept = accept an out-of-order reply as the response; followup = route a follow-up question (Q&A only); complete = finish the session (pass summary/decisions/proposed_tasks to create the recap)." }),
15632
+ ], { description: "next = prompt the next round-robin participant (from waiting); retry = re-prompt the same agent; skip = skip the expected agent and move on; accept = accept an out-of-order reply as the response; followup = route one additional Q&A question to one target agent; complete = finish after asker confirmation when in Q&A (pass summary/decisions/proposed_tasks to create the recap)." }),
15263
15633
  prompt: Type.Optional(Type.String({ description: "Custom prompt for the turn (next/retry/followup)" })),
15264
15634
  target_agent_name: Type.Optional(Type.String({ description: "followup only: the agent to ask" })),
15265
15635
  summary: Type.Optional(Type.String({ description: "complete only: session recap summary" })),
@@ -15457,11 +15827,13 @@ ${body}`,
15457
15827
  `# Task #${task.taskNumber}: ${task.title}`,
15458
15828
  `**Status:** ${task.status} | **Priority:** ${task.priority ?? "none"} | **Effort:** ${task.effort ?? "none"}`,
15459
15829
  `**Assigned to:** ${task.assignedTo ?? "unassigned"}`,
15460
- `**Created:** ${task.createdAt}`,
15461
- "",
15462
- "## Description",
15463
- task.description || "(no description)"
15830
+ `**Created:** ${task.createdAt}`
15464
15831
  ];
15832
+ const contextBlock = renderTaskContext(task.context);
15833
+ if (contextBlock) {
15834
+ lines.push("", contextBlock);
15835
+ }
15836
+ lines.push("", "## Description", task.description || "(no description)");
15465
15837
  if (params.include_comments !== false) {
15466
15838
  const limit = params.comment_limit ?? 10;
15467
15839
  const commentsRes = await fetch(
@@ -15492,6 +15864,62 @@ ${body}`,
15492
15864
  }
15493
15865
  };
15494
15866
  });
15867
+ api.registerTool(() => {
15868
+ return {
15869
+ name: "cohort_goal",
15870
+ label: "cohort_goal",
15871
+ description: "Fetch a Cohort goal \u2014 including its test \u2014 or submit a verification result after running the test. A goal's test tells you how to check whether the outcome is achieved.",
15872
+ parameters: Type.Object({
15873
+ goal_id: Type.String({ description: "Goal ID from Cohort." }),
15874
+ verify: Type.Optional(Type.Object({
15875
+ passed: Type.Boolean({ description: "Whether the goal test passed." }),
15876
+ evidence: Type.String({ description: "Evidence from running the test." }),
15877
+ metric_current: Type.Optional(Type.Number({ description: "Updated current metric value, if the goal has a metric." }))
15878
+ }))
15879
+ }),
15880
+ async execute(_toolCallId, params) {
15881
+ const rt = getToolRuntime();
15882
+ if (!rt.isReady) {
15883
+ return textResult("cohort_goal is not ready yet \u2014 the plugin is still starting up.");
15884
+ }
15885
+ const goalUrl = `${rt.apiUrl}/api/v1/goals/${encodeURIComponent(params.goal_id)}`;
15886
+ try {
15887
+ const response = await fetch(
15888
+ params.verify ? `${goalUrl}/verify` : goalUrl,
15889
+ {
15890
+ method: params.verify ? "POST" : "GET",
15891
+ headers: {
15892
+ "Authorization": `Bearer ${rt.apiKey}`,
15893
+ ...params.verify ? { "Content-Type": "application/json" } : {}
15894
+ },
15895
+ ...params.verify ? {
15896
+ body: JSON.stringify({
15897
+ passed: params.verify.passed,
15898
+ evidence: params.verify.evidence,
15899
+ ...params.verify.metric_current !== void 0 ? { metricCurrent: params.verify.metric_current } : {}
15900
+ })
15901
+ } : {},
15902
+ signal: AbortSignal.timeout(1e4)
15903
+ }
15904
+ );
15905
+ if (!response.ok) {
15906
+ const message = await safeHttpError(response);
15907
+ return textResult(`Failed to fetch goal ${params.goal_id}: ${response.status}${message}`);
15908
+ }
15909
+ const goal = await response.json();
15910
+ if (!params.verify) {
15911
+ return textResult(renderGoal(goal), goal);
15912
+ }
15913
+ const explanation = goal.status === "verification_pending" ? "Reported. A human will confirm before the goal is marked met." : goal.status === "met" ? "Goal marked met." : `Verification recorded. Goal status: ${goal.status}.`;
15914
+ return textResult(`${explanation}
15915
+
15916
+ ${renderGoal(goal)}`, goal);
15917
+ } catch (err) {
15918
+ return textResult(`Failed to fetch goal ${params.goal_id}: ${err instanceof Error ? redactSecrets(err.message) : "Unknown error"}`);
15919
+ }
15920
+ }
15921
+ };
15922
+ });
15495
15923
  api.registerTool((toolCtx) => {
15496
15924
  const agentId = toolCtx.agentId ?? "main";
15497
15925
  return {
@@ -15599,5 +16027,6 @@ ${body}`,
15599
16027
  });
15600
16028
  var index_default = plugin;
15601
16029
  export {
15602
- index_default as default
16030
+ index_default as default,
16031
+ renderTaskContext
15603
16032
  };
@@ -17,10 +17,12 @@
17
17
  "cohort_room_start_moderation_session",
18
18
  "cohort_room_advance_moderation_session",
19
19
  "cohort_room_cancel_moderation_session",
20
+ "wire_publish",
20
21
  "cohort_context",
21
22
  "cohort_briefing_context",
22
23
  "cohort_briefing",
23
24
  "cohort_task",
25
+ "cohort_goal",
24
26
  "cohort_transition",
25
27
  "cohort_assign"
26
28
  ]
@@ -80,5 +82,5 @@
80
82
  }
81
83
  }
82
84
  },
83
- "version": "0.32.0"
85
+ "version": "0.33.0"
84
86
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "OpenClaw plugin — syncs agent telemetry, sessions, and activity to the Cohort dashboard",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -15,10 +15,12 @@
15
15
  "cohort_room_start_moderation_session",
16
16
  "cohort_room_advance_moderation_session",
17
17
  "cohort_room_cancel_moderation_session",
18
+ "wire_publish",
18
19
  "cohort_context",
19
20
  "cohort_briefing_context",
20
21
  "cohort_briefing",
21
22
  "cohort_task",
23
+ "cohort_goal",
22
24
  "cohort_transition",
23
25
  "cohort_assign"
24
26
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfio/cohort-sync",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
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",