@useorgx/openclaw-plugin 0.4.3 → 0.4.5
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 +72 -2
- package/dashboard/dist/assets/B68j2crt.js +1 -0
- package/dashboard/dist/assets/BZZ-fiJx.js +32 -0
- package/dashboard/dist/assets/{CE5pVdev.js → BoXlCHKa.js} +1 -1
- package/dashboard/dist/assets/Bq9x_Xyh.css +1 -0
- package/dashboard/dist/assets/DBhrRVdp.js +1 -0
- package/dashboard/dist/assets/DD1jv1Hd.js +8 -0
- package/dashboard/dist/assets/DNjbmawF.js +214 -0
- package/dashboard/dist/index.html +5 -5
- package/dist/contracts/client.js +22 -2
- package/dist/http-handler.js +14 -5
- package/dist/index.js +259 -26
- package/dist/mcp-client-setup.d.ts +30 -0
- package/dist/mcp-client-setup.js +215 -0
- package/dist/mcp-http-handler.d.ts +38 -0
- package/dist/mcp-http-handler.js +254 -0
- package/dist/telemetry/posthog.d.ts +8 -0
- package/dist/telemetry/posthog.js +81 -0
- package/package.json +1 -1
- package/dashboard/dist/assets/BqukHQH-.js +0 -8
- package/dashboard/dist/assets/Cpr7n8fE.js +0 -1
- package/dashboard/dist/assets/Nip3CrNC.js +0 -1
- package/dashboard/dist/assets/TN5wE36J.js +0 -1
- package/dashboard/dist/assets/X6IcjS74.js +0 -212
- package/dashboard/dist/assets/jyFhCND-.css +0 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { writeFileAtomicSync, writeJsonFileAtomicSync } from "./fs-utils.js";
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
8
|
+
}
|
|
9
|
+
function parseJsonObjectSafe(raw) {
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
return isRecord(parsed) ? parsed : null;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function fileModeOrDefault(path, fallback) {
|
|
19
|
+
try {
|
|
20
|
+
const stat = statSync(path);
|
|
21
|
+
return stat.mode & 0o777;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function backupPath(path) {
|
|
28
|
+
return `${path}.bak.${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
29
|
+
}
|
|
30
|
+
function backupFileSync(path, mode) {
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(path);
|
|
33
|
+
const next = backupPath(path);
|
|
34
|
+
writeFileAtomicSync(next, content.toString("utf8"), { mode, encoding: "utf8" });
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function patchClaudeMcpConfig(input) {
|
|
42
|
+
const currentServers = isRecord(input.current.mcpServers) ? input.current.mcpServers : {};
|
|
43
|
+
const currentOrgx = isRecord(currentServers.orgx) ? currentServers.orgx : {};
|
|
44
|
+
const priorUrl = typeof currentOrgx.url === "string" ? currentOrgx.url : "";
|
|
45
|
+
const priorType = typeof currentOrgx.type === "string" ? currentOrgx.type : "";
|
|
46
|
+
const nextOrgx = {
|
|
47
|
+
...currentOrgx,
|
|
48
|
+
type: "http",
|
|
49
|
+
url: input.localMcpUrl,
|
|
50
|
+
description: typeof currentOrgx.description === "string" && currentOrgx.description.trim().length > 0
|
|
51
|
+
? currentOrgx.description
|
|
52
|
+
: "OrgX platform via local OpenClaw plugin (no OAuth)",
|
|
53
|
+
};
|
|
54
|
+
const nextServers = {
|
|
55
|
+
...currentServers,
|
|
56
|
+
orgx: nextOrgx,
|
|
57
|
+
};
|
|
58
|
+
const next = {
|
|
59
|
+
...input.current,
|
|
60
|
+
mcpServers: nextServers,
|
|
61
|
+
};
|
|
62
|
+
const updated = priorUrl !== input.localMcpUrl || priorType !== "http";
|
|
63
|
+
return { updated, next };
|
|
64
|
+
}
|
|
65
|
+
export function patchCursorMcpConfig(input) {
|
|
66
|
+
const currentServers = isRecord(input.current.mcpServers) ? input.current.mcpServers : {};
|
|
67
|
+
const key = "orgx-openclaw";
|
|
68
|
+
const existing = isRecord(currentServers[key]) ? currentServers[key] : {};
|
|
69
|
+
const priorUrl = typeof existing.url === "string" ? existing.url : "";
|
|
70
|
+
const nextEntry = {
|
|
71
|
+
...existing,
|
|
72
|
+
url: input.localMcpUrl,
|
|
73
|
+
};
|
|
74
|
+
const nextServers = {
|
|
75
|
+
...currentServers,
|
|
76
|
+
[key]: nextEntry,
|
|
77
|
+
};
|
|
78
|
+
const next = {
|
|
79
|
+
...input.current,
|
|
80
|
+
mcpServers: nextServers,
|
|
81
|
+
};
|
|
82
|
+
const updated = priorUrl !== input.localMcpUrl;
|
|
83
|
+
return { updated, next };
|
|
84
|
+
}
|
|
85
|
+
export function patchCodexConfigToml(input) {
|
|
86
|
+
const lines = input.current.split(/\r?\n/);
|
|
87
|
+
const headerRegex = /^\[mcp_servers\.(?:orgx|"orgx")\]\s*$/;
|
|
88
|
+
let headerIndex = -1;
|
|
89
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
90
|
+
if (headerRegex.test(lines[i].trim())) {
|
|
91
|
+
headerIndex = i;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const urlLine = `url = "${input.localMcpUrl}"`;
|
|
96
|
+
if (headerIndex === -1) {
|
|
97
|
+
const suffix = [
|
|
98
|
+
"",
|
|
99
|
+
"[mcp_servers.orgx]",
|
|
100
|
+
urlLine,
|
|
101
|
+
"",
|
|
102
|
+
].join("\n");
|
|
103
|
+
const normalized = input.current.endsWith("\n") ? input.current : `${input.current}\n`;
|
|
104
|
+
return { updated: true, next: `${normalized}${suffix}` };
|
|
105
|
+
}
|
|
106
|
+
let sectionEnd = lines.length;
|
|
107
|
+
for (let i = headerIndex + 1; i < lines.length; i += 1) {
|
|
108
|
+
if (lines[i].trim().startsWith("[")) {
|
|
109
|
+
sectionEnd = i;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
let updated = false;
|
|
114
|
+
let urlIndex = -1;
|
|
115
|
+
for (let i = headerIndex + 1; i < sectionEnd; i += 1) {
|
|
116
|
+
if (/^\s*url\s*=/.test(lines[i])) {
|
|
117
|
+
urlIndex = i;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (urlIndex >= 0) {
|
|
122
|
+
if (lines[urlIndex].trim() !== urlLine) {
|
|
123
|
+
lines[urlIndex] = urlLine;
|
|
124
|
+
updated = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
lines.splice(headerIndex + 1, 0, urlLine);
|
|
129
|
+
updated = true;
|
|
130
|
+
}
|
|
131
|
+
return { updated, next: `${lines.join("\n")}\n` };
|
|
132
|
+
}
|
|
133
|
+
export async function autoConfigureDetectedMcpClients(input) {
|
|
134
|
+
const logger = input.logger ?? {};
|
|
135
|
+
const home = input.homeDir ?? homedir();
|
|
136
|
+
const updatedPaths = [];
|
|
137
|
+
const skippedPaths = [];
|
|
138
|
+
const targets = [
|
|
139
|
+
{ kind: "claude", path: join(home, ".claude", "mcp.json") },
|
|
140
|
+
{ kind: "cursor", path: join(home, ".cursor", "mcp.json") },
|
|
141
|
+
{ kind: "codex", path: join(home, ".codex", "config.toml") },
|
|
142
|
+
];
|
|
143
|
+
for (const target of targets) {
|
|
144
|
+
if (!existsSync(target.path)) {
|
|
145
|
+
skippedPaths.push(target.path);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const mode = fileModeOrDefault(target.path, 0o600);
|
|
149
|
+
try {
|
|
150
|
+
if (target.kind === "codex") {
|
|
151
|
+
const current = readFileSync(target.path, "utf8");
|
|
152
|
+
const patched = patchCodexConfigToml({ current, localMcpUrl: input.localMcpUrl });
|
|
153
|
+
if (!patched.updated) {
|
|
154
|
+
skippedPaths.push(target.path);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const backup = backupFileSync(target.path, mode);
|
|
158
|
+
if (!backup) {
|
|
159
|
+
logger.warn?.("[orgx] MCP client autoconfig: backup failed; skipping", {
|
|
160
|
+
path: target.path,
|
|
161
|
+
kind: target.kind,
|
|
162
|
+
});
|
|
163
|
+
skippedPaths.push(target.path);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
writeFileAtomicSync(target.path, patched.next, { mode, encoding: "utf8" });
|
|
167
|
+
updatedPaths.push(target.path);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const currentText = readFileSync(target.path, "utf8");
|
|
171
|
+
const current = parseJsonObjectSafe(currentText);
|
|
172
|
+
if (!current) {
|
|
173
|
+
logger.warn?.("[orgx] MCP client autoconfig: invalid JSON; skipping", {
|
|
174
|
+
path: target.path,
|
|
175
|
+
kind: target.kind,
|
|
176
|
+
});
|
|
177
|
+
skippedPaths.push(target.path);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const patched = target.kind === "claude"
|
|
181
|
+
? patchClaudeMcpConfig({ current, localMcpUrl: input.localMcpUrl })
|
|
182
|
+
: patchCursorMcpConfig({ current, localMcpUrl: input.localMcpUrl });
|
|
183
|
+
if (!patched.updated) {
|
|
184
|
+
skippedPaths.push(target.path);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const backup = backupFileSync(target.path, mode);
|
|
188
|
+
if (!backup) {
|
|
189
|
+
logger.warn?.("[orgx] MCP client autoconfig: backup failed; skipping", {
|
|
190
|
+
path: target.path,
|
|
191
|
+
kind: target.kind,
|
|
192
|
+
});
|
|
193
|
+
skippedPaths.push(target.path);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
writeJsonFileAtomicSync(target.path, patched.next, mode);
|
|
197
|
+
updatedPaths.push(target.path);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
logger.warn?.("[orgx] MCP client autoconfig failed; leaving backup in place", {
|
|
201
|
+
path: target.path,
|
|
202
|
+
kind: target.kind,
|
|
203
|
+
error: err instanceof Error ? err.message : String(err),
|
|
204
|
+
});
|
|
205
|
+
skippedPaths.push(target.path);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (updatedPaths.length > 0) {
|
|
209
|
+
logger.info?.("[orgx] MCP client autoconfig applied", {
|
|
210
|
+
localMcpUrl: input.localMcpUrl,
|
|
211
|
+
updated: updatedPaths,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return { updatedPaths, skippedPaths };
|
|
215
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type Logger = {
|
|
2
|
+
info?: (message: string, meta?: Record<string, unknown>) => void;
|
|
3
|
+
warn?: (message: string, meta?: Record<string, unknown>) => void;
|
|
4
|
+
debug?: (message: string, meta?: Record<string, unknown>) => void;
|
|
5
|
+
};
|
|
6
|
+
export interface PluginRequest {
|
|
7
|
+
method?: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
headers: Record<string, string | string[] | undefined>;
|
|
10
|
+
body?: unknown;
|
|
11
|
+
on?: (event: string, listener: (...args: unknown[]) => void) => void;
|
|
12
|
+
once?: (event: string, listener: (...args: unknown[]) => void) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface PluginResponse {
|
|
15
|
+
writeHead(status: number, headers?: Record<string, string>): void;
|
|
16
|
+
end(body?: string | Buffer): void;
|
|
17
|
+
write?(chunk: string | Buffer): boolean | void;
|
|
18
|
+
writableEnded?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export type ToolResult = {
|
|
21
|
+
content: Array<{
|
|
22
|
+
type: "text";
|
|
23
|
+
text: string;
|
|
24
|
+
}>;
|
|
25
|
+
isError?: boolean;
|
|
26
|
+
};
|
|
27
|
+
export type RegisteredTool = {
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
parameters: Record<string, unknown>;
|
|
31
|
+
execute: (callId: string, params?: unknown) => Promise<ToolResult>;
|
|
32
|
+
};
|
|
33
|
+
export declare function createMcpHttpHandler(input: {
|
|
34
|
+
tools: Map<string, RegisteredTool>;
|
|
35
|
+
logger?: Logger;
|
|
36
|
+
serverName: string;
|
|
37
|
+
serverVersion: string;
|
|
38
|
+
}): (req: PluginRequest, res: PluginResponse) => Promise<boolean>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
const DEFAULT_PROTOCOL_VERSION = "2024-11-05";
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
5
|
+
}
|
|
6
|
+
function sendJson(res, status, payload) {
|
|
7
|
+
res.writeHead(status, {
|
|
8
|
+
"content-type": "application/json; charset=utf-8",
|
|
9
|
+
"cache-control": "no-store",
|
|
10
|
+
});
|
|
11
|
+
res.end(JSON.stringify(payload));
|
|
12
|
+
}
|
|
13
|
+
function sendText(res, status, body) {
|
|
14
|
+
res.writeHead(status, {
|
|
15
|
+
"content-type": "text/plain; charset=utf-8",
|
|
16
|
+
"cache-control": "no-store",
|
|
17
|
+
});
|
|
18
|
+
res.end(body);
|
|
19
|
+
}
|
|
20
|
+
function jsonRpcError(id, code, message) {
|
|
21
|
+
return {
|
|
22
|
+
jsonrpc: "2.0",
|
|
23
|
+
id,
|
|
24
|
+
error: {
|
|
25
|
+
code,
|
|
26
|
+
message,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function jsonRpcResult(id, result) {
|
|
31
|
+
return {
|
|
32
|
+
jsonrpc: "2.0",
|
|
33
|
+
id,
|
|
34
|
+
result,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function normalizePath(rawUrl) {
|
|
38
|
+
const [path] = rawUrl.split("?", 2);
|
|
39
|
+
return path || "/";
|
|
40
|
+
}
|
|
41
|
+
async function readRequestBodyBuffer(req) {
|
|
42
|
+
const body = req.body;
|
|
43
|
+
if (typeof body === "string")
|
|
44
|
+
return Buffer.from(body, "utf8");
|
|
45
|
+
if (Buffer.isBuffer(body))
|
|
46
|
+
return body;
|
|
47
|
+
if (body instanceof Uint8Array)
|
|
48
|
+
return Buffer.from(body);
|
|
49
|
+
if (body instanceof ArrayBuffer)
|
|
50
|
+
return Buffer.from(body);
|
|
51
|
+
if (body && typeof body === "object" && !Buffer.isBuffer(body)) {
|
|
52
|
+
try {
|
|
53
|
+
return Buffer.from(JSON.stringify(body), "utf8");
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return Buffer.from("", "utf8");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (typeof req.on !== "function")
|
|
60
|
+
return Buffer.from("", "utf8");
|
|
61
|
+
return await new Promise((resolve) => {
|
|
62
|
+
const chunks = [];
|
|
63
|
+
const onData = (chunk) => {
|
|
64
|
+
if (typeof chunk === "string") {
|
|
65
|
+
chunks.push(Buffer.from(chunk, "utf8"));
|
|
66
|
+
}
|
|
67
|
+
else if (Buffer.isBuffer(chunk)) {
|
|
68
|
+
chunks.push(chunk);
|
|
69
|
+
}
|
|
70
|
+
else if (chunk instanceof Uint8Array) {
|
|
71
|
+
chunks.push(Buffer.from(chunk));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const onEnd = () => resolve(Buffer.concat(chunks));
|
|
75
|
+
const onError = () => resolve(Buffer.concat(chunks));
|
|
76
|
+
req.on?.("data", onData);
|
|
77
|
+
req.on?.("end", onEnd);
|
|
78
|
+
req.on?.("error", onError);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function parseJsonBody(req) {
|
|
82
|
+
const buffer = await readRequestBodyBuffer(req);
|
|
83
|
+
if (!buffer || buffer.length === 0)
|
|
84
|
+
return null;
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(buffer.toString("utf8"));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function pickId(value) {
|
|
93
|
+
if (typeof value === "string")
|
|
94
|
+
return value;
|
|
95
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
96
|
+
return value;
|
|
97
|
+
if (value === null)
|
|
98
|
+
return null;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function normalizeToolArguments(value) {
|
|
102
|
+
if (isRecord(value))
|
|
103
|
+
return value;
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
function buildToolsList(tools) {
|
|
107
|
+
const entries = Array.from(tools.values())
|
|
108
|
+
.map((tool) => ({
|
|
109
|
+
name: tool.name,
|
|
110
|
+
description: tool.description,
|
|
111
|
+
inputSchema: tool.parameters,
|
|
112
|
+
}))
|
|
113
|
+
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
114
|
+
return entries;
|
|
115
|
+
}
|
|
116
|
+
async function handleRpcMessage(input) {
|
|
117
|
+
const msg = input.message;
|
|
118
|
+
if (!isRecord(msg)) {
|
|
119
|
+
return jsonRpcError(null, -32600, "Invalid Request");
|
|
120
|
+
}
|
|
121
|
+
const id = pickId(msg.id);
|
|
122
|
+
const method = typeof msg.method === "string" ? msg.method.trim() : "";
|
|
123
|
+
if (!method) {
|
|
124
|
+
return jsonRpcError(id, -32600, "Invalid Request");
|
|
125
|
+
}
|
|
126
|
+
const params = isRecord(msg.params) ? msg.params : {};
|
|
127
|
+
// Notifications do not receive a response.
|
|
128
|
+
if (id === null && method.startsWith("notifications/")) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (method === "initialize") {
|
|
132
|
+
const requestedProtocol = typeof params.protocolVersion === "string" ? params.protocolVersion : null;
|
|
133
|
+
const protocolVersion = requestedProtocol?.trim() || DEFAULT_PROTOCOL_VERSION;
|
|
134
|
+
return jsonRpcResult(id, {
|
|
135
|
+
protocolVersion,
|
|
136
|
+
capabilities: {
|
|
137
|
+
tools: {},
|
|
138
|
+
},
|
|
139
|
+
serverInfo: {
|
|
140
|
+
name: input.serverName,
|
|
141
|
+
version: input.serverVersion,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (method === "ping") {
|
|
146
|
+
return jsonRpcResult(id, { ok: true });
|
|
147
|
+
}
|
|
148
|
+
if (method === "tools/list") {
|
|
149
|
+
return jsonRpcResult(id, {
|
|
150
|
+
tools: buildToolsList(input.tools),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (method === "tools/call") {
|
|
154
|
+
const toolName = typeof params.name === "string" ? params.name.trim() : "";
|
|
155
|
+
if (!toolName) {
|
|
156
|
+
return jsonRpcError(id, -32602, "Missing tool name");
|
|
157
|
+
}
|
|
158
|
+
const tool = input.tools.get(toolName) ?? null;
|
|
159
|
+
if (!tool) {
|
|
160
|
+
return jsonRpcError(id, -32601, `Tool not found: ${toolName}`);
|
|
161
|
+
}
|
|
162
|
+
const args = normalizeToolArguments(params.arguments);
|
|
163
|
+
try {
|
|
164
|
+
const callId = `mcp-${id ?? randomUUID()}`;
|
|
165
|
+
const result = await tool.execute(callId, args);
|
|
166
|
+
return jsonRpcResult(id, {
|
|
167
|
+
content: Array.isArray(result?.content) ? result.content : [],
|
|
168
|
+
isError: result?.isError === true,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
input.logger.warn?.("[orgx] Local MCP tool call failed", {
|
|
173
|
+
tool: toolName,
|
|
174
|
+
error: err instanceof Error ? err.message : String(err),
|
|
175
|
+
});
|
|
176
|
+
return jsonRpcResult(id, {
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: "text",
|
|
180
|
+
text: `❌ Tool execution failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
isError: true,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (method === "resources/list") {
|
|
188
|
+
return jsonRpcResult(id, { resources: [] });
|
|
189
|
+
}
|
|
190
|
+
if (method === "prompts/list") {
|
|
191
|
+
return jsonRpcResult(id, { prompts: [] });
|
|
192
|
+
}
|
|
193
|
+
if (method.startsWith("notifications/")) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
return jsonRpcError(id, -32601, `Method not found: ${method}`);
|
|
197
|
+
}
|
|
198
|
+
export function createMcpHttpHandler(input) {
|
|
199
|
+
const logger = input.logger ?? {};
|
|
200
|
+
return async function handler(req, res) {
|
|
201
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
202
|
+
const rawUrl = req.url ?? "/";
|
|
203
|
+
const url = normalizePath(rawUrl);
|
|
204
|
+
if (!(url === "/orgx/mcp" || url.startsWith("/orgx/mcp/"))) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (method === "OPTIONS") {
|
|
208
|
+
res.writeHead(204, {
|
|
209
|
+
"cache-control": "no-store",
|
|
210
|
+
});
|
|
211
|
+
res.end();
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (method === "GET") {
|
|
215
|
+
sendText(res, 200, "OrgX Local MCP bridge is running.\n");
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
if (method !== "POST") {
|
|
219
|
+
sendJson(res, 405, {
|
|
220
|
+
error: "Use POST /orgx/mcp",
|
|
221
|
+
});
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
const payload = await parseJsonBody(req);
|
|
225
|
+
if (!payload) {
|
|
226
|
+
sendJson(res, 400, {
|
|
227
|
+
error: "Invalid JSON body",
|
|
228
|
+
});
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
const messages = Array.isArray(payload) ? payload : [payload];
|
|
232
|
+
const responses = [];
|
|
233
|
+
for (const message of messages) {
|
|
234
|
+
const response = await handleRpcMessage({
|
|
235
|
+
message,
|
|
236
|
+
tools: input.tools,
|
|
237
|
+
logger,
|
|
238
|
+
serverName: input.serverName,
|
|
239
|
+
serverVersion: input.serverVersion,
|
|
240
|
+
});
|
|
241
|
+
if (response)
|
|
242
|
+
responses.push(response);
|
|
243
|
+
}
|
|
244
|
+
if (responses.length === 0) {
|
|
245
|
+
res.writeHead(204, {
|
|
246
|
+
"cache-control": "no-store",
|
|
247
|
+
});
|
|
248
|
+
res.end();
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
sendJson(res, 200, Array.isArray(payload) ? responses : responses[0]);
|
|
252
|
+
return true;
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function isOrgxTelemetryDisabled(): boolean;
|
|
2
|
+
export declare function resolvePosthogApiKey(): string | null;
|
|
3
|
+
export declare function resolvePosthogHost(): string;
|
|
4
|
+
export declare function posthogCapture(input: {
|
|
5
|
+
event: string;
|
|
6
|
+
distinctId: string;
|
|
7
|
+
properties?: Record<string, unknown>;
|
|
8
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const POSTHOG_DEFAULT_API_KEY = "phc_s4KPgkYEFZgvkMYw4zXG41H5FN6haVwbEWPYHfNjxOc";
|
|
2
|
+
const POSTHOG_DEFAULT_HOST = "https://us.i.posthog.com";
|
|
3
|
+
function isTruthyEnv(value) {
|
|
4
|
+
if (!value)
|
|
5
|
+
return false;
|
|
6
|
+
switch (value.trim().toLowerCase()) {
|
|
7
|
+
case "1":
|
|
8
|
+
case "true":
|
|
9
|
+
case "yes":
|
|
10
|
+
case "y":
|
|
11
|
+
case "on":
|
|
12
|
+
return true;
|
|
13
|
+
default:
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function isOrgxTelemetryDisabled() {
|
|
18
|
+
return (isTruthyEnv(process.env.ORGX_TELEMETRY_DISABLED) ||
|
|
19
|
+
isTruthyEnv(process.env.OPENCLAW_TELEMETRY_DISABLED) ||
|
|
20
|
+
isTruthyEnv(process.env.POSTHOG_DISABLED));
|
|
21
|
+
}
|
|
22
|
+
export function resolvePosthogApiKey() {
|
|
23
|
+
const fromEnv = process.env.ORGX_POSTHOG_API_KEY ??
|
|
24
|
+
process.env.POSTHOG_API_KEY ??
|
|
25
|
+
process.env.ORGX_POSTHOG_KEY ??
|
|
26
|
+
process.env.POSTHOG_KEY ??
|
|
27
|
+
"";
|
|
28
|
+
const trimmed = fromEnv.trim();
|
|
29
|
+
if (trimmed)
|
|
30
|
+
return trimmed;
|
|
31
|
+
return POSTHOG_DEFAULT_API_KEY;
|
|
32
|
+
}
|
|
33
|
+
export function resolvePosthogHost() {
|
|
34
|
+
const fromEnv = process.env.ORGX_POSTHOG_HOST ??
|
|
35
|
+
process.env.POSTHOG_HOST ??
|
|
36
|
+
process.env.ORGX_POSTHOG_API_HOST ??
|
|
37
|
+
process.env.POSTHOG_API_HOST ??
|
|
38
|
+
"";
|
|
39
|
+
const trimmed = fromEnv.trim();
|
|
40
|
+
return trimmed || POSTHOG_DEFAULT_HOST;
|
|
41
|
+
}
|
|
42
|
+
function toPosthogBatchUrl(host) {
|
|
43
|
+
try {
|
|
44
|
+
return new URL("/batch/", host).toString();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return `${POSTHOG_DEFAULT_HOST}/batch/`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function posthogCapture(input) {
|
|
51
|
+
if (isOrgxTelemetryDisabled())
|
|
52
|
+
return;
|
|
53
|
+
const apiKey = resolvePosthogApiKey();
|
|
54
|
+
if (!apiKey)
|
|
55
|
+
return;
|
|
56
|
+
const url = toPosthogBatchUrl(resolvePosthogHost());
|
|
57
|
+
const now = new Date().toISOString();
|
|
58
|
+
const body = {
|
|
59
|
+
api_key: apiKey,
|
|
60
|
+
batch: [
|
|
61
|
+
{
|
|
62
|
+
type: "capture",
|
|
63
|
+
event: input.event,
|
|
64
|
+
distinct_id: input.distinctId,
|
|
65
|
+
properties: {
|
|
66
|
+
$lib: "orgx-openclaw-plugin",
|
|
67
|
+
...(input.properties ?? {}),
|
|
68
|
+
},
|
|
69
|
+
timestamp: now,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
sent_at: now,
|
|
73
|
+
};
|
|
74
|
+
await fetch(url, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: {
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify(body),
|
|
80
|
+
}).then(() => undefined);
|
|
81
|
+
}
|
package/package.json
CHANGED