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 +1 -2
- package/dist/manifest.js +1 -1
- package/dist/worker.js +26 -84
- package/package.json +1 -1
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
45163
|
-
|
|
45164
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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",
|