Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All example plugins live here with `manifest.json` and prebuilt `plugin.js` file
- `shortcut-counter`: offline keyboard-shortcut consumer; Command+Shift+K /
Control+Shift+K increments through the injected host service. Requires `shortcuts`.
- `broken-page`: intentionally fails when its page renders to exercise error handling.
- `remote-ackbot`: a hosted Agents agent type, with a provider server to run on this
computer. See its README.

## Try the offline playgrounds

Expand Down
1 change: 1 addition & 0 deletions examples/plugins/remote-ackbot/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
remote-ackbot.json
30 changes: 30 additions & 0 deletions examples/plugins/remote-ackbot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Remote ack bot

A hosted Agents type with a pretend provider on this computer. It exercises
the `remote` steps an agent type can supply instead of `run`. The server
(`server.mjs`) holds each agent's key and takes the owner's NIP-OA
attestation. It publishes the agent's profile and answers each of its owner's
mentions with "ack". The plugin (`plugin.js`) passes the app's steps on to the
server, and its **Status** tab reads and saves the provider's reply text.

1. From the repository root, start the server: `node examples/plugins/remote-ackbot/server.mjs`.
It listens on `127.0.0.1:8787` and keeps its agents, with their keys in plain
text, in `remote-ackbot.json` beside it. Use it for testing only.
2. Run the desktop app in development (`bin/just desktop`). Release builds' content
security policy blocks the plugin's requests to `http://127.0.0.1`.
3. Go to Settings → Plugins → **Load from folder**, choose this folder, install
**Remote ack bot** and enable it.
4. In Agents, choose **New agent**, pick **Remote ack bot (hosted)**, name it,
then choose **Create and set up**. The app asks the server for the agent's key,
signs your attestation for it, and lists the agent in your agent records.
5. Add the agent to a channel and mention it. Within a few seconds it replies "ack".

What to check along the way:

- **Interrupted setup.** Start the server with `FAIL_ACTIVATE=1`, and its first
activation fails. The new agent then shows **Setup unfinished**. Choose
**Finish setup** to resume without making a second agent.
- **Other devices.** On another device signed in as you, the agent appears as hosted
if this plugin is installed there. Otherwise it shows as **On another device**.
- **Rename, picture and Delete.** These go through the server. Delete also removes
the agent from its channels and archives it.
5 changes: 5 additions & 0 deletions examples/plugins/remote-ackbot/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"id": "example.remote-ackbot",
"name": "Remote ack bot",
"apiVersion": 1
}
119 changes: 119 additions & 0 deletions examples/plugins/remote-ackbot/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Prebuilt API v1 module: a hosted Agents type whose provider is the ack bot
// server beside this file (server.mjs), on this computer. The provider holds
// each agent's key and runs it; this plugin only forwards the app's `remote`
// steps to it, and its Status tab reads and saves the provider's settings.
export const inject = ["react", "agents"];

const SERVER = "http://127.0.0.1:8787";

async function call(path, method, body, signal) {
let response;
try {
response = await fetch(`${SERVER}${path}`, {
method,
headers: { "Content-Type": "application/json" },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
...(signal ? { signal } : {}),
});
} catch (error) {
if (signal?.aborted) throw error;
throw new Error(
`The ack bot server is not reachable at ${SERVER}. Start it with: node examples/plugins/remote-ackbot/server.mjs`,
);
}
const result = await response.json().catch(() => ({}));
if (!response.ok)
throw new Error(
result.error ?? `The ack bot server answered ${response.status}`,
);
return result;
}

export function apply(ctx) {
const React = ctx.react;
const h = React.createElement;

function Status({ agent }) {
const [state, setState] = React.useState(undefined);
const [reply, setReply] = React.useState("");
const [error, setError] = React.useState("");
React.useEffect(() => {
const controller = new AbortController();
call(`/agents/${agent.pubkey}`, "GET", undefined, controller.signal).then(
(result) => {
setState(result);
setReply(result.reply);
},
(reason) => controller.signal.aborted || setError(reason.message),
);
return () => controller.abort();
}, [agent.pubkey]);
const saveReply = () => {
setError("");
call(`/agents/${agent.pubkey}/settings`, "POST", { reply }).then(
(result) => setState((prior) => ({ ...prior, ...result })),
(reason) => setError(reason.message),
);
};
if (error && !state) return h("p", { role: "alert" }, error);
if (!state) return h("p", null, "Asking the ack bot server…");
return h(
"div",
{ style: { display: "grid", gap: "var(--space-2, 8px)" } },
h(
"p",
null,
state.active
? `Running on ${SERVER}. It has answered ${state.replies} mention${state.replies === 1 ? "" : "s"}.`
: "Made, but not set up yet.",
),
h(
"label",
{ style: { display: "grid", gap: 4 } },
"Reply",
h("input", {
value: reply,
onChange: (event) => setReply(event.target.value),
}),
),
h(
"button",
{ type: "button", onClick: saveReply, disabled: reply === state.reply },
"Save reply",
),
error && h("p", { role: "alert" }, error),
);
}

ctx.agents.register({
id: "ackbot",
title: "Remote ack bot",
description: `Replies to your mentions from a server on this computer (${SERVER}). For trying hosted agents.`,
defaults: () => ({ config: {} }),
summary: () => "Hosted on this computer",
tabs: [{ id: "status", title: "Status", component: Status }],
remote: {
provision: ({ name, community, signal }) =>
call("/agents", "POST", { name, community }, signal),
activate: async ({ pubkey, name, community, auth, signal }) => {
await call(
`/agents/${pubkey}/activate`,
"POST",
{ name, community, auth },
signal,
);
},
profile: async ({ pubkey, name, picture, signal }) => {
await call(
`/agents/${pubkey}/profile`,
"POST",
{ name, picture },
signal,
);
},
remove: async ({ pubkey, signal }) => {
await call(`/agents/${pubkey}`, "DELETE", undefined, signal);
},
},
});
}
Loading
Loading