@kernelius/openclaw-plugin 0.1.0 → 0.2.1
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 +62 -5
- package/dist/index.js +191 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -20,6 +20,14 @@ bun add @kernelius/openclaw-plugin
|
|
|
20
20
|
|
|
21
21
|
## Configuration
|
|
22
22
|
|
|
23
|
+
This plugin supports two integration modes:
|
|
24
|
+
|
|
25
|
+
### Option 1: Simple Webhook Mode (Recommended for Getting Started)
|
|
26
|
+
|
|
27
|
+
Uses OpenClaw's generic `/hooks/agent` endpoint with template mappings. Each webhook creates an isolated agent session.
|
|
28
|
+
|
|
29
|
+
**Best for:** Quick setup, simple workflows, disposable conversations
|
|
30
|
+
|
|
23
31
|
Add to your OpenClaw `config.json5`:
|
|
24
32
|
|
|
25
33
|
```json5
|
|
@@ -29,7 +37,6 @@ Add to your OpenClaw `config.json5`:
|
|
|
29
37
|
enabled: true,
|
|
30
38
|
apiUrl: "https://forge-api.kernelius.com", // Optional, defaults to this
|
|
31
39
|
apiKey: "forge_agent_xxx...", // Get from Forge at /settings/agents
|
|
32
|
-
webhookSecret: "your-webhook-secret", // Optional, for signature verification
|
|
33
40
|
}
|
|
34
41
|
},
|
|
35
42
|
|
|
@@ -60,6 +67,42 @@ Add to your OpenClaw `config.json5`:
|
|
|
60
67
|
}
|
|
61
68
|
```
|
|
62
69
|
|
|
70
|
+
Create webhooks pointing to `http://your-openclaw:18789/hooks/forge`.
|
|
71
|
+
|
|
72
|
+
### Option 2: Gateway Mode with Persistent Sessions (Power Users)
|
|
73
|
+
|
|
74
|
+
Uses the plugin's gateway adapter for persistent conversations per issue/PR.
|
|
75
|
+
|
|
76
|
+
**Best for:** Complex workflows, conversation history, stateful interactions
|
|
77
|
+
|
|
78
|
+
Add to your OpenClaw `config.json5`:
|
|
79
|
+
|
|
80
|
+
```json5
|
|
81
|
+
{
|
|
82
|
+
channels: {
|
|
83
|
+
kernelius: {
|
|
84
|
+
enabled: true,
|
|
85
|
+
apiUrl: "https://forge-api.kernelius.com",
|
|
86
|
+
apiKey: "forge_agent_xxx...",
|
|
87
|
+
webhookSecret: "your-webhook-secret", // REQUIRED for gateway mode
|
|
88
|
+
webhookPath: "/kernelius", // Or use webhookUrl for full URL
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Create webhooks pointing to `http://your-openclaw:18789/kernelius` (or your custom path).
|
|
95
|
+
|
|
96
|
+
**Key Differences:**
|
|
97
|
+
|
|
98
|
+
| Feature | Option 1 (Simple) | Option 2 (Gateway) |
|
|
99
|
+
|---------|-------------------|---------------------|
|
|
100
|
+
| Setup complexity | Minimal | Requires webhookSecret |
|
|
101
|
+
| Conversation history | Isolated per webhook | Persistent per issue/PR |
|
|
102
|
+
| Session management | Disposable | Stateful |
|
|
103
|
+
| Configuration | Hooks + templates | Channel config only |
|
|
104
|
+
| Best for | Quick responses | Multi-turn collaboration |
|
|
105
|
+
|
|
63
106
|
## Forge Setup
|
|
64
107
|
|
|
65
108
|
1. **Get an API key:**
|
|
@@ -68,14 +111,28 @@ Add to your OpenClaw `config.json5`:
|
|
|
68
111
|
- Add it to your OpenClaw config as `apiKey`
|
|
69
112
|
|
|
70
113
|
2. **Create webhooks:**
|
|
114
|
+
|
|
115
|
+
**For Option 1 (Simple Mode):**
|
|
71
116
|
```bash
|
|
72
|
-
forge webhooks create
|
|
73
|
-
--repo @owner/repo
|
|
74
|
-
--url "http://your-openclaw-server:18789/hooks/forge"
|
|
75
|
-
--events "issue.created,issue.commented,pr.created,pr.review_requested,pr.merged"
|
|
117
|
+
forge webhooks create \
|
|
118
|
+
--repo @owner/repo \
|
|
119
|
+
--url "http://your-openclaw-server:18789/hooks/forge" \
|
|
120
|
+
--events "issue.created,issue.commented,pr.created,pr.review_requested,pr.merged" \
|
|
76
121
|
--name "OpenClaw Integration"
|
|
77
122
|
```
|
|
78
123
|
|
|
124
|
+
**For Option 2 (Gateway Mode):**
|
|
125
|
+
```bash
|
|
126
|
+
forge webhooks create \
|
|
127
|
+
--repo @owner/repo \
|
|
128
|
+
--url "http://your-openclaw-server:18789/kernelius" \
|
|
129
|
+
--events "issue.created,issue.commented,pr.created,pr.review_requested,pr.merged" \
|
|
130
|
+
--secret "your-webhook-secret" \
|
|
131
|
+
--name "OpenClaw Gateway"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Note: The webhook secret must match `webhookSecret` in your OpenClaw config.
|
|
135
|
+
|
|
79
136
|
3. **Test the webhook:**
|
|
80
137
|
```bash
|
|
81
138
|
forge webhooks test --repo @owner/repo --id <webhook-id>
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,152 @@ function getKerneliusRuntime() {
|
|
|
19
19
|
return runtime;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// src/inbound.ts
|
|
23
|
+
import crypto from "crypto";
|
|
24
|
+
function verifyWebhookSignature(payload, signature, secret) {
|
|
25
|
+
if (!secret) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const expectedSignature = `sha256=${crypto.createHmac("sha256", secret).update(payload).digest("hex")}`;
|
|
29
|
+
return crypto.timingSafeEqual(
|
|
30
|
+
Buffer.from(signature),
|
|
31
|
+
Buffer.from(expectedSignature)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function forgePayloadToInbound(payload, accountId) {
|
|
35
|
+
const { event, repository, sender, issue, pullRequest, comment } = payload;
|
|
36
|
+
let conversationId;
|
|
37
|
+
let body;
|
|
38
|
+
let chatType = "channel";
|
|
39
|
+
if (issue) {
|
|
40
|
+
conversationId = `repo:${repository.fullName}:issue:${issue.number}`;
|
|
41
|
+
if (event === "issue.created") {
|
|
42
|
+
body = `**New Issue #${issue.number}**: ${issue.title}
|
|
43
|
+
|
|
44
|
+
${issue.body || ""}`;
|
|
45
|
+
} else if (event === "issue.commented" && comment) {
|
|
46
|
+
body = `**Comment on Issue #${issue.number}** by @${sender.username}:
|
|
47
|
+
|
|
48
|
+
${comment.body}`;
|
|
49
|
+
} else if (event === "issue.closed") {
|
|
50
|
+
body = `**Issue #${issue.number} closed** by @${sender.username}`;
|
|
51
|
+
} else if (event === "issue.reopened") {
|
|
52
|
+
body = `**Issue #${issue.number} reopened** by @${sender.username}`;
|
|
53
|
+
} else if (event === "issue.updated") {
|
|
54
|
+
body = `**Issue #${issue.number} updated** by @${sender.username}: ${issue.title}`;
|
|
55
|
+
} else {
|
|
56
|
+
body = `Issue #${issue.number} event: ${event}`;
|
|
57
|
+
}
|
|
58
|
+
} else if (pullRequest) {
|
|
59
|
+
conversationId = `repo:${repository.fullName}:pr:${pullRequest.number}`;
|
|
60
|
+
if (event === "pr.created") {
|
|
61
|
+
body = `**New Pull Request #${pullRequest.number}**: ${pullRequest.title}
|
|
62
|
+
|
|
63
|
+
${pullRequest.body || ""}`;
|
|
64
|
+
} else if (event === "pr.review_requested") {
|
|
65
|
+
body = `**Review Requested on PR #${pullRequest.number}**: ${pullRequest.title}
|
|
66
|
+
|
|
67
|
+
Please review this pull request.`;
|
|
68
|
+
} else if (event === "pr.reviewed") {
|
|
69
|
+
body = `**PR #${pullRequest.number} reviewed** by @${sender.username}`;
|
|
70
|
+
} else if (event === "pr.merged") {
|
|
71
|
+
body = `**PR #${pullRequest.number} merged** by @${sender.username}`;
|
|
72
|
+
} else if (event === "pr.commented" && comment) {
|
|
73
|
+
body = `**Comment on PR #${pullRequest.number}** by @${sender.username}:
|
|
74
|
+
|
|
75
|
+
${comment.body}`;
|
|
76
|
+
} else {
|
|
77
|
+
body = `Pull Request #${pullRequest.number} event: ${event}`;
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
conversationId = `repo:${repository.fullName}`;
|
|
81
|
+
body = `Repository event: ${event}`;
|
|
82
|
+
}
|
|
83
|
+
const timestamp = new Date(payload.timestamp).getTime();
|
|
84
|
+
return {
|
|
85
|
+
id: crypto.randomUUID(),
|
|
86
|
+
from: conversationId,
|
|
87
|
+
conversationId,
|
|
88
|
+
to: accountId,
|
|
89
|
+
accountId,
|
|
90
|
+
body,
|
|
91
|
+
pushName: sender.username,
|
|
92
|
+
timestamp,
|
|
93
|
+
chatType,
|
|
94
|
+
chatId: conversationId,
|
|
95
|
+
senderName: sender.username,
|
|
96
|
+
selfJid: accountId,
|
|
97
|
+
selfE164: null,
|
|
98
|
+
// Helpers (simplified for webhook-based channel)
|
|
99
|
+
sendComposing: async () => {
|
|
100
|
+
},
|
|
101
|
+
reply: async (text) => {
|
|
102
|
+
console.log(`[kernelius] Reply queued: ${text}`);
|
|
103
|
+
},
|
|
104
|
+
sendMedia: async () => {
|
|
105
|
+
throw new Error("Media not supported for Forge webhooks");
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function handleWebhookRequest(req, res, ctx) {
|
|
110
|
+
const chunks = [];
|
|
111
|
+
for await (const chunk of req) {
|
|
112
|
+
chunks.push(chunk);
|
|
113
|
+
}
|
|
114
|
+
const rawBody = Buffer.concat(chunks).toString("utf-8");
|
|
115
|
+
const signature = req.headers["x-forge-signature"];
|
|
116
|
+
if (ctx.account.webhookSecret) {
|
|
117
|
+
if (!signature) {
|
|
118
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
119
|
+
res.end(JSON.stringify({ error: "Missing X-Forge-Signature header" }));
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (!verifyWebhookSignature(rawBody, signature, ctx.account.webhookSecret)) {
|
|
123
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
124
|
+
res.end(JSON.stringify({ error: "Invalid signature" }));
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let payload;
|
|
129
|
+
try {
|
|
130
|
+
payload = JSON.parse(rawBody);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
133
|
+
res.end(JSON.stringify({ error: "Invalid JSON payload" }));
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (payload.source !== "forge") {
|
|
137
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
138
|
+
res.end(JSON.stringify({ error: "Invalid source" }));
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
ctx.statusSink?.({ lastInboundAt: Date.now() });
|
|
142
|
+
const inbound = forgePayloadToInbound(payload, ctx.account.accountId);
|
|
143
|
+
if (!inbound) {
|
|
144
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
145
|
+
res.end(JSON.stringify({ error: "Could not process event" }));
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
149
|
+
res.end(JSON.stringify({ success: true, event: payload.event }));
|
|
150
|
+
return inbound;
|
|
151
|
+
}
|
|
152
|
+
function resolveKerneliusWebhookPath(webhookPath, webhookUrl) {
|
|
153
|
+
if (webhookPath?.trim()) {
|
|
154
|
+
const path = webhookPath.trim();
|
|
155
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
156
|
+
}
|
|
157
|
+
if (webhookUrl?.trim()) {
|
|
158
|
+
try {
|
|
159
|
+
const parsed = new URL(webhookUrl);
|
|
160
|
+
return parsed.pathname || "/kernelius";
|
|
161
|
+
} catch {
|
|
162
|
+
return "/kernelius";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return "/kernelius";
|
|
166
|
+
}
|
|
167
|
+
|
|
22
168
|
// src/channel.ts
|
|
23
169
|
var meta = getChatChannelMeta("kernelius");
|
|
24
170
|
function resolveKerneliusAccount(cfg, accountId) {
|
|
@@ -30,7 +176,9 @@ function resolveKerneliusAccount(cfg, accountId) {
|
|
|
30
176
|
enabled: accountConfig.enabled !== false,
|
|
31
177
|
apiUrl: accountConfig.apiUrl || "https://forge-api.kernelius.com",
|
|
32
178
|
apiKey: accountConfig.apiKey,
|
|
33
|
-
webhookSecret: accountConfig.webhookSecret
|
|
179
|
+
webhookSecret: accountConfig.webhookSecret,
|
|
180
|
+
webhookPath: accountConfig.webhookPath,
|
|
181
|
+
webhookUrl: accountConfig.webhookUrl
|
|
34
182
|
};
|
|
35
183
|
}
|
|
36
184
|
var kerneliusPlugin = {
|
|
@@ -173,6 +321,48 @@ var kerneliusPlugin = {
|
|
|
173
321
|
}
|
|
174
322
|
return { success: true };
|
|
175
323
|
}
|
|
324
|
+
},
|
|
325
|
+
gateway: {
|
|
326
|
+
start: async (ctx) => {
|
|
327
|
+
const runtime2 = getKerneliusRuntime();
|
|
328
|
+
const config = runtime2.config.loadConfig();
|
|
329
|
+
const account = resolveKerneliusAccount(config, ctx.accountId);
|
|
330
|
+
if (!account.webhookPath && !account.webhookUrl) {
|
|
331
|
+
console.log("[kernelius] No webhook path configured, skipping gateway start");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const webhookPath = resolveKerneliusWebhookPath(
|
|
335
|
+
account.webhookPath,
|
|
336
|
+
account.webhookUrl
|
|
337
|
+
);
|
|
338
|
+
console.log(`[kernelius] Starting gateway for account ${account.accountId} at ${webhookPath}`);
|
|
339
|
+
ctx.registerHttpHandler({
|
|
340
|
+
path: webhookPath,
|
|
341
|
+
method: "POST",
|
|
342
|
+
handler: async (req, res) => {
|
|
343
|
+
try {
|
|
344
|
+
const inbound = await handleWebhookRequest(req, res, {
|
|
345
|
+
account,
|
|
346
|
+
runtime: runtime2,
|
|
347
|
+
statusSink: ctx.statusSink
|
|
348
|
+
});
|
|
349
|
+
if (inbound) {
|
|
350
|
+
await ctx.queueInbound(inbound);
|
|
351
|
+
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
console.error("[kernelius] Webhook handler error:", error);
|
|
354
|
+
if (!res.headersSent) {
|
|
355
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
356
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
console.log(`[kernelius] Gateway started successfully at ${webhookPath}`);
|
|
362
|
+
},
|
|
363
|
+
stop: async (ctx) => {
|
|
364
|
+
console.log(`[kernelius] Stopping gateway for account ${ctx.accountId}`);
|
|
365
|
+
}
|
|
176
366
|
}
|
|
177
367
|
};
|
|
178
368
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/channel.ts","../src/runtime.ts"],"sourcesContent":["import type { OpenClawPluginApi } from \"openclaw/plugin-sdk\";\nimport { emptyPluginConfigSchema } from \"openclaw/plugin-sdk\";\nimport { kerneliusPlugin } from \"./channel.js\";\nimport { setKerneliusRuntime } from \"./runtime.js\";\n\nconst plugin = {\n id: \"kernelius\",\n name: \"Kernelius Forge\",\n description: \"Connect to Kernelius Forge repositories, issues, and pull requests\",\n configSchema: emptyPluginConfigSchema(),\n register(api: OpenClawPluginApi) {\n setKerneliusRuntime(api.runtime);\n api.registerChannel({ plugin: kerneliusPlugin });\n },\n};\n\nexport default plugin;\n","import type {\n ChannelPlugin,\n ChannelConfigAdapter,\n} from \"openclaw/plugin-sdk\";\nimport {\n getChatChannelMeta,\n DEFAULT_ACCOUNT_ID,\n} from \"openclaw/plugin-sdk\";\nimport { getKerneliusRuntime } from \"./runtime.js\";\nimport type { KerneliusConfig } from \"./types.js\";\n\nconst meta = getChatChannelMeta(\"kernelius\");\n\n// Resolve Kernelius account configuration\nfunction resolveKerneliusAccount(cfg: any, accountId?: string) {\n const effectiveAccountId = accountId || DEFAULT_ACCOUNT_ID;\n const channelConfig = cfg.channels?.kernelius || {};\n\n // Support both top-level config and accounts structure\n const accountConfig = channelConfig.accounts?.[effectiveAccountId] || channelConfig;\n\n return {\n accountId: effectiveAccountId,\n enabled: accountConfig.enabled !== false,\n apiUrl: accountConfig.apiUrl || \"https://forge-api.kernelius.com\",\n apiKey: accountConfig.apiKey,\n webhookSecret: accountConfig.webhookSecret,\n };\n}\n\nexport const kerneliusPlugin: ChannelPlugin = {\n id: \"kernelius\",\n meta: {\n ...meta,\n name: \"Kernelius Forge\",\n emoji: \"🔥\",\n description: \"Git platform for human-agent collaboration\",\n },\n capabilities: {\n chatTypes: [\"direct\", \"channel\", \"thread\"],\n reactions: true,\n threads: true,\n media: false,\n nativeCommands: false,\n },\n reload: { configPrefixes: [\"channels.kernelius\"] },\n config: {\n listAccountIds: (cfg) => {\n const channelConfig = cfg.channels?.kernelius;\n if (!channelConfig) return [];\n if (channelConfig.accounts) {\n return Object.keys(channelConfig.accounts);\n }\n return [DEFAULT_ACCOUNT_ID];\n },\n resolveAccount: (cfg, accountId) => resolveKerneliusAccount(cfg, accountId),\n defaultAccountId: () => DEFAULT_ACCOUNT_ID,\n setAccountEnabled: ({ cfg, accountId, enabled }) => {\n const effectiveAccountId = accountId || DEFAULT_ACCOUNT_ID;\n if (!cfg.channels) cfg.channels = {};\n if (!cfg.channels.kernelius) cfg.channels.kernelius = {};\n\n if (cfg.channels.kernelius.accounts?.[effectiveAccountId]) {\n cfg.channels.kernelius.accounts[effectiveAccountId].enabled = enabled;\n } else {\n cfg.channels.kernelius.enabled = enabled;\n }\n return cfg;\n },\n deleteAccount: ({ cfg, accountId }) => {\n if (accountId && accountId !== DEFAULT_ACCOUNT_ID) {\n delete cfg.channels?.kernelius?.accounts?.[accountId];\n }\n return cfg;\n },\n isConfigured: (account: any) => Boolean(account.apiKey),\n describeAccount: (account: any) => ({\n accountId: account.accountId,\n enabled: account.enabled,\n configured: Boolean(account.apiKey),\n apiUrl: account.apiUrl,\n }),\n resolveAllowFrom: () => [],\n formatAllowFrom: ({ allowFrom }) => allowFrom,\n },\n messaging: {\n // Send message to Forge (comment on issue/PR)\n send: async (ctx, action) => {\n const runtime = getKerneliusRuntime();\n const account = resolveKerneliusAccount(runtime.config.loadConfig(), action.accountId);\n\n if (!account.apiKey) {\n throw new Error(\"Kernelius API key not configured\");\n }\n\n // Parse target: repo:owner/name:issue:42 or repo:owner/name:pr:10\n const target = action.to;\n const match = target.match(/^repo:([^/]+)\\/([^:]+):(issue|pr):(\\d+)$/);\n\n if (!match) {\n throw new Error(`Invalid Kernelius target format: ${target}. Expected: repo:owner/name:issue:42 or repo:owner/name:pr:10`);\n }\n\n const [, owner, repo, type, number] = match;\n const endpoint = type === \"issue\"\n ? `/api/repositories/${owner}/${repo}/issues/${number}/comments`\n : `/api/pulls/${number}/comments`;\n\n const response = await fetch(`${account.apiUrl}${endpoint}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${account.apiKey}`,\n },\n body: JSON.stringify({\n body: action.body,\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Failed to send message to Kernelius: ${response.status} ${error}`);\n }\n\n const result = await response.json();\n\n return {\n messageId: result.id,\n timestamp: new Date(result.createdAt),\n };\n },\n\n // React to message (not implemented for Forge yet)\n react: async () => {\n throw new Error(\"Reactions not yet implemented for Kernelius Forge\");\n },\n },\n actions: {\n listActions: () => [\"send\"],\n extractToolSend: ({ args }) => {\n const action = typeof args.action === \"string\" ? args.action.trim() : \"\";\n if (action !== \"sendMessage\") {\n return null;\n }\n const to = typeof args.to === \"string\" ? args.to : undefined;\n if (!to) {\n return null;\n }\n const accountId = typeof args.accountId === \"string\" ? args.accountId.trim() : undefined;\n return { to, accountId };\n },\n handleAction: async ({ action, params, cfg, accountId }) => {\n if (action !== \"send\") {\n throw new Error(`Unknown action: ${action}`);\n }\n\n const to = typeof params.to === \"string\" ? params.to : undefined;\n const message = typeof params.message === \"string\" ? params.message : undefined;\n\n if (!to || !message) {\n throw new Error(\"Missing required parameters: to, message\");\n }\n\n const account = resolveKerneliusAccount(cfg, accountId);\n\n if (!account.apiKey) {\n throw new Error(\"Kernelius API key not configured\");\n }\n\n // Parse target and send\n const match = to.match(/^repo:([^/]+)\\/([^:]+):(issue|pr):(\\d+)$/);\n if (!match) {\n throw new Error(`Invalid target format: ${to}`);\n }\n\n const [, owner, repo, type, number] = match;\n const endpoint = type === \"issue\"\n ? `/api/repositories/${owner}/${repo}/issues/${number}/comments`\n : `/api/pulls/${number}/comments`;\n\n const response = await fetch(`${account.apiUrl}${endpoint}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${account.apiKey}`,\n },\n body: JSON.stringify({ body: message }),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send: ${response.status}`);\n }\n\n return { success: true };\n },\n },\n};\n","import type { OpenClawRuntime } from \"openclaw/plugin-sdk\";\n\nlet runtime: OpenClawRuntime | null = null;\n\nexport function setKerneliusRuntime(rt: OpenClawRuntime): void {\n runtime = rt;\n}\n\nexport function getKerneliusRuntime(): OpenClawRuntime {\n if (!runtime) {\n throw new Error(\"Kernelius runtime not initialized\");\n }\n return runtime;\n}\n"],"mappings":"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","names":["runtime"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/channel.ts","../src/runtime.ts","../src/inbound.ts"],"sourcesContent":["import type { OpenClawPluginApi } from \"openclaw/plugin-sdk\";\nimport { emptyPluginConfigSchema } from \"openclaw/plugin-sdk\";\nimport { kerneliusPlugin } from \"./channel.js\";\nimport { setKerneliusRuntime } from \"./runtime.js\";\n\nconst plugin = {\n id: \"kernelius\",\n name: \"Kernelius Forge\",\n description: \"Connect to Kernelius Forge repositories, issues, and pull requests\",\n configSchema: emptyPluginConfigSchema(),\n register(api: OpenClawPluginApi) {\n setKerneliusRuntime(api.runtime);\n api.registerChannel({ plugin: kerneliusPlugin });\n },\n};\n\nexport default plugin;\n","import type {\n ChannelPlugin,\n ChannelConfigAdapter,\n} from \"openclaw/plugin-sdk\";\nimport {\n getChatChannelMeta,\n DEFAULT_ACCOUNT_ID,\n} from \"openclaw/plugin-sdk\";\nimport { getKerneliusRuntime } from \"./runtime.js\";\nimport type { KerneliusConfig, KerneliusResolvedAccount } from \"./types.js\";\nimport {\n handleWebhookRequest,\n resolveKerneliusWebhookPath,\n} from \"./inbound.js\";\n\nconst meta = getChatChannelMeta(\"kernelius\");\n\n// Resolve Kernelius account configuration\nfunction resolveKerneliusAccount(cfg: any, accountId?: string): KerneliusResolvedAccount {\n const effectiveAccountId = accountId || DEFAULT_ACCOUNT_ID;\n const channelConfig = cfg.channels?.kernelius || {};\n\n // Support both top-level config and accounts structure\n const accountConfig = channelConfig.accounts?.[effectiveAccountId] || channelConfig;\n\n return {\n accountId: effectiveAccountId,\n enabled: accountConfig.enabled !== false,\n apiUrl: accountConfig.apiUrl || \"https://forge-api.kernelius.com\",\n apiKey: accountConfig.apiKey,\n webhookSecret: accountConfig.webhookSecret,\n webhookPath: accountConfig.webhookPath,\n webhookUrl: accountConfig.webhookUrl,\n };\n}\n\nexport const kerneliusPlugin: ChannelPlugin = {\n id: \"kernelius\",\n meta: {\n ...meta,\n name: \"Kernelius Forge\",\n emoji: \"🔥\",\n description: \"Git platform for human-agent collaboration\",\n },\n capabilities: {\n chatTypes: [\"direct\", \"channel\", \"thread\"],\n reactions: true,\n threads: true,\n media: false,\n nativeCommands: false,\n },\n reload: { configPrefixes: [\"channels.kernelius\"] },\n config: {\n listAccountIds: (cfg) => {\n const channelConfig = cfg.channels?.kernelius;\n if (!channelConfig) return [];\n if (channelConfig.accounts) {\n return Object.keys(channelConfig.accounts);\n }\n return [DEFAULT_ACCOUNT_ID];\n },\n resolveAccount: (cfg, accountId) => resolveKerneliusAccount(cfg, accountId),\n defaultAccountId: () => DEFAULT_ACCOUNT_ID,\n setAccountEnabled: ({ cfg, accountId, enabled }) => {\n const effectiveAccountId = accountId || DEFAULT_ACCOUNT_ID;\n if (!cfg.channels) cfg.channels = {};\n if (!cfg.channels.kernelius) cfg.channels.kernelius = {};\n\n if (cfg.channels.kernelius.accounts?.[effectiveAccountId]) {\n cfg.channels.kernelius.accounts[effectiveAccountId].enabled = enabled;\n } else {\n cfg.channels.kernelius.enabled = enabled;\n }\n return cfg;\n },\n deleteAccount: ({ cfg, accountId }) => {\n if (accountId && accountId !== DEFAULT_ACCOUNT_ID) {\n delete cfg.channels?.kernelius?.accounts?.[accountId];\n }\n return cfg;\n },\n isConfigured: (account: any) => Boolean(account.apiKey),\n describeAccount: (account: any) => ({\n accountId: account.accountId,\n enabled: account.enabled,\n configured: Boolean(account.apiKey),\n apiUrl: account.apiUrl,\n }),\n resolveAllowFrom: () => [],\n formatAllowFrom: ({ allowFrom }) => allowFrom,\n },\n messaging: {\n // Send message to Forge (comment on issue/PR)\n send: async (ctx, action) => {\n const runtime = getKerneliusRuntime();\n const account = resolveKerneliusAccount(runtime.config.loadConfig(), action.accountId);\n\n if (!account.apiKey) {\n throw new Error(\"Kernelius API key not configured\");\n }\n\n // Parse target: repo:owner/name:issue:42 or repo:owner/name:pr:10\n const target = action.to;\n const match = target.match(/^repo:([^/]+)\\/([^:]+):(issue|pr):(\\d+)$/);\n\n if (!match) {\n throw new Error(`Invalid Kernelius target format: ${target}. Expected: repo:owner/name:issue:42 or repo:owner/name:pr:10`);\n }\n\n const [, owner, repo, type, number] = match;\n const endpoint = type === \"issue\"\n ? `/api/repositories/${owner}/${repo}/issues/${number}/comments`\n : `/api/pulls/${number}/comments`;\n\n const response = await fetch(`${account.apiUrl}${endpoint}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${account.apiKey}`,\n },\n body: JSON.stringify({\n body: action.body,\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Failed to send message to Kernelius: ${response.status} ${error}`);\n }\n\n const result = await response.json();\n\n return {\n messageId: result.id,\n timestamp: new Date(result.createdAt),\n };\n },\n\n // React to message (not implemented for Forge yet)\n react: async () => {\n throw new Error(\"Reactions not yet implemented for Kernelius Forge\");\n },\n },\n actions: {\n listActions: () => [\"send\"],\n extractToolSend: ({ args }) => {\n const action = typeof args.action === \"string\" ? args.action.trim() : \"\";\n if (action !== \"sendMessage\") {\n return null;\n }\n const to = typeof args.to === \"string\" ? args.to : undefined;\n if (!to) {\n return null;\n }\n const accountId = typeof args.accountId === \"string\" ? args.accountId.trim() : undefined;\n return { to, accountId };\n },\n handleAction: async ({ action, params, cfg, accountId }) => {\n if (action !== \"send\") {\n throw new Error(`Unknown action: ${action}`);\n }\n\n const to = typeof params.to === \"string\" ? params.to : undefined;\n const message = typeof params.message === \"string\" ? params.message : undefined;\n\n if (!to || !message) {\n throw new Error(\"Missing required parameters: to, message\");\n }\n\n const account = resolveKerneliusAccount(cfg, accountId);\n\n if (!account.apiKey) {\n throw new Error(\"Kernelius API key not configured\");\n }\n\n // Parse target and send\n const match = to.match(/^repo:([^/]+)\\/([^:]+):(issue|pr):(\\d+)$/);\n if (!match) {\n throw new Error(`Invalid target format: ${to}`);\n }\n\n const [, owner, repo, type, number] = match;\n const endpoint = type === \"issue\"\n ? `/api/repositories/${owner}/${repo}/issues/${number}/comments`\n : `/api/pulls/${number}/comments`;\n\n const response = await fetch(`${account.apiUrl}${endpoint}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${account.apiKey}`,\n },\n body: JSON.stringify({ body: message }),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to send: ${response.status}`);\n }\n\n return { success: true };\n },\n },\n gateway: {\n start: async (ctx) => {\n const runtime = getKerneliusRuntime();\n const config = runtime.config.loadConfig();\n const account = resolveKerneliusAccount(config, ctx.accountId);\n\n // Only start gateway if webhook path/url is configured\n if (!account.webhookPath && !account.webhookUrl) {\n console.log(\"[kernelius] No webhook path configured, skipping gateway start\");\n return;\n }\n\n const webhookPath = resolveKerneliusWebhookPath(\n account.webhookPath,\n account.webhookUrl\n );\n\n console.log(`[kernelius] Starting gateway for account ${account.accountId} at ${webhookPath}`);\n\n // Register HTTP handler for webhooks\n ctx.registerHttpHandler({\n path: webhookPath,\n method: \"POST\",\n handler: async (req, res) => {\n try {\n const inbound = await handleWebhookRequest(req, res, {\n account,\n runtime,\n statusSink: ctx.statusSink,\n });\n\n if (inbound) {\n // Queue message for agent processing\n await ctx.queueInbound(inbound);\n }\n } catch (error) {\n console.error(\"[kernelius] Webhook handler error:\", error);\n if (!res.headersSent) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Internal server error\" }));\n }\n }\n },\n });\n\n console.log(`[kernelius] Gateway started successfully at ${webhookPath}`);\n },\n stop: async (ctx) => {\n console.log(`[kernelius] Stopping gateway for account ${ctx.accountId}`);\n // Cleanup is handled by OpenClaw when unregistering handlers\n },\n },\n};\n","import type { OpenClawRuntime } from \"openclaw/plugin-sdk\";\n\nlet runtime: OpenClawRuntime | null = null;\n\nexport function setKerneliusRuntime(rt: OpenClawRuntime): void {\n runtime = rt;\n}\n\nexport function getKerneliusRuntime(): OpenClawRuntime {\n if (!runtime) {\n throw new Error(\"Kernelius runtime not initialized\");\n }\n return runtime;\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { WebInboundMessage } from \"openclaw/plugin-sdk\";\nimport type { ForgeWebhookPayload, KerneliusResolvedAccount } from \"./types.js\";\nimport crypto from \"node:crypto\";\n\nexport interface InboundHandlerContext {\n account: KerneliusResolvedAccount;\n runtime: any;\n statusSink?: (patch: { lastInboundAt?: number }) => void;\n}\n\n/**\n * Verify webhook signature\n */\nexport function verifyWebhookSignature(\n payload: string,\n signature: string,\n secret: string\n): boolean {\n if (!secret) {\n return true; // No secret configured, skip verification\n }\n\n const expectedSignature = `sha256=${crypto\n .createHmac(\"sha256\", secret)\n .update(payload)\n .digest(\"hex\")}`;\n\n return crypto.timingSafeEqual(\n Buffer.from(signature),\n Buffer.from(expectedSignature)\n );\n}\n\n/**\n * Convert Forge webhook payload to WebInboundMessage\n */\nexport function forgePayloadToInbound(\n payload: ForgeWebhookPayload,\n accountId: string\n): WebInboundMessage | null {\n const { event, repository, sender, issue, pullRequest, comment } = payload;\n\n // Determine conversation ID based on event type\n let conversationId: string;\n let body: string;\n let chatType: \"direct\" | \"group\" = \"channel\";\n\n if (issue) {\n conversationId = `repo:${repository.fullName}:issue:${issue.number}`;\n\n if (event === \"issue.created\") {\n body = `**New Issue #${issue.number}**: ${issue.title}\\n\\n${issue.body || \"\"}`;\n } else if (event === \"issue.commented\" && comment) {\n body = `**Comment on Issue #${issue.number}** by @${sender.username}:\\n\\n${comment.body}`;\n } else if (event === \"issue.closed\") {\n body = `**Issue #${issue.number} closed** by @${sender.username}`;\n } else if (event === \"issue.reopened\") {\n body = `**Issue #${issue.number} reopened** by @${sender.username}`;\n } else if (event === \"issue.updated\") {\n body = `**Issue #${issue.number} updated** by @${sender.username}: ${issue.title}`;\n } else {\n body = `Issue #${issue.number} event: ${event}`;\n }\n } else if (pullRequest) {\n conversationId = `repo:${repository.fullName}:pr:${pullRequest.number}`;\n\n if (event === \"pr.created\") {\n body = `**New Pull Request #${pullRequest.number}**: ${pullRequest.title}\\n\\n${pullRequest.body || \"\"}`;\n } else if (event === \"pr.review_requested\") {\n body = `**Review Requested on PR #${pullRequest.number}**: ${pullRequest.title}\\n\\nPlease review this pull request.`;\n } else if (event === \"pr.reviewed\") {\n body = `**PR #${pullRequest.number} reviewed** by @${sender.username}`;\n } else if (event === \"pr.merged\") {\n body = `**PR #${pullRequest.number} merged** by @${sender.username}`;\n } else if (event === \"pr.commented\" && comment) {\n body = `**Comment on PR #${pullRequest.number}** by @${sender.username}:\\n\\n${comment.body}`;\n } else {\n body = `Pull Request #${pullRequest.number} event: ${event}`;\n }\n } else {\n // Repository-level event\n conversationId = `repo:${repository.fullName}`;\n body = `Repository event: ${event}`;\n }\n\n const timestamp = new Date(payload.timestamp).getTime();\n\n return {\n id: crypto.randomUUID(),\n from: conversationId,\n conversationId,\n to: accountId,\n accountId,\n body,\n pushName: sender.username,\n timestamp,\n chatType,\n chatId: conversationId,\n senderName: sender.username,\n selfJid: accountId,\n selfE164: null,\n // Helpers (simplified for webhook-based channel)\n sendComposing: async () => {},\n reply: async (text: string) => {\n // Would send back to Forge, but we handle that via messaging adapter\n console.log(`[kernelius] Reply queued: ${text}`);\n },\n sendMedia: async () => {\n throw new Error(\"Media not supported for Forge webhooks\");\n },\n };\n}\n\n/**\n * Handle incoming webhook request\n */\nexport async function handleWebhookRequest(\n req: IncomingMessage,\n res: ServerResponse,\n ctx: InboundHandlerContext\n): Promise<WebInboundMessage | null> {\n // Read request body\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(chunk as Buffer);\n }\n const rawBody = Buffer.concat(chunks).toString(\"utf-8\");\n\n // Verify signature if secret is configured\n const signature = req.headers[\"x-forge-signature\"] as string;\n if (ctx.account.webhookSecret) {\n if (!signature) {\n res.writeHead(401, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing X-Forge-Signature header\" }));\n return null;\n }\n\n if (!verifyWebhookSignature(rawBody, signature, ctx.account.webhookSecret)) {\n res.writeHead(401, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid signature\" }));\n return null;\n }\n }\n\n // Parse payload\n let payload: ForgeWebhookPayload;\n try {\n payload = JSON.parse(rawBody);\n } catch (error) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid JSON payload\" }));\n return null;\n }\n\n // Verify source is from Forge\n if (payload.source !== \"forge\") {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid source\" }));\n return null;\n }\n\n // Update status\n ctx.statusSink?.({ lastInboundAt: Date.now() });\n\n // Convert to WebInboundMessage\n const inbound = forgePayloadToInbound(payload, ctx.account.accountId);\n\n if (!inbound) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Could not process event\" }));\n return null;\n }\n\n // Respond success\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ success: true, event: payload.event }));\n\n return inbound;\n}\n\n/**\n * Resolve webhook path from config\n */\nexport function resolveKerneliusWebhookPath(\n webhookPath?: string,\n webhookUrl?: string\n): string {\n if (webhookPath?.trim()) {\n const path = webhookPath.trim();\n return path.startsWith(\"/\") ? path : `/${path}`;\n }\n\n if (webhookUrl?.trim()) {\n try {\n const parsed = new URL(webhookUrl);\n return parsed.pathname || \"/kernelius\";\n } catch {\n return \"/kernelius\";\n }\n }\n\n return \"/kernelius\";\n}\n"],"mappings":"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","names":["runtime"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernelius/openclaw-plugin",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "OpenClaw channel plugin for Kernelius Forge - enables agents to work with repositories, issues, and pull requests",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -41,11 +41,13 @@
|
|
|
41
41
|
"typescript": "^5.7.2"
|
|
42
42
|
},
|
|
43
43
|
"openclaw": {
|
|
44
|
-
"extensions": [
|
|
44
|
+
"extensions": [
|
|
45
|
+
"./dist/index.js"
|
|
46
|
+
]
|
|
45
47
|
},
|
|
46
48
|
"repository": {
|
|
47
49
|
"type": "git",
|
|
48
|
-
"url": "https://github.com/kernelius-hq/openclaw-kernelius-plugin.git"
|
|
50
|
+
"url": "git+https://github.com/kernelius-hq/openclaw-kernelius-plugin.git"
|
|
49
51
|
},
|
|
50
52
|
"bugs": {
|
|
51
53
|
"url": "https://github.com/kernelius-hq/openclaw-kernelius-plugin/issues"
|