paperclip-plugin-mattermost 0.13.0 → 0.15.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
@@ -13,10 +13,11 @@ Your [Paperclip](https://github.com/paperclipai/paperclip) agents as Mattermost
13
13
  - **Confirmations and suggested tasks** get Accept / Reject buttons.
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
- - **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. `GET /api/plugins/mattermost-bridge/api/mm/help` tells an agent how.
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
17
  - **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.
18
18
  - **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.
19
19
  - **Ask first.** Some actions wait for an approver to click Approve: `@channel`, `@all` and `@here`, public channels, and adding people to a channel the agent didn't create. Agents can't change or archive channels they didn't create, remove anyone, or touch team and server settings.
20
+ - **Agent computer requests.** With the Agent computer plugin installed, an agent that needs a person in its browser (a login, 2FA, a password) posts in the thread it is working in, or the approvals channel, with a link to take over.
20
21
  - **One install, many companies.** Each Paperclip company connects its own Mattermost, with its own settings, bots and state.
21
22
 
22
23
  ## Requirements
@@ -82,7 +83,7 @@ PAPERCLIP_REF=<commit> node build.mjs # build for another Paperclip version
82
83
  Bump `version` in `package.json` and `src/manifest.ts`, commit, then tag and push:
83
84
 
84
85
  ```bash
85
- git tag -a v0.13.0 -m v0.13.0 && git push origin main v0.13.0
86
+ git tag -a v0.15.0 -m v0.15.0 && git push origin main v0.15.0
86
87
  ```
87
88
 
88
89
  GitHub Actions builds, tests and publishes to npm through npm trusted publishing, so no npm token is involved.
package/dist/manifest.js CHANGED
@@ -1,9 +1,80 @@
1
+ // src/help.ts
2
+ var HELP = `# Working in Mattermost
3
+
4
+ You have your own Mattermost account (a bot). Everything below runs as you, in your company's
5
+ Mattermost, and you can only see and use channels you are a member of. Use it whenever you need
6
+ to reach a person or another agent in Mattermost, including from a task or a scheduled run: to
7
+ ping someone, DM them, or post in a channel. Call it with your Paperclip API credentials:
8
+
9
+ B="\${PAPERCLIP_API_URL%/}"; B="\${B%/api}/api/plugins/mattermost-bridge/api"
10
+ curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" "$B/mm/channels"
11
+ curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" -H "content-type: application/json" \\
12
+ -d '{"person":"username","text":"..."}' "$B/mm/dm"
13
+
14
+ Answers are JSON. A 202 with "waiting_for_approval" means a person has been asked to approve;
15
+ tell whoever you are working with and carry on. A 403 says why something is not allowed.
16
+
17
+ ## How to work
18
+
19
+ - **Pick the size of the work.** A quick question: answer in the thread. A task: create a Paperclip
20
+ task (assign it to yourself or delegate it) and say so in the thread with its identifier. An
21
+ investigation, incident or project, or anything involving several agents or more than a day:
22
+ create the task, then open a work channel for it (below), with the person who asked and whoever
23
+ you need. Name it for what it is: "inc checkout 5xx", "inv q3 conversion drop", "proj crm migration".
24
+ - **Announce a work channel where the request came in**, in one message: what it is for, who you
25
+ pulled in, and that you will report back there. When the task is done or cancelled the plugin
26
+ archives the channel and posts the outcome back where you asked; you do not need to.
27
+ - **Hand work to another agent by mentioning it** (@cto) in a channel or thread it is in; invite it
28
+ first if it is not. Give it the context it needs in that message. It replies in the thread.
29
+ Mention an agent only when you want it to act: every mention starts it.
30
+ - **Say something before going quiet.** If an answer will take a while, post one line ("On it,
31
+ checking what shipped this morning") and then do the work.
32
+ - Do not write @channel, @all or @here unless everyone in the channel must be notified: it needs
33
+ approval.
34
+
35
+ ## Calls
36
+
37
+ Reading (GET):
38
+ - /mm/channels channels you are in
39
+ - /mm/channels/{channel}/posts?limit=30 recent posts in a channel (oldest first)
40
+ - /mm/posts/{postId}/thread a whole thread
41
+ - /mm/channels/{channel}/members who is in a channel (people and agents)
42
+ - /mm/search?q=words search messages in your channels
43
+ - /mm/people?q=name find people and agents by name
44
+ - /mm/requests/{id} an approval request you made
45
+
46
+ Talking (POST, JSON body):
47
+ - /mm/post {"channel": "...", "text": "...", "threadId": "optional post id to reply to"}
48
+ - /mm/react {"postId": "...", "emoji": "white_check_mark"}
49
+ - /mm/dm {"person": "username", "text": "..."} only people allowed to talk to you
50
+
51
+ Channels (POST, JSON body):
52
+ - /mm/channels {"name": "inc checkout 5xx", "purpose": "...", "people": ["username"],
53
+ "agents": ["cto"], "taskId": "BAD-123 or task id", "private": true}
54
+ Private by default. A public channel needs approval. With taskId, the channel is tied to the
55
+ task: its comments and status show up there, and it is archived when the task closes.
56
+ - /mm/channels/{channel}/invite {"people": [...], "agents": [...]} free on channels you created
57
+ - /mm/channels/{channel}/update {"purpose": "...", "header": "...", "name": "..."} yours only
58
+ - /mm/channels/{channel}/archive {} yours only
59
+
60
+ {channel} is a channel's id, its URL name (inc-checkout-5xx) or its display name.
61
+ `;
62
+ var SKILL = `---
63
+ name: mattermost
64
+ description: >
65
+ Use to talk to people and agents in Mattermost as your own bot: DM someone, post or reply in
66
+ a channel, read and search channels you are in, hand work to another agent by mentioning it,
67
+ and open a work channel for an investigation, incident or project tied to a Paperclip task.
68
+ ---
69
+
70
+ ${HELP}`;
71
+
1
72
  // src/manifest.ts
2
73
  var route = (routeKey, method, path) => ({ routeKey, method, path, auth: "agent", capability: "api.routes.register" });
3
74
  var manifest = {
4
75
  id: "mattermost-bridge",
5
76
  apiVersion: 1,
6
- version: "0.13.0",
77
+ version: "0.15.0",
7
78
  displayName: "Mattermost",
8
79
  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.",
9
80
  author: "Haseeb Azhar",
@@ -29,7 +100,8 @@ var manifest = {
29
100
  "issue.comments.create",
30
101
  "issue.comments.create_human_attributed",
31
102
  "issue.comments.read",
32
- "api.routes.register"
103
+ "api.routes.register",
104
+ "skills.managed"
33
105
  ],
34
106
  entrypoints: { worker: "./dist/worker.js", ui: "./dist/ui" },
35
107
  // Replaces the generated form: approvers picked from members, URL detected and checked.
@@ -53,6 +125,8 @@ var manifest = {
53
125
  route("update-channel", "POST", "/mm/channels/:channel/update"),
54
126
  route("archive", "POST", "/mm/channels/:channel/archive")
55
127
  ],
128
+ // Installed in each company's skill library; the settings page adds it to every agent with a bot.
129
+ skills: [{ skillKey: "mattermost", displayName: "Mattermost", slug: "mattermost", description: "Talk to people and agents in Mattermost as your own bot.", markdown: SKILL }],
56
130
  webhooks: [
57
131
  { endpointKey: "actions", displayName: "Mattermost buttons", description: "Approve / Reject clicks on approval posts. Every click carries a signature the plugin checks." }
58
132
  ],
package/dist/ui/index.js CHANGED
@@ -203,6 +203,8 @@ function SettingsPage({ context }) {
203
203
  const [saved, setSaved] = useState(false);
204
204
  const [check, setCheck] = useState(null);
205
205
  const verify = usePluginAction("verify-url");
206
+ const skill = usePluginAction("skill");
207
+ const [skillNote, setSkillNote] = useState(null);
206
208
  useEffect(() => {
207
209
  if (!companyId) return;
208
210
  Promise.all([
@@ -264,8 +266,22 @@ function SettingsPage({ context }) {
264
266
  else next.add(id);
265
267
  setSelected(next);
266
268
  };
269
+ async function giveSkill(targets) {
270
+ try {
271
+ const { key } = await skill({ companyId });
272
+ if (!key) return "The Mattermost skill is not installed yet; save again in a minute.";
273
+ const failed = [];
274
+ for (const a of targets) {
275
+ await api(`/api/agents/${a.id}/skills/sync`, { method: "POST", body: JSON.stringify({ mode: "add", desiredSkills: [key] }) }).catch(() => failed.push(a.name));
276
+ }
277
+ return failed.length ? `Could not give the Mattermost skill to ${failed.join(", ")}; add it on their Skills tab.` : null;
278
+ } catch (e) {
279
+ return `Could not give agents the Mattermost skill: ${String(e?.message ?? e).slice(0, 200)}`;
280
+ }
281
+ }
267
282
  async function save() {
268
283
  setSaveError(null);
284
+ setSkillNote(null);
269
285
  const errors = validate(config);
270
286
  if (agentsMode === "selected" && !pickedAgents.size) errors.agents = "Tick at least one agent, or choose All agents.";
271
287
  if (access === "selected" && !grants.size) errors.access = "Tick at least one person, or choose Everyone.";
@@ -283,6 +299,7 @@ function SettingsPage({ context }) {
283
299
  await api(`/api/plugins/${PLUGIN}/config`, { method: "POST", body: JSON.stringify({ companyId, configJson: next }) });
284
300
  setConfig(next);
285
301
  setSaved(true);
302
+ setSkillNote(await giveSkill(agentsMode === "selected" ? agents.filter((a) => pickedAgents.has(a.id)) : agents));
286
303
  } catch (e) {
287
304
  setSaveError(saveErrorText(String(e?.message ?? e)));
288
305
  } finally {
@@ -334,7 +351,7 @@ function SettingsPage({ context }) {
334
351
  onChange: (e) => set({ agentTurnLimit: Math.min(100, Math.max(1, Math.round(Number(e.target.value) || 10))) })
335
352
  }
336
353
  ) }),
337
- /* @__PURE__ */ jsxs(Field, { label: "Agents in Mattermost", error: fieldErrors.agents, help: "Each agent shown here gets its own Mattermost bot. Unticking an agent switches its bot off but keeps it, with its history, for when you tick it again.", children: [
354
+ /* @__PURE__ */ jsxs(Field, { label: "Agents in Mattermost", error: fieldErrors.agents, help: "Each agent shown here gets its own Mattermost bot, and saving gives it the Mattermost skill so it can message people from any run. Unticking an agent switches its bot off but keeps it, with its history, for when you tick it again.", children: [
338
355
  /* @__PURE__ */ jsx(
339
356
  Modes,
340
357
  {
@@ -380,7 +397,8 @@ function SettingsPage({ context }) {
380
397
  /* @__PURE__ */ jsxs("div", { style: s.row, children: [
381
398
  /* @__PURE__ */ jsx("button", { style: s.primary, onClick: save, disabled: saving, children: saving ? "Saving\u2026" : "Save" }),
382
399
  saved ? /* @__PURE__ */ jsx("span", { style: s.ok, children: "Saved" }) : null,
383
- saveError ? /* @__PURE__ */ jsx("span", { style: s.bad, children: saveError }) : null
400
+ saveError ? /* @__PURE__ */ jsx("span", { style: s.bad, children: saveError }) : null,
401
+ skillNote ? /* @__PURE__ */ jsx("span", { style: s.bad, children: skillNote }) : null
384
402
  ] })
385
403
  ] });
386
404
  }
package/dist/worker.js CHANGED
@@ -45426,10 +45426,14 @@ _~${name} is archived._`
45426
45426
  var HELP = `# Working in Mattermost
45427
45427
 
45428
45428
  You have your own Mattermost account (a bot). Everything below runs as you, in your company's
45429
- Mattermost, and you can only see and use channels you are a member of. Call these with your
45430
- Paperclip API credentials, the same way you call the Paperclip API:
45429
+ Mattermost, and you can only see and use channels you are a member of. Use it whenever you need
45430
+ to reach a person or another agent in Mattermost, including from a task or a scheduled run: to
45431
+ ping someone, DM them, or post in a channel. Call it with your Paperclip API credentials:
45431
45432
 
45432
- <your Paperclip API URL>/api/plugins/mattermost-bridge/api/mm/...
45433
+ B="\${PAPERCLIP_API_URL%/}"; B="\${B%/api}/api/plugins/mattermost-bridge/api"
45434
+ curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" "$B/mm/channels"
45435
+ curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" -H "content-type: application/json" \\
45436
+ -d '{"person":"username","text":"..."}' "$B/mm/dm"
45433
45437
 
45434
45438
  Answers are JSON. A 202 with "waiting_for_approval" means a person has been asked to approve;
45435
45439
  tell whoever you are working with and carry on. A 403 says why something is not allowed.
@@ -45479,6 +45483,15 @@ Channels (POST, JSON body):
45479
45483
 
45480
45484
  {channel} is a channel's id, its URL name (inc-checkout-5xx) or its display name.
45481
45485
  `;
45486
+ var SKILL = `---
45487
+ name: mattermost
45488
+ description: >
45489
+ Use to talk to people and agents in Mattermost as your own bot: DM someone, post or reply in
45490
+ a channel, read and search channels you are in, hand work to another agent by mentioning it,
45491
+ and open a work channel for an investigation, incident or project tied to a Paperclip task.
45492
+ ---
45493
+
45494
+ ${HELP}`;
45482
45495
 
45483
45496
  // src/worker.ts
45484
45497
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -46198,7 +46211,7 @@ var route = (routeKey, method, path3) => ({ routeKey, method, path: path3, auth:
46198
46211
  var manifest = {
46199
46212
  id: "mattermost-bridge",
46200
46213
  apiVersion: 1,
46201
- version: "0.13.0",
46214
+ version: "0.15.0",
46202
46215
  displayName: "Mattermost",
46203
46216
  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.",
46204
46217
  author: "Haseeb Azhar",
@@ -46224,7 +46237,8 @@ var manifest = {
46224
46237
  "issue.comments.create",
46225
46238
  "issue.comments.create_human_attributed",
46226
46239
  "issue.comments.read",
46227
- "api.routes.register"
46240
+ "api.routes.register",
46241
+ "skills.managed"
46228
46242
  ],
46229
46243
  entrypoints: { worker: "./dist/worker.js", ui: "./dist/ui" },
46230
46244
  // Replaces the generated form: approvers picked from members, URL detected and checked.
@@ -46248,6 +46262,8 @@ var manifest = {
46248
46262
  route("update-channel", "POST", "/mm/channels/:channel/update"),
46249
46263
  route("archive", "POST", "/mm/channels/:channel/archive")
46250
46264
  ],
46265
+ // Installed in each company's skill library; the settings page adds it to every agent with a bot.
46266
+ skills: [{ skillKey: "mattermost", displayName: "Mattermost", slug: "mattermost", description: "Talk to people and agents in Mattermost as your own bot.", markdown: SKILL }],
46251
46267
  webhooks: [
46252
46268
  { endpointKey: "actions", displayName: "Mattermost buttons", description: "Approve / Reject clicks on approval posts. Every click carries a signature the plugin checks." }
46253
46269
  ],
@@ -46350,6 +46366,8 @@ var Tenant = class {
46350
46366
  work = null;
46351
46367
  key = "";
46352
46368
  approvers = /* @__PURE__ */ new Map();
46369
+ paperclipUrl = "";
46370
+ approvalsChannel = "";
46353
46371
  timers = [];
46354
46372
  agents() {
46355
46373
  const companyId = this.companyId;
@@ -46539,7 +46557,7 @@ var Tenant = class {
46539
46557
  task: async (issueId) => await ctx.issues.get(issueId, companyId),
46540
46558
  comments: async (issueId) => (await ctx.issues.listComments(issueId, companyId)).map((c) => ({ id: c.id, body: c.body, authorAgentId: c.authorAgentId }))
46541
46559
  });
46542
- Object.assign(this, { manager, bots: roster, bridge, approvals, cards, actions, work, key, approvers });
46560
+ Object.assign(this, { manager, bots: roster, bridge, approvals, cards, actions, work, key, approvers, paperclipUrl, approvalsChannel });
46543
46561
  this.status = `connecting to ${url2} as @${manager.botUsername}`;
46544
46562
  detached(() => {
46545
46563
  manager.connect(onPosted);
@@ -46551,6 +46569,7 @@ var Tenant = class {
46551
46569
  });
46552
46570
  void sync();
46553
46571
  this.timers.push(setInterval(sync, SYNC_EVERY_MS));
46572
+ void ensureSkill(companyId).catch((err) => warn("could not install the Mattermost skill", { err: errText(err) }));
46554
46573
  if (cards) {
46555
46574
  const poll = () => cards.poll().catch((err) => warn("decision cards poll failed", { err: errText(err) }));
46556
46575
  this.timers.push(setTimeout(poll, 2e4), setInterval(poll, CARDS_EVERY_MS));
@@ -46585,6 +46604,17 @@ var Tenant = class {
46585
46604
  if (!this.actions) return { status: 503, body: { error: `Mattermost is not connected for this company (${this.status})` } };
46586
46605
  return this.actions.handle(agentId, routeKey, params, input2);
46587
46606
  }
46607
+ /** The Agent computer plugin: an agent wants a person in its browser. Posted where the agent is working. */
46608
+ async computerNeedsPerson(p) {
46609
+ if (!this.manager || !this.bots || !this.bridge || !p.agentId) return;
46610
+ const poster = this.poster(this.manager, this.bots);
46611
+ const where = this.bridge.activeThread(p.agentId) ?? (this.approvalsChannel ? await poster.channel(this.approvalsChannel).then((id) => id ? { channelId: id, rootId: "" } : null) : null);
46612
+ if (!where) return;
46613
+ const link = this.paperclipUrl && p.path ? `${this.paperclipUrl}${p.path}` : null;
46614
+ const on = p.url ? ` on ${p.url}` : "";
46615
+ const message2 = p.kind === "password" ? `:key: I ${p.reason ?? "need a password"}${on}. ${link ? `[Enter it here](${link})` : "Enter it from Computers in Paperclip"}: it goes straight into the page, and I never see it.` : `:raising_hand: I need a person in my browser${on}: ${p.reason ?? "I am stuck"}. ${link ? `[Take over](${link})` : "Take over from Computers in Paperclip"}, then hand it back.`;
46616
+ await poster.post(p.agentId, where, message2, { ...FROM_PAPERCLIP });
46617
+ }
46588
46618
  async notify(req, notice) {
46589
46619
  if (notice && req.user_id && req.channel_id) {
46590
46620
  await this.manager?.request("POST", "/posts/ephemeral", { user_id: req.user_id, post: { channel_id: req.channel_id, message: notice } }).catch(() => {
@@ -46675,6 +46705,11 @@ function configure(companyId, config2) {
46675
46705
  });
46676
46706
  return run;
46677
46707
  }
46708
+ async function ensureSkill(companyId) {
46709
+ let r = await ctx.skills.managed.reconcile("mattermost", companyId);
46710
+ if (r.skill && r.defaultDrift?.changedFiles.length) r = await ctx.skills.managed.reset("mattermost", companyId);
46711
+ return r.skill?.key ?? null;
46712
+ }
46678
46713
  async function saveBoot(companyId, config2) {
46679
46714
  const all = await ctx.state.get(BOOT) ?? {};
46680
46715
  all[companyId] = config2;
@@ -46704,6 +46739,10 @@ var plugin = definePlugin({
46704
46739
  }
46705
46740
  return { detectedUrl: await detectPublicUrl(), connected: Boolean(manager), status: tenant?.status ?? null, versionNote, channels, accounts };
46706
46741
  });
46742
+ ctx.actions.register("skill", async (params) => {
46743
+ if (typeof params.companyId !== "string") throw new Error("companyId is required");
46744
+ return { key: await ensureSkill(params.companyId) };
46745
+ });
46707
46746
  ctx.actions.register("verify-url", async (params) => verifyUrl(String(params.url ?? ""), PLUGIN_KEY));
46708
46747
  const quietly = (what, fn) => fn().catch((err) => ctx.logger.warn(what, { err: errText(err) }));
46709
46748
  ctx.events.on("approval.created", (e) => quietly("could not post approval", async () => {
@@ -46716,6 +46755,9 @@ var plugin = definePlugin({
46716
46755
  ctx.events.on("issue.updated", (e) => quietly("could not follow a task update", async () => {
46717
46756
  if (e.entityId) await tenants.get(e.companyId)?.work?.updated(e.entityId);
46718
46757
  }));
46758
+ ctx.events.on("plugin.agent-computer.needs-person", (e) => quietly("could not post a computer help request", async () => {
46759
+ await tenants.get(e.companyId)?.computerNeedsPerson(e.payload ?? {});
46760
+ }));
46719
46761
  ctx.events.on("approval.decided", (e) => quietly("could not update approval", async () => {
46720
46762
  if (e.entityId) await tenants.get(e.companyId)?.approvals?.decided(e.entityId);
46721
46763
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-plugin-mattermost",
3
- "version": "0.13.0",
3
+ "version": "0.15.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",