diff --git a/README.md b/README.md index a1a091e..9a5bbc0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Plugin Library (sidecar browse UI) -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. +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. ## Tour @@ -18,17 +18,13 @@ Click a skill and the full `SKILL.md` opens in the reading pane. Front matter be ![A reference file](screens/04-reference-tab.png) -"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. - -![Pick a bot](screens/05-pick-a-bot.png) - -![Confirm](screens/06-confirm.png) +"Send to a bot" pulls the live roster from `gbot` (bots and groups, with their avatars), asks for confirmation, then calls `gbot send` directly. Every installed plugin gets the same treatment, not only pstack. ![Cursor Team Kit](screens/07-cursor-team-kit.png) -Marketplace listings show catalog copy and offer an install path. +Marketplace listings show catalog copy and can be sent to a bot. ![Marketplace](screens/08-marketplace.png) @@ -61,6 +57,17 @@ Team Marketplaces require a Cursor Teams or Enterprise plan. If you previously imported `main`, re-import the marketplace and select the generated branch. +#### Without team marketplace + +```sh +git clone --branch agent-bundle-artifact --depth 1 \ + https://github.com/ScriptedAlchemy/plugin-library.git +cd plugin-library && node ./install.mjs +``` + +Reload Cursor (`Developer: Reload Window`). From that clone directory, verify +with `npx --no-install agent-bundle doctor --from . --host cursor`. + For a local proof from a checkout: ```bash @@ -96,11 +103,11 @@ node artifact/scripts/plugin-library.mjs status --json node artifact/scripts/plugin-library.mjs stop ``` -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. +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. `npm test` builds the generated artifact, then runs the `node --test` suite (installer lifecycle, front matter, cache dedupe, path containment, -apply-status mapping, and an HTTP smoke test against a throwaway cache). +direct `gbot send`, and an HTTP smoke test against a throwaway cache). ## Browse API (read-only) @@ -113,24 +120,17 @@ apply-status mapping, and an HTTP smoke test against a throwaway cache). Override the cache root with `CURSOR_PLUGIN_CACHE`, the CLI with `GBOT_BIN`. -## Apply contract (Plugin Applier) +## Send contract -`POST /api/apply` body: +The UI calls `POST /api/send` only after the user confirms: ```json -{ "plugin_id": "…", "bot_ref": "…", "skill_id?": "…", "confirmed": true, "mode?": "profile_bake|nudge_send" } +{ "plugin_id": "…", "bot_ref": "…", "skill_id?": "…" } ``` -| Result | When | -|--------|------| -| `needs_install_confirm` | not installed, `confirmed` not true | -| `install_queued` | not installed, `confirmed: true` → Applier drains → `InstallPlugin` (account only) | -| `missing_attach_api` | installed; no per-bot skill attach yet (default) | -| `profile_bake_queued` / `nudge_send_queued` | only if `mode` set + confirmed (opt-in; never silent) | - -Also: `GET /api/apply/pending`, `POST /api/apply/ack` `{ "id" }`. - -Explorer UI confirm counts as the user's Explorer confirm. No silent fleet bot writes. +The server verifies the plugin and optional skill against its catalog, then +runs `gbot send --json`. There is no queue, Applier, install +branch, or per-bot attach fallback. ## Data diff --git a/agent-bundle.config.ts b/agent-bundle.config.ts index 71b99a5..23cc4e3 100644 --- a/agent-bundle.config.ts +++ b/agent-bundle.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ plugin: { description: 'Browse installed and marketplace plugins, read every skill in full, and hand one to a Grok Bot.', + logo: './assets/logo.svg', name: 'plugin-library', }, runtime: { node: '22.19.0' }, diff --git a/lib/bots.js b/lib/bots.js index e00fc17..1616424 100644 --- a/lib/bots.js +++ b/lib/bots.js @@ -18,7 +18,7 @@ function runGbot(args) { const e = new Error( err.code === "ENOENT" ? `\`${GBOT_BIN}\` not found on PATH` - : String(stderr || err.message).trim(), + : String(stderr || stdout || err.message).trim(), ); e.code = err.code; return reject(e); @@ -33,6 +33,13 @@ function runGbot(args) { }); } +/** Send one confirmed instruction directly through gbot. */ +async function sendToBot(target, message) { + const result = await runGbot(["send", target, message]); + if (!result || typeof result !== "object") throw new Error("gbot send: expected an object"); + return result; +} + /** Boundary check on gbot output: a shape change surfaces as a 502, not an empty roster. */ function shapeList(list, what) { if (!Array.isArray(list)) throw new Error(`gbot ${what} list: expected an array`); @@ -78,4 +85,4 @@ async function listBots({ force = false } = {}) { } } -module.exports = { listBots }; +module.exports = { listBots, sendToBot }; diff --git a/public/app.js b/public/app.js index a0ff214..fce53ab 100644 --- a/public/app.js +++ b/public/app.js @@ -71,7 +71,7 @@ els.readerStatus.textContent = msg; els.readerStatus.className = `status ${cls}`; } - function onApplyResult(msg, tone) { + function onSendResult(msg, tone) { toast(msg, tone); setStatus(msg, tone); } @@ -171,7 +171,7 @@ if (p.skill_count) chips.push(`${p.skill_count} skills`); if (p.connector_count) chips.push(`${p.connector_count} connectors`); } - chips.push(``); + chips.push(``); let html = `
${logoHtml(p, "lg")} @@ -210,7 +210,7 @@ els.main.scrollTop = 0; wireLogos(els.main); els.main.querySelectorAll(".card[data-doc]").forEach((c) => c.addEventListener("click", () => navigate(c.dataset.doc))); - $("applyPlugin").addEventListener("click", () => Picker.openPicker({ root: els.picker, plugin: p, skill: null, onResult: onApplyResult })); + $("sendPlugin").addEventListener("click", () => Picker.openPicker({ root: els.picker, plugin: p, skill: null, onResult: onSendResult })); markCurrentCard(); } @@ -288,8 +288,8 @@ const resolved = p.local ? docTabs(p, kind, id) : null; const skill = resolved ? resolved.skill : null; - els.readerApply.textContent = skill ? `Apply “${skill.name}” to a bot…` : "Apply plugin to a bot…"; - els.readerApply.onclick = () => Picker.openPicker({ root: els.picker, plugin: p, skill, onResult: onApplyResult }); + els.readerApply.textContent = skill ? `Send “${skill.name}” to a bot…` : "Send plugin to a bot…"; + els.readerApply.onclick = () => Picker.openPicker({ root: els.picker, plugin: p, skill, onResult: onSendResult }); if (!resolved) { els.readerBody.innerHTML = `
Not found in the local plugin cache.
`; return; diff --git a/public/index.html b/public/index.html index bd12b79..deef176 100644 --- a/public/index.html +++ b/public/index.html @@ -40,7 +40,7 @@

Plugin Library

- +
diff --git a/public/picker.js b/public/picker.js index 13b5231..1ed0383 100644 --- a/public/picker.js +++ b/public/picker.js @@ -1,9 +1,7 @@ /* global Markdown */ /** - * Bot picker + apply flow. One state object, one render() that switches on - * `stage`, and one pure function that maps an /api/apply response to the - * next stage. `confirmed: true` is only ever sent from a stage whose button - * the user just clicked. + * Bot picker + direct send flow. One state object and one render() switch. + * The selected target receives a message only after the user confirms. */ (function (global) { "use strict"; @@ -35,31 +33,10 @@ return `${esc(initials(b.name))}`; } - /** /api/apply response → next stage. Pure; the only place the contract is interpreted. */ - function nextStage(json) { - const status = json.status || json.error; - switch (status) { - case "needs_install_confirm": - return { stage: "confirm-install" }; - case "needs_mode_confirm": - return { stage: "confirm-nudge" }; - case "missing_attach_api": - return { stage: "offer-nudge" }; - case "install_queued": - return { stage: "done", tone: "ok", message: "Install queued for the Plugin Applier." }; - case "nudge_send_queued": - return { stage: "done", tone: "ok", message: "Nudge queued for the Plugin Applier." }; - case "profile_bake_queued": - return { stage: "done", tone: "ok", message: "Profile bake queued for the Plugin Applier." }; - default: - return { stage: "done", tone: "err", message: json.message || `Unexpected response: ${status}` }; - } - } - /** * @param {object} opts * @param {HTMLElement} opts.root container to render into - * @param {object} opts.plugin { plugin_id, name, installed } + * @param {object} opts.plugin { plugin_id, name } * @param {object|null} opts.skill { id, name } or null for the whole plugin * @param {(msg: string, tone: string) => void} opts.onResult */ @@ -100,18 +77,20 @@ render(); } - async function submit(extra) { + async function submit() { s.stage = "working"; render(); - const payload = { plugin_id: plugin.plugin_id, bot_ref: s.selected.id, ...extra }; + const payload = { plugin_id: plugin.plugin_id, bot_ref: s.selected.id }; if (skill) payload.skill_id = skill.id; try { - const r = await fetch("/api/apply", { + const r = await fetch("/api/send", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); - Object.assign(s, nextStage(await r.json())); + const json = await r.json(); + if (!r.ok || !json.ok) throw new Error(json.message || json.error || `HTTP ${r.status}`); + Object.assign(s, { stage: "done", tone: "ok", message: json.message }); } catch (e) { Object.assign(s, { stage: "done", tone: "err", message: String(e.message || e) }); } @@ -135,39 +114,14 @@ } function confirmHtml() { - const verb = plugin.installed ? "Apply" : "Install"; switch (s.stage) { case "confirm": return { - sentence: `${verb} ${thing} for ${target()}?`, - fine: "Nothing changes on the bot until the Plugin Applier confirms it.", - cancel: "Cancel", - go: verb, - action: () => submit({}), - }; - case "confirm-install": - return { - sentence: `${esc(plugin.name)} isn't installed yet.`, - fine: `Queue an account-wide install for the Plugin Applier, then the apply for ${target()}?`, - cancel: "Cancel", - go: "Queue install", - action: () => submit({ confirmed: true }), - }; - case "offer-nudge": - return { - sentence: "There's no per-bot attach API yet, so nothing was changed.", - fine: `You can instead send ${target()} a message pointing at ${thing}. This posts to the bot's thread.`, - cancel: "Done", - go: "Send nudge", - action: () => submit({ mode: "nudge_send", confirmed: true }), - }; - case "confirm-nudge": - return { - sentence: `Send ${target()} a message about ${thing}?`, - fine: "This posts to the bot's thread once the Plugin Applier drains the queue.", + sentence: `Send ${thing} to ${target()}?`, + fine: "This sends a direct message through gbot.", cancel: "Cancel", - go: "Send nudge", - action: () => submit({ mode: "nudge_send", confirmed: true }), + go: "Send", + action: submit, }; case "working": return { sentence: "Working…", fine: "", cancel: null, go: null, action: null }; @@ -194,7 +148,7 @@ root.innerHTML = `