Skip to content

Commit e08279b

Browse files
refactor: send directly through gbot
1 parent 81da34f commit e08279b

13 files changed

Lines changed: 117 additions & 308 deletions

File tree

‎README.md‎

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Plugin Library (sidecar browse UI)
22

3-
Three-pane explorer for the Plugin Catalog: plugins → skills / agents / rules → the full `SKILL.md` rendered in a reading pane, sourced live from the on-disk plugin cache. "Apply to a bot" picks a target from the live `gbot` roster and confirms before anything is queued.
3+
Three-pane explorer for the Plugin Catalog: plugins → skills / agents / rules → the full `SKILL.md` rendered in a reading pane, sourced live from the on-disk plugin cache. "Send to a bot" picks a target from the live `gbot` roster and confirms before sending it directly.
44

55
## Tour
66

@@ -18,11 +18,7 @@ Click a skill and the full `SKILL.md` opens in the reading pane. Front matter be
1818

1919
![A reference file](screens/04-reference-tab.png)
2020

21-
"Apply to a bot" pulls the live roster from `gbot` (bots and groups, with their avatars) and ends in one sentence you confirm. Every `/api/apply` status is turned into plain language; an install that isn't there yet is its own explicit step.
22-
23-
![Pick a bot](screens/05-pick-a-bot.png)
24-
25-
![Confirm](screens/06-confirm.png)
21+
"Send to a bot" pulls the live roster from `gbot` (bots and groups, with their avatars), asks for confirmation, then calls `gbot send` directly.
2622

2723
Every installed plugin gets the same treatment, not only pstack.
2824

@@ -96,11 +92,11 @@ node artifact/scripts/plugin-library.mjs status --json
9692
node artifact/scripts/plugin-library.mjs stop
9793
```
9894

99-
The bundled skill also tells agents how to answer skill questions from the read-only API without opening a window, and forbids them from calling `POST /api/apply` — applying to a bot stays a user click.
95+
The bundled skill also tells agents how to answer skill questions from the read-only API without opening a window. Sending to a bot stays a user-confirmed click.
10096

10197
`npm test` builds the generated artifact, then runs the `node --test` suite
10298
(installer lifecycle, front matter, cache dedupe, path containment,
103-
apply-status mapping, and an HTTP smoke test against a throwaway cache).
99+
direct `gbot send`, and an HTTP smoke test against a throwaway cache).
104100

105101
## Browse API (read-only)
106102

@@ -113,24 +109,17 @@ apply-status mapping, and an HTTP smoke test against a throwaway cache).
113109

114110
Override the cache root with `CURSOR_PLUGIN_CACHE`, the CLI with `GBOT_BIN`.
115111

116-
## Apply contract (Plugin Applier)
112+
## Send contract
117113

118-
`POST /api/apply` body:
114+
The UI calls `POST /api/send` only after the user confirms:
119115

120116
```json
121-
{ "plugin_id": "…", "bot_ref": "…", "skill_id?": "…", "confirmed": true, "mode?": "profile_bake|nudge_send" }
117+
{ "plugin_id": "…", "bot_ref": "…", "skill_id?": "…" }
122118
```
123119

124-
| Result | When |
125-
|--------|------|
126-
| `needs_install_confirm` | not installed, `confirmed` not true |
127-
| `install_queued` | not installed, `confirmed: true` → Applier drains → `InstallPlugin` (account only) |
128-
| `missing_attach_api` | installed; no per-bot skill attach yet (default) |
129-
| `profile_bake_queued` / `nudge_send_queued` | only if `mode` set + confirmed (opt-in; never silent) |
130-
131-
Also: `GET /api/apply/pending`, `POST /api/apply/ack` `{ "id" }`.
132-
133-
Explorer UI confirm counts as the user's Explorer confirm. No silent fleet bot writes.
120+
The server verifies the plugin and optional skill against its catalog, then
121+
runs `gbot send <target> <message> --json`. There is no queue, Applier, install
122+
branch, or per-bot attach fallback.
134123

135124
## Data
136125

‎agent-bundle.config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default defineConfig({
1212
plugin: {
1313
description:
1414
'Browse installed and marketplace plugins, read every skill in full, and hand one to a Grok Bot.',
15+
logo: './assets/logo.svg',
1516
name: 'plugin-library',
1617
},
1718
runtime: { node: '22.19.0' },

‎lib/bots.js‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ function runGbot(args) {
3333
});
3434
}
3535

36+
/** Send one confirmed instruction directly through gbot. */
37+
async function sendToBot(target, message) {
38+
const result = await runGbot(["send", target, message]);
39+
if (!result || typeof result !== "object") throw new Error("gbot send: expected an object");
40+
return result;
41+
}
42+
3643
/** Boundary check on gbot output: a shape change surfaces as a 502, not an empty roster. */
3744
function shapeList(list, what) {
3845
if (!Array.isArray(list)) throw new Error(`gbot ${what} list: expected an array`);
@@ -78,4 +85,4 @@ async function listBots({ force = false } = {}) {
7885
}
7986
}
8087

81-
module.exports = { listBots };
88+
module.exports = { listBots, sendToBot };

‎public/app.js‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
els.readerStatus.textContent = msg;
7272
els.readerStatus.className = `status ${cls}`;
7373
}
74-
function onApplyResult(msg, tone) {
74+
function onSendResult(msg, tone) {
7575
toast(msg, tone);
7676
setStatus(msg, tone);
7777
}
@@ -171,7 +171,7 @@
171171
if (p.skill_count) chips.push(`<span class="chip"><b>${p.skill_count}</b> skills</span>`);
172172
if (p.connector_count) chips.push(`<span class="chip"><b>${p.connector_count}</b> connectors</span>`);
173173
}
174-
chips.push(`<button class="chip link" id="applyPlugin">${p.installed ? "Apply whole plugin to a bot…" : "Install for a bot…"}</button>`);
174+
chips.push(`<button class="chip link" id="sendPlugin">Send plugin to a bot…</button>`);
175175

176176
let html = `<div class="hero">
177177
${logoHtml(p, "lg")}
@@ -210,7 +210,7 @@
210210
els.main.scrollTop = 0;
211211
wireLogos(els.main);
212212
els.main.querySelectorAll(".card[data-doc]").forEach((c) => c.addEventListener("click", () => navigate(c.dataset.doc)));
213-
$("applyPlugin").addEventListener("click", () => Picker.openPicker({ root: els.picker, plugin: p, skill: null, onResult: onApplyResult }));
213+
$("sendPlugin").addEventListener("click", () => Picker.openPicker({ root: els.picker, plugin: p, skill: null, onResult: onSendResult }));
214214
markCurrentCard();
215215
}
216216

@@ -288,8 +288,8 @@
288288

289289
const resolved = p.local ? docTabs(p, kind, id) : null;
290290
const skill = resolved ? resolved.skill : null;
291-
els.readerApply.textContent = skill ? `Apply “${skill.name}” to a bot…` : "Apply plugin to a bot…";
292-
els.readerApply.onclick = () => Picker.openPicker({ root: els.picker, plugin: p, skill, onResult: onApplyResult });
291+
els.readerApply.textContent = skill ? `Send “${skill.name}” to a bot…` : "Send plugin to a bot…";
292+
els.readerApply.onclick = () => Picker.openPicker({ root: els.picker, plugin: p, skill, onResult: onSendResult });
293293
if (!resolved) {
294294
els.readerBody.innerHTML = `<div class="notice" style="margin-top:16px">Not found in the local plugin cache.</div>`;
295295
return;

‎public/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ <h1>Plugin Library</h1>
4040
<div class="reader-body" id="readerBody"></div>
4141
<div class="reader-foot">
4242
<span class="status" id="readerStatus"></span>
43-
<button type="button" class="btn" id="readerApply">Apply to a bot…</button>
43+
<button type="button" class="btn" id="readerApply">Send to a bot…</button>
4444
</div>
4545
</aside>
4646
</div>

‎public/picker.js‎

Lines changed: 13 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
/* global Markdown */
22
/**
3-
* Bot picker + apply flow. One state object, one render() that switches on
4-
* `stage`, and one pure function that maps an /api/apply response to the
5-
* next stage. `confirmed: true` is only ever sent from a stage whose button
6-
* the user just clicked.
3+
* Bot picker + direct send flow. One state object and one render() switch.
4+
* The selected target receives a message only after the user confirms.
75
*/
86
(function (global) {
97
"use strict";
@@ -35,27 +33,6 @@
3533
return `<span class="avatar ${esc(shape)}" style="${style}">${esc(initials(b.name))}</span>`;
3634
}
3735

38-
/** /api/apply response → next stage. Pure; the only place the contract is interpreted. */
39-
function nextStage(json) {
40-
const status = json.status || json.error;
41-
switch (status) {
42-
case "needs_install_confirm":
43-
return { stage: "confirm-install" };
44-
case "needs_mode_confirm":
45-
return { stage: "confirm-nudge" };
46-
case "missing_attach_api":
47-
return { stage: "offer-nudge" };
48-
case "install_queued":
49-
return { stage: "done", tone: "ok", message: "Install queued for the Plugin Applier." };
50-
case "nudge_send_queued":
51-
return { stage: "done", tone: "ok", message: "Nudge queued for the Plugin Applier." };
52-
case "profile_bake_queued":
53-
return { stage: "done", tone: "ok", message: "Profile bake queued for the Plugin Applier." };
54-
default:
55-
return { stage: "done", tone: "err", message: json.message || `Unexpected response: ${status}` };
56-
}
57-
}
58-
5936
/**
6037
* @param {object} opts
6138
* @param {HTMLElement} opts.root container to render into
@@ -100,18 +77,20 @@
10077
render();
10178
}
10279

103-
async function submit(extra) {
80+
async function submit() {
10481
s.stage = "working";
10582
render();
106-
const payload = { plugin_id: plugin.plugin_id, bot_ref: s.selected.id, ...extra };
83+
const payload = { plugin_id: plugin.plugin_id, bot_ref: s.selected.id };
10784
if (skill) payload.skill_id = skill.id;
10885
try {
109-
const r = await fetch("/api/apply", {
86+
const r = await fetch("/api/send", {
11087
method: "POST",
11188
headers: { "Content-Type": "application/json" },
11289
body: JSON.stringify(payload),
11390
});
114-
Object.assign(s, nextStage(await r.json()));
91+
const json = await r.json();
92+
if (!r.ok || !json.ok) throw new Error(json.message || json.error || `HTTP ${r.status}`);
93+
Object.assign(s, { stage: "done", tone: "ok", message: json.message });
11594
} catch (e) {
11695
Object.assign(s, { stage: "done", tone: "err", message: String(e.message || e) });
11796
}
@@ -135,39 +114,14 @@
135114
}
136115

137116
function confirmHtml() {
138-
const verb = plugin.installed ? "Apply" : "Install";
139117
switch (s.stage) {
140118
case "confirm":
141119
return {
142-
sentence: `${verb} ${thing} for ${target()}?`,
143-
fine: "Nothing changes on the bot until the Plugin Applier confirms it.",
144-
cancel: "Cancel",
145-
go: verb,
146-
action: () => submit({}),
147-
};
148-
case "confirm-install":
149-
return {
150-
sentence: `<b>${esc(plugin.name)}</b> isn't installed yet.`,
151-
fine: `Queue an account-wide install for the Plugin Applier, then the apply for ${target()}?`,
152-
cancel: "Cancel",
153-
go: "Queue install",
154-
action: () => submit({ confirmed: true }),
155-
};
156-
case "offer-nudge":
157-
return {
158-
sentence: "There's no per-bot attach API yet, so nothing was changed.",
159-
fine: `You can instead <b>send ${target()} a message</b> pointing at ${thing}. This posts to the bot's thread.`,
160-
cancel: "Done",
161-
go: "Send nudge",
162-
action: () => submit({ mode: "nudge_send", confirmed: true }),
163-
};
164-
case "confirm-nudge":
165-
return {
166-
sentence: `Send ${target()} a message about ${thing}?`,
167-
fine: "This posts to the bot's thread once the Plugin Applier drains the queue.",
120+
sentence: `Send ${thing} to ${target()}?`,
121+
fine: "This sends a direct message through gbot.",
168122
cancel: "Cancel",
169-
go: "Send nudge",
170-
action: () => submit({ mode: "nudge_send", confirmed: true }),
123+
go: "Send",
124+
action: submit,
171125
};
172126
case "working":
173127
return { sentence: "Working…", fine: "", cancel: null, go: null, action: null };
@@ -253,5 +207,5 @@
253207
return { close };
254208
}
255209

256-
global.Picker = { openPicker, nextStage };
210+
global.Picker = { openPicker };
257211
})(window);

‎screens/05-pick-a-bot.png‎

-1.22 MB
Binary file not shown.

‎screens/06-confirm.png‎

-1.19 MB
Binary file not shown.

0 commit comments

Comments
 (0)