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 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.18.3 -m v0.18.3 && git push origin main v0.18.3
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.18.3",
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 channelId = d.fallbackChannel ? await d.poster.channel(d.fallbackChannel) : null;
44719
- if (channelId && channelId !== thread?.channelId) targets.push({ channelId, rootId: "" });
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, props));
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 channelId = await d.poster.channel(d.channel);
44866
- if (!channelId) return;
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 { message: message2, props } = renderCard(c, issue2, d.agentName(c.createdByAgentId ?? issue2.assigneeAgentId ?? null), d);
44886
- posted[c.id] = { postId: await d.poster.post(c.createdByAgentId ?? issue2.assigneeAgentId ?? null, { channelId, rootId: "" }, message2, props), issueId: issue2.id };
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.18.3",
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
- approvalsChannel = "";
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
- fallbackChannel: approvalsChannel,
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
- channel: approvalsChannel,
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 targets = [req.origin, approvalsChannel ? await poster.channel(approvalsChannel).then((id) => id && id !== req.origin?.channelId ? { channelId: id, rootId: "" } : null) : null];
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, approvalsChannel });
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) ?? (this.approvalsChannel ? await poster.channel(this.approvalsChannel).then((id) => id ? { channelId: id, rootId: "" } : null) : null);
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.18.3",
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",