Add support for users to connect their own Nostr identities via NIP-46 remote signers (bunkers) so agents can post, react, and repost on their behalf. New features: - Multi-account bunker connection management (bunker-store.ts) - Event posting: notes (NIP-10), reactions (NIP-25), reposts (NIP-18), long-form articles (NIP-23) - 8 agent tools: nostr_connect, nostr_post, nostr_react, nostr_repost, nostr_fetch, nostr_article, nostr_disconnect, nostr_status - HTTP endpoints for bunker management via web UI - bunkerAccounts array in config schema for multi-bunker support - Shared pool utility for bunker tools Excludes NIP-09 deletion for safety (no agent deletion capability). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
153 lines
5.1 KiB
TypeScript
153 lines
5.1 KiB
TypeScript
import type { ClawdbotPluginApi, ClawdbotConfig } from "clawdbot/plugin-sdk";
|
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
|
|
|
import { nostrPlugin } from "./src/channel.js";
|
|
import { setNostrRuntime, getNostrRuntime } from "./src/runtime.js";
|
|
import { createNostrProfileHttpHandler } from "./src/nostr-profile-http.js";
|
|
import { createNostrBunkerHttpHandler } from "./src/nostr-bunker-http.js";
|
|
import { createNostrAgentTools } from "./src/agent-tools.js";
|
|
import { resolveNostrAccount } from "./src/types.js";
|
|
import type { NostrProfile, BunkerAccountConfig } from "./src/config-schema.js";
|
|
|
|
const plugin = {
|
|
id: "nostr",
|
|
name: "Nostr",
|
|
description: "Nostr DM channel plugin via NIP-04",
|
|
configSchema: emptyPluginConfigSchema(),
|
|
register(api: ClawdbotPluginApi) {
|
|
setNostrRuntime(api.runtime);
|
|
api.registerChannel({ plugin: nostrPlugin });
|
|
|
|
// Register HTTP handler for profile management
|
|
const httpHandler = createNostrProfileHttpHandler({
|
|
getConfigProfile: (accountId: string) => {
|
|
const runtime = getNostrRuntime();
|
|
const cfg = runtime.config.loadConfig() as ClawdbotConfig;
|
|
const account = resolveNostrAccount({ cfg, accountId });
|
|
return account.profile;
|
|
},
|
|
updateConfigProfile: async (accountId: string, profile: NostrProfile) => {
|
|
const runtime = getNostrRuntime();
|
|
const cfg = runtime.config.loadConfig() as ClawdbotConfig;
|
|
|
|
// Build the config patch for channels.nostr.profile
|
|
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
const nostrConfig = (channels.nostr ?? {}) as Record<string, unknown>;
|
|
|
|
const updatedNostrConfig = {
|
|
...nostrConfig,
|
|
profile,
|
|
};
|
|
|
|
const updatedChannels = {
|
|
...channels,
|
|
nostr: updatedNostrConfig,
|
|
};
|
|
|
|
await runtime.config.writeConfigFile({
|
|
...cfg,
|
|
channels: updatedChannels,
|
|
});
|
|
},
|
|
getAccountInfo: (accountId: string) => {
|
|
const runtime = getNostrRuntime();
|
|
const cfg = runtime.config.loadConfig() as ClawdbotConfig;
|
|
const account = resolveNostrAccount({ cfg, accountId });
|
|
if (!account.configured || !account.publicKey) {
|
|
return null;
|
|
}
|
|
return {
|
|
pubkey: account.publicKey,
|
|
relays: account.relays,
|
|
};
|
|
},
|
|
log: api.logger,
|
|
});
|
|
|
|
api.registerHttpHandler(httpHandler);
|
|
|
|
// Register HTTP handler for bunker management
|
|
const bunkerHttpHandler = createNostrBunkerHttpHandler({
|
|
getBunkerAccounts: (accountId: string) => {
|
|
const runtime = getNostrRuntime();
|
|
const cfg = runtime.config.loadConfig() as ClawdbotConfig;
|
|
const account = resolveNostrAccount({ cfg, accountId });
|
|
return account.bunkerAccounts;
|
|
},
|
|
updateBunkerAccount: async (
|
|
accountId: string,
|
|
bunkerIndex: number,
|
|
update: Partial<BunkerAccountConfig>
|
|
) => {
|
|
const runtime = getNostrRuntime();
|
|
const cfg = runtime.config.loadConfig() as ClawdbotConfig;
|
|
const account = resolveNostrAccount({ cfg, accountId });
|
|
|
|
// Update the specific bunker account
|
|
const bunkerAccounts = [...account.bunkerAccounts];
|
|
while (bunkerAccounts.length <= bunkerIndex) {
|
|
bunkerAccounts.push({ bunkerUrl: "" });
|
|
}
|
|
bunkerAccounts[bunkerIndex] = {
|
|
...bunkerAccounts[bunkerIndex],
|
|
...update,
|
|
};
|
|
|
|
// Write back to config
|
|
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
const nostrConfig = (channels.nostr ?? {}) as Record<string, unknown>;
|
|
await runtime.config.writeConfigFile({
|
|
...cfg,
|
|
channels: {
|
|
...channels,
|
|
nostr: {
|
|
...nostrConfig,
|
|
bunkerAccounts,
|
|
},
|
|
},
|
|
});
|
|
},
|
|
clearConfigBunkerUrl: async (accountId: string, bunkerIndex: number) => {
|
|
const runtime = getNostrRuntime();
|
|
const cfg = runtime.config.loadConfig() as ClawdbotConfig;
|
|
const account = resolveNostrAccount({ cfg, accountId });
|
|
|
|
// Clear the specific bunker account
|
|
const bunkerAccounts = [...account.bunkerAccounts];
|
|
if (bunkerIndex < bunkerAccounts.length) {
|
|
bunkerAccounts[bunkerIndex] = {
|
|
...bunkerAccounts[bunkerIndex],
|
|
bunkerUrl: "",
|
|
userPubkey: undefined,
|
|
connectedAt: undefined,
|
|
};
|
|
}
|
|
|
|
// Write back to config
|
|
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
const nostrConfig = (channels.nostr ?? {}) as Record<string, unknown>;
|
|
await runtime.config.writeConfigFile({
|
|
...cfg,
|
|
channels: {
|
|
...channels,
|
|
nostr: {
|
|
...nostrConfig,
|
|
bunkerAccounts,
|
|
},
|
|
},
|
|
});
|
|
},
|
|
log: api.logger,
|
|
});
|
|
|
|
api.registerHttpHandler(bunkerHttpHandler);
|
|
|
|
// Register agent tools for bunker operations
|
|
for (const tool of createNostrAgentTools()) {
|
|
api.registerTool(tool);
|
|
}
|
|
},
|
|
};
|
|
|
|
export default plugin;
|