paperclip-plugin-mattermost 0.18.3 → 0.19.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 +7 -1
- package/dist/ui/index.js +30 -0
- package/dist/worker.js +50 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ Or install it from **Settings → Plugins** in Paperclip.
|
|
|
49
49
|
- **Mattermost admin token**: pick the secret from step 2.
|
|
50
50
|
- **Paperclip public URL**: filled in from Paperclip's own settings. The page checks it by sending a test click through it.
|
|
51
51
|
- **Approvals channel**: where approvals and decision cards are posted.
|
|
52
|
+
- **Channel per agent** (optional): send one agent's approvals, decision cards and computer help requests to its own channel, for example a client reporting agent's drafts to `#client-reporting`. Only approvers can decide them, but everyone in the channel sees them. A channel that cannot be found falls back to the approvals channel, with a note.
|
|
52
53
|
- **Agents in Mattermost**: all agents, or the ones you pick.
|
|
53
54
|
- **Who can talk to agents**: everyone, or the people you pick, each with all agents or some.
|
|
54
55
|
- **Approvers**: who may decide approvals and cards from Mattermost. Their clicks count as their Paperclip decisions, matched to Mattermost by email.
|
|
@@ -85,7 +86,7 @@ PAPERCLIP_REF=<commit> node build.mjs # build for another Paperclip version
|
|
|
85
86
|
Bump `version` in `package.json` and `src/manifest.ts`, commit, then tag and push:
|
|
86
87
|
|
|
87
88
|
```bash
|
|
88
|
-
git tag -a v0.
|
|
89
|
+
git tag -a v0.19.0 -m v0.19.0 && git push origin main v0.19.0
|
|
89
90
|
```
|
|
90
91
|
|
|
91
92
|
GitHub Actions builds, tests and publishes to npm through npm trusted publishing, so no npm token is involved.
|
package/dist/manifest.js
CHANGED
|
@@ -84,7 +84,7 @@ var route = (routeKey, method, path) => ({ routeKey, method, path, auth: "agent"
|
|
|
84
84
|
var manifest = {
|
|
85
85
|
id: "mattermost-bridge",
|
|
86
86
|
apiVersion: 1,
|
|
87
|
-
version: "0.
|
|
87
|
+
version: "0.19.0",
|
|
88
88
|
displayName: "Mattermost",
|
|
89
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.",
|
|
90
90
|
author: "Haseeb Azhar",
|
|
@@ -170,6 +170,12 @@ var manifest = {
|
|
|
170
170
|
title: "Approvals channel",
|
|
171
171
|
description: "Channel name (as in its URL, e.g. approvals) where every approval is posted; one raised during a Mattermost conversation also appears in that thread."
|
|
172
172
|
},
|
|
173
|
+
agentChannels: {
|
|
174
|
+
type: "array",
|
|
175
|
+
title: "Channel per agent",
|
|
176
|
+
description: "Agents whose approvals, decision cards and help requests go to their own channel instead of the approvals channel.",
|
|
177
|
+
items: { type: "object", properties: { agentId: { type: "string" }, channel: { type: "string" } }, required: ["agentId", "channel"] }
|
|
178
|
+
},
|
|
173
179
|
agentsMode: {
|
|
174
180
|
type: "string",
|
|
175
181
|
enum: ["all", "selected"],
|
package/dist/ui/index.js
CHANGED
|
@@ -122,6 +122,26 @@ function AccessList({ members, agents, grants, accounts, onChange }) {
|
|
|
122
122
|
] })
|
|
123
123
|
] });
|
|
124
124
|
}
|
|
125
|
+
function AgentChannels({ agents, channels, value, onChange }) {
|
|
126
|
+
const named = (id) => agents.find((a) => a.id === id)?.name ?? "A removed agent";
|
|
127
|
+
const free = agents.filter((a) => !value.some((r) => r.agentId === a.id));
|
|
128
|
+
const update = (agentId, channel) => onChange(value.map((r) => r.agentId === agentId ? { agentId, channel } : r));
|
|
129
|
+
const picker = (r) => channels.length ? /* @__PURE__ */ jsxs("select", { style: s.input, value: r.channel, onChange: (e) => update(r.agentId, e.target.value), children: [
|
|
130
|
+
channels.some((ch) => ch.name === r.channel) ? null : /* @__PURE__ */ jsx("option", { value: r.channel, children: r.channel || "Pick a channel" }),
|
|
131
|
+
channels.map((ch) => /* @__PURE__ */ jsx("option", { value: ch.name, children: ch.displayName }, ch.name))
|
|
132
|
+
] }) : /* @__PURE__ */ jsx("input", { style: s.input, value: r.channel, onChange: (e) => update(r.agentId, e.target.value.trim()), placeholder: "channel name, e.g. client-reporting" });
|
|
133
|
+
return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 6 }, children: [
|
|
134
|
+
value.map((r) => /* @__PURE__ */ jsxs("div", { style: s.row, children: [
|
|
135
|
+
/* @__PURE__ */ jsx("span", { style: { minWidth: 160 }, children: named(r.agentId) }),
|
|
136
|
+
picker(r),
|
|
137
|
+
/* @__PURE__ */ jsx("button", { style: { ...s.button, padding: "4px 9px" }, title: "Use the approvals channel", onClick: () => onChange(value.filter((x) => x.agentId !== r.agentId)), children: "\u2715" })
|
|
138
|
+
] }, r.agentId)),
|
|
139
|
+
free.length ? /* @__PURE__ */ jsxs("select", { style: { ...s.input, maxWidth: 320 }, value: "", onChange: (e) => e.target.value && onChange([...value, { agentId: e.target.value, channel: channels[0]?.name ?? "" }]), children: [
|
|
140
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "Add an agent\u2026" }),
|
|
141
|
+
free.map((a) => /* @__PURE__ */ jsx("option", { value: a.id, children: a.name }, a.id))
|
|
142
|
+
] }) : null
|
|
143
|
+
] });
|
|
144
|
+
}
|
|
125
145
|
function TokenHelp({ mattermostUrl }) {
|
|
126
146
|
const base = /^https?:\/\/\S+/.test(mattermostUrl.trim()) ? mattermostUrl.trim().replace(/\/+$/, "") : null;
|
|
127
147
|
const page = (path, label) => base ? /* @__PURE__ */ jsx("a", { href: `${base}${path}`, target: "_blank", rel: "noreferrer", children: label }) : /* @__PURE__ */ jsx("b", { children: label });
|
|
@@ -159,6 +179,7 @@ function validate(c) {
|
|
|
159
179
|
if (!url) errors.mattermostUrl = "Enter your Mattermost address.";
|
|
160
180
|
else if (!/^https?:\/\/[^\s/]+/.test(url)) errors.mattermostUrl = "Enter a full address starting with https://";
|
|
161
181
|
if (!c.botToken?.secretId) errors.botToken = "Choose the secret that holds the Mattermost admin token.";
|
|
182
|
+
if (Array.isArray(c.agentChannels) && c.agentChannels.some((r) => !String(r.channel ?? "").trim())) errors.agentChannels = "Pick a channel for every agent listed, or remove it.";
|
|
162
183
|
return errors;
|
|
163
184
|
}
|
|
164
185
|
function saveErrorText(raw) {
|
|
@@ -340,6 +361,15 @@ function SettingsPage({ context }) {
|
|
|
340
361
|
/* @__PURE__ */ jsx("option", { value: "", children: "None: only in the agent's thread" }),
|
|
341
362
|
setup.data.channels.map((ch) => /* @__PURE__ */ jsx("option", { value: ch.name, children: ch.displayName }, ch.name))
|
|
342
363
|
] }) : /* @__PURE__ */ jsx("input", { style: s.input, value: String(config.approvalsChannel ?? ""), onChange: (e) => set({ approvalsChannel: e.target.value }), placeholder: "channel name, e.g. approvals" }) }),
|
|
364
|
+
/* @__PURE__ */ jsx(Field, { label: "Channel per agent", error: fieldErrors.agentChannels, help: "Send one agent's approvals, decision cards and computer help requests to its own channel, for example a client reporting agent's drafts to #client-reporting. Only approvers can decide them, but everyone in that channel sees them. If its channel cannot be found, they go to the approvals channel with a note.", children: /* @__PURE__ */ jsx(
|
|
365
|
+
AgentChannels,
|
|
366
|
+
{
|
|
367
|
+
agents,
|
|
368
|
+
channels: setup.data?.channels ?? [],
|
|
369
|
+
value: Array.isArray(config.agentChannels) ? config.agentChannels : [],
|
|
370
|
+
onChange: (next) => set({ agentChannels: next })
|
|
371
|
+
}
|
|
372
|
+
) }),
|
|
343
373
|
/* @__PURE__ */ jsx(Field, { label: "Agent-to-agent turns", help: "Agents hand work to each other by mentioning one another in a thread. After this many agent messages in a row, they pause until a person replies.", children: /* @__PURE__ */ jsx(
|
|
344
374
|
"input",
|
|
345
375
|
{
|
package/dist/worker.js
CHANGED
|
@@ -44640,6 +44640,24 @@ import { createHmac as createHmac2 } from "node:crypto";
|
|
|
44640
44640
|
|
|
44641
44641
|
// src/approvals.ts
|
|
44642
44642
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
44643
|
+
function parseAgentChannels(value) {
|
|
44644
|
+
const out = {};
|
|
44645
|
+
if (Array.isArray(value)) for (const r of value) {
|
|
44646
|
+
if (typeof r?.agentId === "string" && typeof r.channel === "string" && r.channel.trim()) out[r.agentId] = r.channel.trim();
|
|
44647
|
+
}
|
|
44648
|
+
return out;
|
|
44649
|
+
}
|
|
44650
|
+
function decisionChannels(poster, perAgent, fallback) {
|
|
44651
|
+
return async (agentId) => {
|
|
44652
|
+
const own2 = agentId ? perAgent[agentId] : void 0;
|
|
44653
|
+
if (own2) {
|
|
44654
|
+
const id2 = await poster.channel(own2);
|
|
44655
|
+
if (id2) return { channelId: id2 };
|
|
44656
|
+
}
|
|
44657
|
+
const id = fallback ? await poster.channel(fallback) : null;
|
|
44658
|
+
return id ? { channelId: id, ...own2 ? { note: `Posted here because this agent's channel "${own2}" was not found.` } : {} } : null;
|
|
44659
|
+
};
|
|
44660
|
+
}
|
|
44643
44661
|
var TYPE_LABEL = {
|
|
44644
44662
|
request_board_approval: "Approval requested",
|
|
44645
44663
|
hire_agent: "Hire request",
|
|
@@ -44715,12 +44733,13 @@ var Approvals = class {
|
|
|
44715
44733
|
const targets = [];
|
|
44716
44734
|
const thread = a.requestedByAgentId ? d.activeThread(a.requestedByAgentId) : null;
|
|
44717
44735
|
if (thread) targets.push(thread);
|
|
44718
|
-
const
|
|
44719
|
-
if (
|
|
44736
|
+
const home = await d.channelFor(a.requestedByAgentId);
|
|
44737
|
+
if (home && home.channelId !== thread?.channelId) targets.push({ channelId: home.channelId, rootId: "", note: home.note });
|
|
44720
44738
|
if (!targets.length) return;
|
|
44721
44739
|
const { message: message2, props } = render(a, d.agentName(a.requestedByAgentId), d.actionUrl, d.key);
|
|
44722
44740
|
const postIds = [];
|
|
44723
|
-
for (const where of targets) postIds.push(await d.poster.post(a.requestedByAgentId, where, message2
|
|
44741
|
+
for (const { note, ...where } of targets) postIds.push(await d.poster.post(a.requestedByAgentId, where, note ? `${message2}
|
|
44742
|
+
_${note}_` : message2, props));
|
|
44724
44743
|
await d.store.set(`approval:${approvalId}`, { postIds });
|
|
44725
44744
|
}
|
|
44726
44745
|
/** `approval.decided` (here or in Paperclip): drop the buttons, show the outcome. */
|
|
@@ -44862,8 +44881,11 @@ var Cards = class {
|
|
|
44862
44881
|
}
|
|
44863
44882
|
async pass() {
|
|
44864
44883
|
const d = this.d;
|
|
44865
|
-
const
|
|
44866
|
-
|
|
44884
|
+
const homes = /* @__PURE__ */ new Map();
|
|
44885
|
+
const home = async (agentId) => {
|
|
44886
|
+
if (!homes.has(agentId)) homes.set(agentId, await d.channelFor(agentId));
|
|
44887
|
+
return homes.get(agentId) ?? null;
|
|
44888
|
+
};
|
|
44867
44889
|
const now = (d.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
44868
44890
|
let since = await d.store.get("cards:since");
|
|
44869
44891
|
if (!since) {
|
|
@@ -44882,8 +44904,12 @@ var Cards = class {
|
|
|
44882
44904
|
delete posted[c.id];
|
|
44883
44905
|
}
|
|
44884
44906
|
} else if (c.status === "pending" && new Date(c.createdAt).getTime() > Date.parse(since)) {
|
|
44885
|
-
const
|
|
44886
|
-
|
|
44907
|
+
const agentId = c.createdByAgentId ?? issue2.assigneeAgentId ?? null;
|
|
44908
|
+
const where = await home(agentId);
|
|
44909
|
+
if (!where) continue;
|
|
44910
|
+
const { message: message2, props } = renderCard(c, issue2, d.agentName(agentId), d);
|
|
44911
|
+
posted[c.id] = { postId: await d.poster.post(agentId, { channelId: where.channelId, rootId: "" }, where.note ? `${message2}
|
|
44912
|
+
_${where.note}_` : message2, props), issueId: issue2.id };
|
|
44887
44913
|
}
|
|
44888
44914
|
}
|
|
44889
44915
|
}
|
|
@@ -46394,7 +46420,7 @@ var route = (routeKey, method, path3) => ({ routeKey, method, path: path3, auth:
|
|
|
46394
46420
|
var manifest = {
|
|
46395
46421
|
id: "mattermost-bridge",
|
|
46396
46422
|
apiVersion: 1,
|
|
46397
|
-
version: "0.
|
|
46423
|
+
version: "0.19.0",
|
|
46398
46424
|
displayName: "Mattermost",
|
|
46399
46425
|
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.",
|
|
46400
46426
|
author: "Haseeb Azhar",
|
|
@@ -46480,6 +46506,12 @@ var manifest = {
|
|
|
46480
46506
|
title: "Approvals channel",
|
|
46481
46507
|
description: "Channel name (as in its URL, e.g. approvals) where every approval is posted; one raised during a Mattermost conversation also appears in that thread."
|
|
46482
46508
|
},
|
|
46509
|
+
agentChannels: {
|
|
46510
|
+
type: "array",
|
|
46511
|
+
title: "Channel per agent",
|
|
46512
|
+
description: "Agents whose approvals, decision cards and help requests go to their own channel instead of the approvals channel.",
|
|
46513
|
+
items: { type: "object", properties: { agentId: { type: "string" }, channel: { type: "string" } }, required: ["agentId", "channel"] }
|
|
46514
|
+
},
|
|
46483
46515
|
agentsMode: {
|
|
46484
46516
|
type: "string",
|
|
46485
46517
|
enum: ["all", "selected"],
|
|
@@ -46553,7 +46585,7 @@ var Tenant = class {
|
|
|
46553
46585
|
key = "";
|
|
46554
46586
|
approvers = /* @__PURE__ */ new Map();
|
|
46555
46587
|
paperclipUrl = "";
|
|
46556
|
-
|
|
46588
|
+
channelFor = async () => null;
|
|
46557
46589
|
timers = [];
|
|
46558
46590
|
agents() {
|
|
46559
46591
|
const companyId = this.companyId;
|
|
@@ -46683,6 +46715,8 @@ var Tenant = class {
|
|
|
46683
46715
|
};
|
|
46684
46716
|
const approvers = parseApprovers(config2.approvers);
|
|
46685
46717
|
const approvalsChannel = typeof config2.approvalsChannel === "string" ? config2.approvalsChannel.trim() : "";
|
|
46718
|
+
const agentChannels = parseAgentChannels(config2.agentChannels);
|
|
46719
|
+
const channelFor = decisionChannels(this.poster(manager, roster), agentChannels, approvalsChannel);
|
|
46686
46720
|
const agentName = (agentId) => agentId && roster.roster().byAgentId(agentId)?.agentName || "Paperclip";
|
|
46687
46721
|
const approvals = paperclipUrl ? new Approvals({
|
|
46688
46722
|
poster: this.poster(manager, roster),
|
|
@@ -46695,10 +46729,10 @@ var Tenant = class {
|
|
|
46695
46729
|
store,
|
|
46696
46730
|
actionUrl,
|
|
46697
46731
|
key,
|
|
46698
|
-
|
|
46732
|
+
channelFor,
|
|
46699
46733
|
approvers
|
|
46700
46734
|
}) : null;
|
|
46701
|
-
const cards = paperclipUrl && approvalsChannel ? new Cards({
|
|
46735
|
+
const cards = paperclipUrl && (approvalsChannel || Object.keys(agentChannels).length) ? new Cards({
|
|
46702
46736
|
poster: this.poster(manager, roster),
|
|
46703
46737
|
openIssues: async () => (await Promise.all(OPEN_STATUSES.map((status) => ctx.issues.list({ companyId, status, limit: 500 })))).flat(),
|
|
46704
46738
|
cards: async (issueId) => await ctx.issues.listInteractions(issueId, companyId),
|
|
@@ -46713,7 +46747,7 @@ var Tenant = class {
|
|
|
46713
46747
|
actionUrl,
|
|
46714
46748
|
key,
|
|
46715
46749
|
paperclipUrl,
|
|
46716
|
-
|
|
46750
|
+
channelFor,
|
|
46717
46751
|
approvers
|
|
46718
46752
|
}) : null;
|
|
46719
46753
|
const poster = this.poster(manager, roster);
|
|
@@ -46736,7 +46770,8 @@ var Tenant = class {
|
|
|
46736
46770
|
askApproval: async (req) => {
|
|
46737
46771
|
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 });
|
|
46738
46772
|
const { message: message2, props } = requestCard(req, actionUrl, key);
|
|
46739
|
-
const
|
|
46773
|
+
const home = await channelFor(req.agentId);
|
|
46774
|
+
const targets = [req.origin, home && home.channelId !== req.origin?.channelId ? { channelId: home.channelId, rootId: "" } : null];
|
|
46740
46775
|
const postIds = [];
|
|
46741
46776
|
for (const where of targets) if (where) postIds.push(await poster.post(req.agentId, where, message2, props));
|
|
46742
46777
|
await store.set(`reqcard:${req.id}`, postIds);
|
|
@@ -46760,7 +46795,7 @@ var Tenant = class {
|
|
|
46760
46795
|
task: async (issueId) => await ctx.issues.get(issueId, companyId),
|
|
46761
46796
|
comments: async (issueId) => (await ctx.issues.listComments(issueId, companyId)).map((c) => ({ id: c.id, body: c.body, authorAgentId: c.authorAgentId }))
|
|
46762
46797
|
});
|
|
46763
|
-
Object.assign(this, { manager, bots: roster, bridge, approvals, cards, actions, work, key, approvers, paperclipUrl,
|
|
46798
|
+
Object.assign(this, { manager, bots: roster, bridge, approvals, cards, actions, work, key, approvers, paperclipUrl, channelFor });
|
|
46764
46799
|
this.status = `connecting to ${url2} as @${manager.botUsername}`;
|
|
46765
46800
|
detached(() => {
|
|
46766
46801
|
manager.connect(onPosted);
|
|
@@ -46813,7 +46848,7 @@ var Tenant = class {
|
|
|
46813
46848
|
async computerNeedsPerson(p) {
|
|
46814
46849
|
if (!this.manager || !this.bots || !this.bridge || !p.agentId) return;
|
|
46815
46850
|
const poster = this.poster(this.manager, this.bots);
|
|
46816
|
-
const where = this.bridge.activeThread(p.agentId) ??
|
|
46851
|
+
const where = this.bridge.activeThread(p.agentId) ?? await this.channelFor(p.agentId).then((home) => home ? { channelId: home.channelId, rootId: "" } : null);
|
|
46817
46852
|
if (!where) return;
|
|
46818
46853
|
const link = this.paperclipUrl && p.path ? `${this.paperclipUrl}${p.path}` : null;
|
|
46819
46854
|
const on = p.url ? ` on ${p.url}` : "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperclip-plugin-mattermost",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|