paperclip-plugin-mattermost 0.11.3 → 0.12.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
@@ -5,7 +5,7 @@ Your [Paperclip](https://github.com/paperclipai/paperclip) agents as Mattermost
5
5
  ## What it does
6
6
 
7
7
  - **A bot per agent.** Bots are created, renamed and switched off as agents change (every 5 minutes). Choose all agents, or only the ones you pick.
8
- - **You see it working.** The agent's bot reacts 👀 to your message straight away. Its reply starts as _thinking…_ and lists the steps it runs, then the answer replaces the list.
8
+ - **You see it working.** The agent's bot reacts 👀 to your message straight away, and shows Mattermost's "is typing…" indicator until its answer arrives.
9
9
  - **Files both ways.** Attach files to a message and the agent can read them. Files the agent saves are attached to its reply.
10
10
  - **Choices as buttons.** An agent can offer options (`choices: A | B | C` on its last line), which appear as buttons. A click is sent back as your answer.
11
11
  - **Approvals with buttons.** Every Paperclip approval is posted in an approvals channel with Approve / Reject buttons, including hires, budget overrides, board approvals and high-risk tool calls. An approval raised during a conversation also appears in that thread.
@@ -60,7 +60,6 @@ Or install it from **Settings → Plugins** in Paperclip.
60
60
  - **Multi-choice questions** and free-text "Other" answers are answered in Paperclip; the card links there.
61
61
  - **Mattermost can't hide bots** from particular people. Someone without access can still DM an agent bot; they get a private note and the agent doesn't run.
62
62
  - **Agents with a custom uploaded portrait** keep the default bot picture, because those images need a Paperclip login to download.
63
- - **Live progress** shows steps for agents run through ACPX (Claude Code, and others Paperclip runs that way). Other adapters show only _thinking…_.
64
63
 
65
64
  ## Development
66
65
 
package/dist/manifest.js CHANGED
@@ -2,7 +2,7 @@
2
2
  var manifest = {
3
3
  id: "mattermost-bridge",
4
4
  apiVersion: 1,
5
- version: "0.11.3",
5
+ version: "0.12.0",
6
6
  displayName: "Mattermost",
7
7
  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.",
8
8
  author: "Haseeb Azhar",
package/dist/worker.js CHANGED
@@ -44992,57 +44992,9 @@ function chosenProps(choice, who) {
44992
44992
  return { attachments: [{ text: `${who} chose **${choice}**`, actions: [] }] };
44993
44993
  }
44994
44994
 
44995
- // src/progress.ts
44996
- var THINKING = "_thinking\u2026_";
44997
- var MAX_STEPS = 6;
44998
- var MAX_LABEL2 = 100;
44999
- var Progress = class {
45000
- buffer = "";
45001
- steps = /* @__PURE__ */ new Map();
45002
- /** Feeds one stdout chunk. Returns true when what `render` shows changed. */
45003
- feed(chunk) {
45004
- this.buffer += chunk;
45005
- const lines = this.buffer.split("\n");
45006
- this.buffer = lines.pop() ?? "";
45007
- let changed = false;
45008
- for (const line of lines) {
45009
- if (!line.startsWith("{")) continue;
45010
- let e;
45011
- try {
45012
- e = JSON.parse(line);
45013
- } catch {
45014
- continue;
45015
- }
45016
- if (e.type !== "acpx.tool_call" || !e.toolCallId) continue;
45017
- const prev = this.steps.get(e.toolCallId);
45018
- const tool3 = prev?.tool ?? e.name ?? "tool";
45019
- const label2 = e.name && e.name !== tool3 ? e.name : prev?.label ?? tool3;
45020
- const status = e.status ?? prev?.status ?? "pending";
45021
- if (prev?.label !== label2 || prev?.status !== status) {
45022
- this.steps.set(e.toolCallId, { tool: tool3, label: label2, status });
45023
- changed = true;
45024
- }
45025
- }
45026
- return changed;
45027
- }
45028
- render() {
45029
- const head = THINKING;
45030
- const all = [...this.steps.values()];
45031
- if (!all.length) return head;
45032
- const shown = all.slice(-MAX_STEPS);
45033
- const lines = shown.map((s) => {
45034
- const mark = s.status === "completed" ? "\u2713" : s.status === "failed" ? "\u2717" : "\u2026";
45035
- const label2 = s.label.replace(/\s+/g, " ").trim();
45036
- return `- ${mark} \`${(label2.length > MAX_LABEL2 ? label2.slice(0, MAX_LABEL2) + "\u2026" : label2).replace(/`/g, "'")}\``;
45037
- });
45038
- if (all.length > shown.length) lines.unshift(`- _${all.length - shown.length} earlier steps_`);
45039
- return `${head}
45040
- ${lines.join("\n")}`;
45041
- }
45042
- };
45043
-
45044
44995
  // src/bridge.ts
45045
- var PROGRESS_EVERY_MS = 2e3;
44996
+ var TYPING_EVERY_MS = 3e3;
44997
+ var OLD_PLACEHOLDER = /^_(thinking…|.+ is working on it…)_/;
45046
44998
  var MAX_POST = 15e3;
45047
44999
  var MAX_HISTORY = 8e3;
45048
45000
  var MENTION = /(?:^|[^\w@])@([a-z0-9][a-z0-9._-]*)/gi;
@@ -45128,7 +45080,7 @@ var Bridge = class {
45128
45080
  const roster = this.roster();
45129
45081
  const lines = [];
45130
45082
  for (const p of await this.directory.threadPosts(rootId)) {
45131
- if (p.id === currentId || p.type || !p.message.trim() || p.message.startsWith(THINKING)) continue;
45083
+ if (p.id === currentId || p.type || !p.message.trim() || OLD_PLACEHOLDER.test(p.message)) continue;
45132
45084
  const who = p.user_id === me.botUserId ? "You" : `@${roster.byBotId(p.user_id)?.username ?? (await this.directory.userName(p.user_id)).username}`;
45133
45085
  lines.push(`${who}: ${p.message}`);
45134
45086
  }
@@ -45159,10 +45111,23 @@ var Bridge = class {
45159
45111
  const sender = await this.directory.userName(post.user_id);
45160
45112
  const where = isDirect ? "a direct message" : `#${evt.channelName}`;
45161
45113
  const history = post.root_id ? await this.history(post.root_id, post.id, member) : "";
45162
- const progress = new Progress();
45163
- const placeholder = await member.chat.createPost(post.channel_id, rootId, progress.render());
45164
- await member.chat.typing(post.channel_id, rootId).catch(() => {
45165
- });
45114
+ const typing = () => {
45115
+ void member.chat.typing(post.channel_id, rootId).catch(() => {
45116
+ });
45117
+ void member.chat.typing(post.channel_id, "").catch(() => {
45118
+ });
45119
+ };
45120
+ typing();
45121
+ const typingTimer = setInterval(typing, TYPING_EVERY_MS);
45122
+ try {
45123
+ await this.answer(evt, member, rootId, threadKey, thread, { sender, where, history, text: text2, fileIds });
45124
+ } finally {
45125
+ clearInterval(typingTimer);
45126
+ }
45127
+ }
45128
+ async answer(evt, member, rootId, threadKey, thread, m) {
45129
+ const { post } = evt;
45130
+ const { sender, where, history, text: text2, fileIds } = m;
45166
45131
  const attached = fileIds.length ? await this.files.receive(member.chat, post.id, fileIds) : [];
45167
45132
  const outbox = this.files ? await this.files.outbox(post.id).catch(() => null) : null;
45168
45133
  const prompt = `${sender.displayName} (@${sender.username}) wrote to you (@${member.username}) in ${where} on Mattermost. Your reply is posted back there, so answer in Markdown. To offer choices, end your reply with one line \`choices: First | Second\` (2 to ${MAX_CHOICES} short options); they are shown as buttons, and the pick comes back as the next message.` + (history ? `
@@ -45178,23 +45143,6 @@ Attached files, saved on disk (read them at these paths):
45178
45143
  ${attached.join("\n")}` : "") + (outbox ? `
45179
45144
 
45180
45145
  To send files back with your reply, save them in ${outbox}` : "");
45181
- let timer = null;
45182
- let edit = Promise.resolve();
45183
- let finished = false;
45184
- const onOutput = (chunk) => {
45185
- if (finished || !progress.feed(chunk) || timer) return;
45186
- timer = setTimeout(() => {
45187
- timer = null;
45188
- if (finished) return;
45189
- edit = edit.then(() => member.chat.patchPost(placeholder.id, progress.render())).catch(() => {
45190
- });
45191
- }, PROGRESS_EVERY_MS);
45192
- };
45193
- const settle2 = async () => {
45194
- finished = true;
45195
- if (timer) clearTimeout(timer);
45196
- await edit;
45197
- };
45198
45146
  let reply;
45199
45147
  const here = { channelId: post.channel_id, rootId };
45200
45148
  this.active.set(member.agentId, here);
@@ -45202,21 +45150,19 @@ To send files back with your reply, save them in ${outbox}` : "");
45202
45150
  let sessionId = thread && thread.agentId === member.agentId ? thread.sessionId : null;
45203
45151
  try {
45204
45152
  if (!sessionId) throw new Error("no session yet");
45205
- reply = await this.agents.ask(sessionId, prompt, onOutput);
45153
+ reply = await this.agents.ask(sessionId, prompt);
45206
45154
  } catch (first2) {
45207
45155
  if (sessionId) this.log("session unusable, starting a new one", { sessionId, err: String(first2) });
45208
45156
  sessionId = await this.agents.createSession(member.agentId, `mattermost:${rootId}`);
45209
45157
  await this.store.set(threadKey, { agentId: member.agentId, sessionId });
45210
- reply = await this.agents.ask(sessionId, prompt, onOutput);
45158
+ reply = await this.agents.ask(sessionId, prompt);
45211
45159
  }
45212
45160
  } catch (err) {
45213
45161
  if (this.active.get(member.agentId) === here) this.active.delete(member.agentId);
45214
- await settle2();
45215
- await member.chat.patchPost(placeholder.id, `:warning: ${member.agentName} could not answer: ${String(err?.message ?? err).slice(0, 500)}`);
45162
+ await member.chat.createPost(post.channel_id, rootId, `:warning: ${member.agentName} could not answer: ${String(err?.message ?? err).slice(0, 500)}`);
45216
45163
  return;
45217
45164
  }
45218
45165
  if (this.active.get(member.agentId) === here) this.active.delete(member.agentId);
45219
- await settle2();
45220
45166
  const { text: body, choices } = extractChoices(reply ?? "");
45221
45167
  const origin = { channelId: post.channel_id, rootId, channelType: evt.channelType, channelName: evt.channelName };
45222
45168
  const buttons = choices.length && this.choiceButtons ? this.choiceButtons(origin, choices) : null;
@@ -45224,7 +45170,7 @@ To send files back with your reply, save them in ${outbox}` : "");
45224
45170
 
45225
45171
  ${choices.map((c, i) => `${i + 1}. ${c}`).join("\n")}` : body;
45226
45172
  const [first, ...more] = splitMessage(shown.trim() || `_${member.agentName} finished without a reply._`);
45227
- await member.chat.patchPost(placeholder.id, first);
45173
+ await member.chat.createPost(post.channel_id, rootId, first);
45228
45174
  for (const part of more) await member.chat.createPost(post.channel_id, rootId, part);
45229
45175
  if (buttons) await member.chat.createPost(post.channel_id, rootId, "", buttons);
45230
45176
  if (outbox) {
@@ -45316,9 +45262,6 @@ var MattermostClient = class {
45316
45262
  async react(postId, emoji3) {
45317
45263
  await this.request("POST", "/reactions", { user_id: this.botUserId, post_id: postId, emoji_name: emoji3 });
45318
45264
  }
45319
- async patchPost(postId, message2) {
45320
- await this.request("PUT", `/posts/${postId}/patch`, { message: message2 });
45321
- }
45322
45265
  async typing(channelId, rootId) {
45323
45266
  await this.request("POST", "/users/me/typing", { channel_id: channelId, parent_id: rootId });
45324
45267
  }
@@ -45714,7 +45657,7 @@ var AgentBots = class {
45714
45657
  var manifest = {
45715
45658
  id: "mattermost-bridge",
45716
45659
  apiVersion: 1,
45717
- version: "0.11.3",
45660
+ version: "0.12.0",
45718
45661
  displayName: "Mattermost",
45719
45662
  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.",
45720
45663
  author: "Haseeb Azhar",
@@ -45842,14 +45785,13 @@ var Tenant = class {
45842
45785
  const key = `plugin:${PLUGIN_KEY}:session:${taskKey}`;
45843
45786
  return (await ctx.agents.sessions.create(agentId, companyId, { taskKey: key, reason: "Mattermost thread" })).sessionId;
45844
45787
  },
45845
- ask(sessionId, prompt, onOutput) {
45788
+ ask(sessionId, prompt) {
45846
45789
  return new Promise((resolve, reject) => {
45847
45790
  const timer = setTimeout(() => reject(new Error("no reply after 30 minutes")), ASK_TIMEOUT_MS);
45848
45791
  ctx.agents.sessions.sendMessage(sessionId, companyId, {
45849
45792
  prompt,
45850
45793
  reason: "Mattermost message",
45851
45794
  onEvent: (e) => {
45852
- if (e.eventType === "chunk" && e.stream === "stdout" && e.message) onOutput?.(e.message);
45853
45795
  if (e.eventType === "done") {
45854
45796
  clearTimeout(timer);
45855
45797
  resolve(e.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-plugin-mattermost",
3
- "version": "0.11.3",
3
+ "version": "0.12.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",