paperclip-plugin-mattermost 0.16.1 → 0.18.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 +2 -1
- package/dist/manifest.js +9 -6
- package/dist/worker.js +51 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ Your [Paperclip](https://github.com/paperclipai/paperclip) agents as Mattermost
|
|
|
14
14
|
- **Single-choice questions** get a button per option. Your pick is posted on the task as a comment from you, which wakes the agent.
|
|
15
15
|
- **Access control.** Choose who may talk to agents: everyone, or only the people you pick. Each person can be limited to some agents, and people outside Paperclip can be added by email.
|
|
16
16
|
- **Agents work in Mattermost themselves.** Through plugin API routes that only agents can call, each agent reads the channels its bot is in, posts, reacts, searches, and DMs the people allowed to talk to it, always as its own bot. Each company gets a Mattermost skill that tells agents how, and saving the settings adds it to every agent with a bot, so they can message people from tasks and scheduled runs too.
|
|
17
|
+
- **Work happens in Paperclip tasks.** Asked to do something, an agent creates a Paperclip task, replies with its number, and does the work there. When the task is done, cancelled or blocked, its outcome is posted back in the thread it was asked in.
|
|
17
18
|
- **Steering.** A message you send in a thread while its agent is still working reaches that run: the agent reads it between steps (and an agent waiting at its computer stops waiting to read it), so "stop" or a correction lands mid-task. One it read is marked ✅ and not run again.
|
|
18
19
|
- **Agents work with agents.** An agent mentioning another agent starts it, and it answers in the thread. After a set number of agent-to-agent messages in a thread (10 by default, set in settings), they pause until a person replies.
|
|
19
20
|
- **Work channels.** For an investigation, incident or project, an agent opens a private channel (`inc-checkout-5xx`), pulls in the people and agents it needs, and ties it to a Paperclip task. Task comments and status changes show up in the channel. When the task is done or cancelled, the channel is archived and the outcome is posted back where the work was asked for.
|
|
@@ -84,7 +85,7 @@ PAPERCLIP_REF=<commit> node build.mjs # build for another Paperclip version
|
|
|
84
85
|
Bump `version` in `package.json` and `src/manifest.ts`, commit, then tag and push:
|
|
85
86
|
|
|
86
87
|
```bash
|
|
87
|
-
git tag -a v0.
|
|
88
|
+
git tag -a v0.18.0 -m v0.18.0 && git push origin main v0.18.0
|
|
88
89
|
```
|
|
89
90
|
|
|
90
91
|
GitHub Actions builds, tests and publishes to npm through npm trusted publishing, so no npm token is involved.
|
package/dist/manifest.js
CHANGED
|
@@ -16,11 +16,13 @@ tell whoever you are working with and carry on. A 403 says why something is not
|
|
|
16
16
|
|
|
17
17
|
## How to work
|
|
18
18
|
|
|
19
|
-
- **
|
|
20
|
-
task (assign it to yourself or delegate it) and say so in the thread
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
- **Work within Paperclip tasks.** A quick question: answer in the thread. Anything you are asked to
|
|
20
|
+
do: create a Paperclip task first (assign it to yourself or delegate it) and say so in the thread
|
|
21
|
+
with its number. Do the work in the task and end with the outcome as its last comment: when the
|
|
22
|
+
task is done, cancelled or blocked, that comment is posted back in the thread it was asked in. An
|
|
23
|
+
investigation, incident or project, or anything involving several agents or more than a day: also
|
|
24
|
+
open a work channel for it (below), with the person who asked and whoever you need. Name it for
|
|
25
|
+
what it is: "inc checkout 5xx", "inv q3 conversion drop", "proj crm migration".
|
|
24
26
|
- **Announce a work channel where the request came in**, in one message: what it is for, who you
|
|
25
27
|
pulled in, and that you will report back there. When the task is done or cancelled the plugin
|
|
26
28
|
archives the channel and posts the outcome back where you asked; you do not need to.
|
|
@@ -82,13 +84,14 @@ var route = (routeKey, method, path) => ({ routeKey, method, path, auth: "agent"
|
|
|
82
84
|
var manifest = {
|
|
83
85
|
id: "mattermost-bridge",
|
|
84
86
|
apiVersion: 1,
|
|
85
|
-
version: "0.
|
|
87
|
+
version: "0.18.0",
|
|
86
88
|
displayName: "Mattermost",
|
|
87
89
|
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
90
|
author: "Haseeb Azhar",
|
|
89
91
|
categories: ["connector"],
|
|
90
92
|
capabilities: [
|
|
91
93
|
"agents.read",
|
|
94
|
+
"companies.read",
|
|
92
95
|
"agent.sessions.create",
|
|
93
96
|
"agent.sessions.send",
|
|
94
97
|
"agent.sessions.list",
|
package/dist/worker.js
CHANGED
|
@@ -45192,7 +45192,7 @@ var Actions = class {
|
|
|
45192
45192
|
return ok(await this.doPost(bot, ch.id, threadId, text2));
|
|
45193
45193
|
}
|
|
45194
45194
|
async doPost(bot, channelId, threadId, text2) {
|
|
45195
|
-
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 });
|
|
45196
45196
|
return { posted: p.id, threadId: p.root_id || p.id };
|
|
45197
45197
|
}
|
|
45198
45198
|
async react(bot, input2) {
|
|
@@ -45353,6 +45353,7 @@ var Actions = class {
|
|
|
45353
45353
|
// src/worklinks.ts
|
|
45354
45354
|
var FROM_PAPERCLIP = { from_paperclip: "true" };
|
|
45355
45355
|
var CLOSED = /* @__PURE__ */ new Set(["done", "cancelled"]);
|
|
45356
|
+
var THREAD_REPORTS = { done: ":white_check_mark: Done:", cancelled: ":no_entry_sign: Cancelled:", blocked: ":pause_button: Blocked:" };
|
|
45356
45357
|
var STATUS_WORDS = {
|
|
45357
45358
|
todo: "to do",
|
|
45358
45359
|
in_progress: "in progress",
|
|
@@ -45388,6 +45389,25 @@ var WorkLinks = class {
|
|
|
45388
45389
|
${body}`;
|
|
45389
45390
|
await this.d.post(own2 ? c.authorAgentId : hit.link.agentId, hit.channelId, "", text2);
|
|
45390
45391
|
}
|
|
45392
|
+
/**
|
|
45393
|
+
* A task asked for in a Mattermost thread (created by the agent while answering there): when it is
|
|
45394
|
+
* done, cancelled or blocked, its outcome (the last comment) is posted back in that thread, so the
|
|
45395
|
+
* result always reaches the conversation it came from.
|
|
45396
|
+
*/
|
|
45397
|
+
async threadUpdate(issueId) {
|
|
45398
|
+
const at = await this.d.store.get(`issuethread:${issueId}`);
|
|
45399
|
+
if (!at) return;
|
|
45400
|
+
const task2 = await this.d.task(issueId);
|
|
45401
|
+
if (!task2 || task2.status === at.status || !THREAD_REPORTS[task2.status]) return;
|
|
45402
|
+
await this.d.store.set(`issuethread:${issueId}`, { ...at, status: task2.status });
|
|
45403
|
+
const last = (await this.d.comments(issueId)).at(-1)?.body?.trim();
|
|
45404
|
+
const outcome = last ? `
|
|
45405
|
+
|
|
45406
|
+
${last.length > 3e3 ? last.slice(0, 3e3) + "\u2026" : last}` : "";
|
|
45407
|
+
const by = [task2.assigneeAgentId, at.agentId].find((a) => Boolean(a && this.d.hasBot(a)));
|
|
45408
|
+
if (!by) return;
|
|
45409
|
+
await this.d.post(by, at.channelId, at.rootId, `${THREAD_REPORTS[task2.status]} **${task2.identifier ?? "The task"}** ${task2.title}${outcome}`);
|
|
45410
|
+
}
|
|
45391
45411
|
/** The task changed: note a new status; on done or cancelled, archive and report back. */
|
|
45392
45412
|
async updated(issueId) {
|
|
45393
45413
|
const hit = await this.linked(issueId);
|
|
@@ -45443,11 +45463,13 @@ tell whoever you are working with and carry on. A 403 says why something is not
|
|
|
45443
45463
|
|
|
45444
45464
|
## How to work
|
|
45445
45465
|
|
|
45446
|
-
- **
|
|
45447
|
-
task (assign it to yourself or delegate it) and say so in the thread
|
|
45448
|
-
|
|
45449
|
-
|
|
45450
|
-
|
|
45466
|
+
- **Work within Paperclip tasks.** A quick question: answer in the thread. Anything you are asked to
|
|
45467
|
+
do: create a Paperclip task first (assign it to yourself or delegate it) and say so in the thread
|
|
45468
|
+
with its number. Do the work in the task and end with the outcome as its last comment: when the
|
|
45469
|
+
task is done, cancelled or blocked, that comment is posted back in the thread it was asked in. An
|
|
45470
|
+
investigation, incident or project, or anything involving several agents or more than a day: also
|
|
45471
|
+
open a work channel for it (below), with the person who asked and whoever you need. Name it for
|
|
45472
|
+
what it is: "inc checkout 5xx", "inv q3 conversion drop", "proj crm migration".
|
|
45451
45473
|
- **Announce a work channel where the request came in**, in one message: what it is for, who you
|
|
45452
45474
|
pulled in, and that you will report back there. When the task is done or cancelled the plugin
|
|
45453
45475
|
archives the channel and posts the outcome back where you asked; you do not need to.
|
|
@@ -45529,6 +45551,13 @@ function absoluteLinks(text2, base) {
|
|
|
45529
45551
|
const root = base.replace(/\/+$/, "");
|
|
45530
45552
|
return text2.replace(/\]\((\/(?!\/)[^)\s]*)\)/g, (_m, path3) => `](${root}${path3})`);
|
|
45531
45553
|
}
|
|
45554
|
+
function linkTasks(text2, base, prefix) {
|
|
45555
|
+
if (!base || !/^[A-Z][A-Z0-9]{0,15}$/.test(prefix)) return text2;
|
|
45556
|
+
const root = base.replace(/\/+$/, "");
|
|
45557
|
+
const skip = /(```[\s\S]*?```|`[^`\n]*`|\[[^\]]*\]\([^)]*\)|<[^>\s]+>|https?:\/\/[^\s)]+)/;
|
|
45558
|
+
const id = new RegExp(`(?<![\\w/-])(${prefix}-\\d+)(?![\\w-])`, "g");
|
|
45559
|
+
return text2.split(skip).map((part, i) => i % 2 ? part : part.replace(id, (m) => `[${m}](${root}/${prefix}/issues/${m})`)).join("");
|
|
45560
|
+
}
|
|
45532
45561
|
function splitMessage(text2, max = MAX_POST) {
|
|
45533
45562
|
const parts = [];
|
|
45534
45563
|
let rest = text2;
|
|
@@ -45554,8 +45583,9 @@ var Bridge = class {
|
|
|
45554
45583
|
choiceButtons;
|
|
45555
45584
|
canTalk;
|
|
45556
45585
|
turnLimit;
|
|
45557
|
-
/** Paperclip's public URL, for agents' relative links. */
|
|
45586
|
+
/** Paperclip's public URL, for agents' relative links, and the company's task prefix (BAD). */
|
|
45558
45587
|
linkBase = "";
|
|
45588
|
+
taskPrefix = "";
|
|
45559
45589
|
/**
|
|
45560
45590
|
* Steering: messages people send in a thread while its agent is mid-run, kept for that agent to
|
|
45561
45591
|
* read in the same run (`takeInbox`). One it read is not run again afterwards.
|
|
@@ -45742,7 +45772,7 @@ var Bridge = class {
|
|
|
45742
45772
|
const attached = fileIds.length ? await this.files.receive(member.chat, post.id, fileIds) : [];
|
|
45743
45773
|
const outbox = this.files ? await this.files.outbox(post.id).catch(() => null) : null;
|
|
45744
45774
|
const fromAgent = this.roster().isBot(post.user_id);
|
|
45745
|
-
const prompt = `${sender.displayName} (@${sender.username}${fromAgent ? ", another agent" : ""}) 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. You can also read channels, post, message people, mention other agents and open work channels in Mattermost: GET <your Paperclip API URL>/api/plugins/mattermost-bridge/api/mm/help shows how. If this takes more than a minute or two, check GET \u2026/api/plugins/mattermost-bridge/api/mm/inbox between steps: it returns anything people write in this thread meanwhile (a correction, "stop", "never mind"); act on it then, as it is not sent to you again.` + (history ? `
|
|
45775
|
+
const prompt = `${sender.displayName} (@${sender.username}${fromAgent ? ", another agent" : ""}) wrote to you (@${member.username}) in ${where} on Mattermost. Your reply is posted back there, so answer in Markdown. Work within Paperclip tasks: if the message asks you to do something (anything more than answering a quick question), first create a Paperclip task for it, assigned to yourself or to the agent it belongs with, and reply here with the task's number, what will happen, and that the result will come back to this thread. Then do the work in that task, not in this chat, and finish with the outcome as the task's last comment: when the task is done, cancelled or blocked, that comment is posted back here for you. Quick questions you can answer directly. 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. You can also read channels, post, message people, mention other agents and open work channels in Mattermost: GET <your Paperclip API URL>/api/plugins/mattermost-bridge/api/mm/help shows how. If this takes more than a minute or two, check GET \u2026/api/plugins/mattermost-bridge/api/mm/inbox between steps: it returns anything people write in this thread meanwhile (a correction, "stop", "never mind"); act on it then, as it is not sent to you again.` + (history ? `
|
|
45746
45776
|
|
|
45747
45777
|
Earlier in this thread (oldest first):
|
|
45748
45778
|
${history}
|
|
@@ -45777,7 +45807,7 @@ To send files back with your reply, save them in ${outbox}` : "");
|
|
|
45777
45807
|
}
|
|
45778
45808
|
if (this.active.get(member.agentId) === here) this.active.delete(member.agentId);
|
|
45779
45809
|
this.inbox.delete(member.agentId);
|
|
45780
|
-
const { text: body, choices } = extractChoices(absoluteLinks(reply ?? "", this.linkBase));
|
|
45810
|
+
const { text: body, choices } = extractChoices(linkTasks(absoluteLinks(reply ?? "", this.linkBase), this.linkBase, this.taskPrefix));
|
|
45781
45811
|
const origin = { channelId: post.channel_id, rootId, channelType: evt.channelType, channelName: evt.channelName };
|
|
45782
45812
|
const buttons = choices.length && this.choiceButtons ? this.choiceButtons(origin, choices) : null;
|
|
45783
45813
|
const shown = choices.length && !buttons ? `${body}
|
|
@@ -46275,13 +46305,14 @@ var route = (routeKey, method, path3) => ({ routeKey, method, path: path3, auth:
|
|
|
46275
46305
|
var manifest = {
|
|
46276
46306
|
id: "mattermost-bridge",
|
|
46277
46307
|
apiVersion: 1,
|
|
46278
|
-
version: "0.
|
|
46308
|
+
version: "0.18.0",
|
|
46279
46309
|
displayName: "Mattermost",
|
|
46280
46310
|
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.",
|
|
46281
46311
|
author: "Haseeb Azhar",
|
|
46282
46312
|
categories: ["connector"],
|
|
46283
46313
|
capabilities: [
|
|
46284
46314
|
"agents.read",
|
|
46315
|
+
"companies.read",
|
|
46285
46316
|
"agent.sessions.create",
|
|
46286
46317
|
"agent.sessions.send",
|
|
46287
46318
|
"agent.sessions.list",
|
|
@@ -46555,6 +46586,8 @@ var Tenant = class {
|
|
|
46555
46586
|
void bridge.handle(evt);
|
|
46556
46587
|
};
|
|
46557
46588
|
bridge.linkBase = paperclipUrl;
|
|
46589
|
+
const company = await ctx.companies.get(companyId).catch(() => null);
|
|
46590
|
+
bridge.taskPrefix = company?.issuePrefix ?? "";
|
|
46558
46591
|
bridge.onSteer = (agentId, postId) => {
|
|
46559
46592
|
void ctx.events.emit("person-wrote", companyId, { agentId, postId }).catch(() => {
|
|
46560
46593
|
});
|
|
@@ -46601,6 +46634,7 @@ var Tenant = class {
|
|
|
46601
46634
|
agents: () => roster.all().map((m) => ({ agentId: m.agentId, agentName: m.agentName, username: m.username, botUserId: m.botUserId })),
|
|
46602
46635
|
canTalk: async (userId, agentId) => !canTalk || canTalk(userId, agentId),
|
|
46603
46636
|
origin: (agentId) => bridge.activeThread(agentId),
|
|
46637
|
+
format: (text2) => linkTasks(absoluteLinks(text2, paperclipUrl), paperclipUrl, bridge.taskPrefix),
|
|
46604
46638
|
// The task this run works on (its execution or checkout run), and the thread it was asked for in.
|
|
46605
46639
|
taskThread: async (agentId, runId, channelId) => {
|
|
46606
46640
|
if (!runId) return null;
|
|
@@ -46625,7 +46659,8 @@ var Tenant = class {
|
|
|
46625
46659
|
store,
|
|
46626
46660
|
post: async (agentId, channelId, rootId, text2) => {
|
|
46627
46661
|
const as = roster.clientOf(agentId) ?? manager;
|
|
46628
|
-
|
|
46662
|
+
const message2 = linkTasks(absoluteLinks(text2, paperclipUrl), paperclipUrl, bridge.taskPrefix);
|
|
46663
|
+
await as.request("POST", "/posts", { channel_id: channelId, root_id: rootId, message: message2, props: FROM_PAPERCLIP });
|
|
46629
46664
|
},
|
|
46630
46665
|
hasBot: (agentId) => Boolean(roster.clientOf(agentId)),
|
|
46631
46666
|
agentName: (agentId) => agentName(agentId),
|
|
@@ -46832,7 +46867,10 @@ var plugin = definePlugin({
|
|
|
46832
46867
|
if (e.entityId && commentId) await tenants.get(e.companyId)?.work?.comment(e.entityId, commentId);
|
|
46833
46868
|
}));
|
|
46834
46869
|
ctx.events.on("issue.updated", (e) => quietly("could not follow a task update", async () => {
|
|
46835
|
-
if (e.entityId)
|
|
46870
|
+
if (!e.entityId) return;
|
|
46871
|
+
const work = tenants.get(e.companyId)?.work;
|
|
46872
|
+
await work?.updated(e.entityId);
|
|
46873
|
+
await work?.threadUpdate(e.entityId);
|
|
46836
46874
|
}));
|
|
46837
46875
|
ctx.events.on("plugin.agent-computer.needs-person", (e) => quietly("could not post a computer help request", async () => {
|
|
46838
46876
|
await tenants.get(e.companyId)?.computerNeedsPerson(e.payload ?? {});
|
|
@@ -46843,7 +46881,7 @@ var plugin = definePlugin({
|
|
|
46843
46881
|
const issue2 = await ctx.issues.get(e.entityId, e.companyId);
|
|
46844
46882
|
const by = issue2?.createdByAgentId;
|
|
46845
46883
|
const at = by ? tenant.bridge.activeThread(by) : null;
|
|
46846
|
-
if (at) await tenant.store().set(`issuethread:${e.entityId}`, at);
|
|
46884
|
+
if (at) await tenant.store().set(`issuethread:${e.entityId}`, { ...at, agentId: by, status: issue2?.status });
|
|
46847
46885
|
}));
|
|
46848
46886
|
ctx.events.on("approval.decided", (e) => quietly("could not update approval", async () => {
|
|
46849
46887
|
if (e.entityId) await tenants.get(e.companyId)?.approvals?.decided(e.entityId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperclip-plugin-mattermost",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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",
|