paperclip-plugin-mattermost 0.16.0 → 0.17.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/README.md CHANGED
@@ -84,7 +84,7 @@ PAPERCLIP_REF=<commit> node build.mjs # build for another Paperclip version
84
84
  Bump `version` in `package.json` and `src/manifest.ts`, commit, then tag and push:
85
85
 
86
86
  ```bash
87
- git tag -a v0.16.0 -m v0.16.0 && git push origin main v0.16.0
87
+ git tag -a v0.17.0 -m v0.17.0 && git push origin main v0.17.0
88
88
  ```
89
89
 
90
90
  GitHub Actions builds, tests and publishes to npm through npm trusted publishing, so no npm token is involved.
package/dist/manifest.js CHANGED
@@ -82,13 +82,14 @@ var route = (routeKey, method, path) => ({ routeKey, method, path, auth: "agent"
82
82
  var manifest = {
83
83
  id: "mattermost-bridge",
84
84
  apiVersion: 1,
85
- version: "0.16.0",
85
+ version: "0.17.0",
86
86
  displayName: "Mattermost",
87
87
  description: "Every agent becomes its own Mattermost bot. DM an agent or mention it in a channel and it answers in the thread, showing its steps as it works; follow-ups continue the conversation. Paperclip approvals arrive with Approve / Reject buttons.",
88
88
  author: "Haseeb Azhar",
89
89
  categories: ["connector"],
90
90
  capabilities: [
91
91
  "agents.read",
92
+ "companies.read",
92
93
  "agent.sessions.create",
93
94
  "agent.sessions.send",
94
95
  "agent.sessions.list",
package/dist/worker.js CHANGED
@@ -45032,7 +45032,9 @@ var Actions = class {
45032
45032
  this.d = deps;
45033
45033
  }
45034
45034
  /** One route call. `params` are the route's path parameters, `input` its query or body. */
45035
- async handle(agentId, routeKey, params, input2) {
45035
+ runId = null;
45036
+ async handle(agentId, routeKey, params, input2, runId = null) {
45037
+ this.runId = runId;
45036
45038
  const bot = this.d.bot(agentId);
45037
45039
  if (!bot) return bad(409, "You have no Mattermost bot in this company yet (your agent may be switched off in the plugin's settings).");
45038
45040
  try {
@@ -45184,13 +45186,13 @@ var Actions = class {
45184
45186
  const text2 = str3(input2.text);
45185
45187
  if (!text2) return bad(400, "text is empty");
45186
45188
  const ch = await this.channel(bot, str3(input2.channel));
45187
- const threadId = str3(input2.threadId);
45189
+ const threadId = str3(input2.threadId) || await this.d.taskThread?.(agentId, this.runId, ch.id).catch(() => null) || "";
45188
45190
  const waiting = await this.gate(agentId, canPost(text2), "post", `post in ~${ch.name}: "${text2.slice(0, 200)}"`, { channelId: ch.id, threadId, text: text2 });
45189
45191
  if (waiting) return waiting;
45190
45192
  return ok(await this.doPost(bot, ch.id, threadId, text2));
45191
45193
  }
45192
45194
  async doPost(bot, channelId, threadId, text2) {
45193
- const p = await bot.request("POST", "/posts", { channel_id: channelId, root_id: threadId, message: text2 });
45195
+ const p = await bot.request("POST", "/posts", { channel_id: channelId, root_id: threadId, message: this.d.format ? this.d.format(text2) : text2 });
45194
45196
  return { posted: p.id, threadId: p.root_id || p.id };
45195
45197
  }
45196
45198
  async react(bot, input2) {
@@ -45207,7 +45209,8 @@ var Actions = class {
45207
45209
  const verdict = canDm(await this.d.canTalk(user2.id, agentId));
45208
45210
  if (!verdict.ok) return bad(403, `Not allowed: ${verdict.why}.`);
45209
45211
  const channel2 = await bot.request("POST", "/channels/direct", [bot.botUserId, user2.id]);
45210
- return ok(await this.doPost(bot, channel2.id, "", text2));
45212
+ const threadId = str3(input2.threadId) || await this.d.taskThread?.(agentId, this.runId, channel2.id).catch(() => null) || "";
45213
+ return ok(await this.doPost(bot, channel2.id, threadId, text2));
45211
45214
  }
45212
45215
  // ---- channels ----------------------------------------------------------------------
45213
45216
  async created() {
@@ -45526,6 +45529,13 @@ function absoluteLinks(text2, base) {
45526
45529
  const root = base.replace(/\/+$/, "");
45527
45530
  return text2.replace(/\]\((\/(?!\/)[^)\s]*)\)/g, (_m, path3) => `](${root}${path3})`);
45528
45531
  }
45532
+ function linkTasks(text2, base, prefix) {
45533
+ if (!base || !/^[A-Z][A-Z0-9]{0,15}$/.test(prefix)) return text2;
45534
+ const root = base.replace(/\/+$/, "");
45535
+ const skip = /(```[\s\S]*?```|`[^`\n]*`|\[[^\]]*\]\([^)]*\)|<[^>\s]+>|https?:\/\/[^\s)]+)/;
45536
+ const id = new RegExp(`(?<![\\w/-])(${prefix}-\\d+)(?![\\w-])`, "g");
45537
+ return text2.split(skip).map((part, i) => i % 2 ? part : part.replace(id, (m) => `[${m}](${root}/${prefix}/issues/${m})`)).join("");
45538
+ }
45529
45539
  function splitMessage(text2, max = MAX_POST) {
45530
45540
  const parts = [];
45531
45541
  let rest = text2;
@@ -45551,8 +45561,9 @@ var Bridge = class {
45551
45561
  choiceButtons;
45552
45562
  canTalk;
45553
45563
  turnLimit;
45554
- /** Paperclip's public URL, for agents' relative links. */
45564
+ /** Paperclip's public URL, for agents' relative links, and the company's task prefix (BAD). */
45555
45565
  linkBase = "";
45566
+ taskPrefix = "";
45556
45567
  /**
45557
45568
  * Steering: messages people send in a thread while its agent is mid-run, kept for that agent to
45558
45569
  * read in the same run (`takeInbox`). One it read is not run again afterwards.
@@ -45774,7 +45785,7 @@ To send files back with your reply, save them in ${outbox}` : "");
45774
45785
  }
45775
45786
  if (this.active.get(member.agentId) === here) this.active.delete(member.agentId);
45776
45787
  this.inbox.delete(member.agentId);
45777
- const { text: body, choices } = extractChoices(absoluteLinks(reply ?? "", this.linkBase));
45788
+ const { text: body, choices } = extractChoices(linkTasks(absoluteLinks(reply ?? "", this.linkBase), this.linkBase, this.taskPrefix));
45778
45789
  const origin = { channelId: post.channel_id, rootId, channelType: evt.channelType, channelName: evt.channelName };
45779
45790
  const buttons = choices.length && this.choiceButtons ? this.choiceButtons(origin, choices) : null;
45780
45791
  const shown = choices.length && !buttons ? `${body}
@@ -46272,13 +46283,14 @@ var route = (routeKey, method, path3) => ({ routeKey, method, path: path3, auth:
46272
46283
  var manifest = {
46273
46284
  id: "mattermost-bridge",
46274
46285
  apiVersion: 1,
46275
- version: "0.16.0",
46286
+ version: "0.17.0",
46276
46287
  displayName: "Mattermost",
46277
46288
  description: "Every agent becomes its own Mattermost bot. DM an agent or mention it in a channel and it answers in the thread, showing its steps as it works; follow-ups continue the conversation. Paperclip approvals arrive with Approve / Reject buttons.",
46278
46289
  author: "Haseeb Azhar",
46279
46290
  categories: ["connector"],
46280
46291
  capabilities: [
46281
46292
  "agents.read",
46293
+ "companies.read",
46282
46294
  "agent.sessions.create",
46283
46295
  "agent.sessions.send",
46284
46296
  "agent.sessions.list",
@@ -46552,6 +46564,8 @@ var Tenant = class {
46552
46564
  void bridge.handle(evt);
46553
46565
  };
46554
46566
  bridge.linkBase = paperclipUrl;
46567
+ const company = await ctx.companies.get(companyId).catch(() => null);
46568
+ bridge.taskPrefix = company?.issuePrefix ?? "";
46555
46569
  bridge.onSteer = (agentId, postId) => {
46556
46570
  void ctx.events.emit("person-wrote", companyId, { agentId, postId }).catch(() => {
46557
46571
  });
@@ -46598,6 +46612,15 @@ var Tenant = class {
46598
46612
  agents: () => roster.all().map((m) => ({ agentId: m.agentId, agentName: m.agentName, username: m.username, botUserId: m.botUserId })),
46599
46613
  canTalk: async (userId, agentId) => !canTalk || canTalk(userId, agentId),
46600
46614
  origin: (agentId) => bridge.activeThread(agentId),
46615
+ format: (text2) => linkTasks(absoluteLinks(text2, paperclipUrl), paperclipUrl, bridge.taskPrefix),
46616
+ // The task this run works on (its execution or checkout run), and the thread it was asked for in.
46617
+ taskThread: async (agentId, runId, channelId) => {
46618
+ if (!runId) return null;
46619
+ const issues = await ctx.issues.list({ companyId, assigneeAgentId: agentId, limit: 200 });
46620
+ const issue2 = issues.find((i) => i.executionRunId === runId || i.checkoutRunId === runId);
46621
+ const at = issue2 ? await store.get(`issuethread:${issue2.id}`) : null;
46622
+ return at && at.channelId === channelId ? at.rootId : null;
46623
+ },
46601
46624
  task: async (taskId) => await ctx.issues.get(taskId, companyId),
46602
46625
  askApproval: async (req) => {
46603
46626
  if (!paperclipUrl || !approvers.size) throw Object.assign(new Error("this needs a person's approval, and nobody can approve from Mattermost yet (the plugin needs its Paperclip public URL and approvers set)"), { status: 403 });
@@ -46668,9 +46691,9 @@ var Tenant = class {
46668
46691
  return null;
46669
46692
  }
46670
46693
  /** An agent's call to one of the plugin's `/mm/...` routes. */
46671
- async api(agentId, routeKey, params, input2) {
46694
+ async api(agentId, routeKey, params, input2, runId = null) {
46672
46695
  if (!this.actions) return { status: 503, body: { error: `Mattermost is not connected for this company (${this.status})` } };
46673
- return this.actions.handle(agentId, routeKey, params, input2);
46696
+ return this.actions.handle(agentId, routeKey, params, input2, runId);
46674
46697
  }
46675
46698
  /** The Agent computer plugin: an agent wants a person at its computer. Posted where the agent is working. */
46676
46699
  async computerNeedsPerson(p) {
@@ -46826,6 +46849,14 @@ var plugin = definePlugin({
46826
46849
  ctx.events.on("plugin.agent-computer.needs-person", (e) => quietly("could not post a computer help request", async () => {
46827
46850
  await tenants.get(e.companyId)?.computerNeedsPerson(e.payload ?? {});
46828
46851
  }));
46852
+ ctx.events.on("issue.created", (e) => quietly("could not link a task to its thread", async () => {
46853
+ const tenant = tenants.get(e.companyId);
46854
+ if (!tenant?.bridge || !e.entityId) return;
46855
+ const issue2 = await ctx.issues.get(e.entityId, e.companyId);
46856
+ const by = issue2?.createdByAgentId;
46857
+ const at = by ? tenant.bridge.activeThread(by) : null;
46858
+ if (at) await tenant.store().set(`issuethread:${e.entityId}`, at);
46859
+ }));
46829
46860
  ctx.events.on("approval.decided", (e) => quietly("could not update approval", async () => {
46830
46861
  if (e.entityId) await tenants.get(e.companyId)?.approvals?.decided(e.entityId);
46831
46862
  }));
@@ -46865,7 +46896,7 @@ var plugin = definePlugin({
46865
46896
  const messages = tenant.bridge ? await tenant.bridge.takeInbox(agentId) : [];
46866
46897
  return { status: 200, body: { messages, note: messages.length ? "Written to you while you were working. Act on them now: they are not sent to you again." : "Nothing new." } };
46867
46898
  }
46868
- const res = await tenant.api(agentId, input2.routeKey, input2.params, { ...query, ...body });
46899
+ const res = await tenant.api(agentId, input2.routeKey, input2.params, { ...query, ...body }, input2.actor.runId ?? null);
46869
46900
  if (tenant.bridge?.hasInbox(agentId) && res.body && typeof res.body === "object" && !Array.isArray(res.body)) {
46870
46901
  res.body = { ...res.body, newMessages: await tenant.bridge.takeInbox(agentId) };
46871
46902
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-plugin-mattermost",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Paperclip agents in Mattermost: a bot per agent, live progress, files, approval and decision buttons, and per-person access, for each Paperclip company.",
5
5
  "type": "module",
6
6
  "license": "MIT",