@mobcode/openclaw-plugin 0.1.0
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 +131 -0
- package/index.js +3 -0
- package/openclaw.plugin.json +63 -0
- package/package.json +32 -0
- package/src/config-utils.js +148 -0
- package/src/feature-matrix.js +49 -0
- package/src/gateway-methods.js +180 -0
- package/src/http-routes.js +159 -0
- package/src/openclaw-gateway-runtime.js +85 -0
- package/src/openclaw-introspection.js +10 -0
- package/src/openclaw-session-reader.js +75 -0
- package/src/plugin-definition.js +63 -0
- package/src/plugin-tools.js +300 -0
- package/src/provider-catalog.js +245 -0
- package/src/provider-model-fetch.js +265 -0
- package/src/runtime-events.js +96 -0
- package/src/state-store.js +1430 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
function jsonResponse(payload, status = 200) {
|
|
2
|
+
return new Response(JSON.stringify(payload, null, 2), {
|
|
3
|
+
status,
|
|
4
|
+
headers: {
|
|
5
|
+
"content-type": "application/json; charset=utf-8",
|
|
6
|
+
},
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function readJsonBody(request) {
|
|
11
|
+
try {
|
|
12
|
+
return await request.json();
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function registerMobcodeHttpRoutes({ api, store, builders }) {
|
|
19
|
+
api.registerHttpRoute({
|
|
20
|
+
path: "/plugins/mobcode/health",
|
|
21
|
+
auth: "plugin",
|
|
22
|
+
match: "exact",
|
|
23
|
+
handler: async () =>
|
|
24
|
+
jsonResponse({
|
|
25
|
+
ok: true,
|
|
26
|
+
pluginId: api.id,
|
|
27
|
+
generatedAt: new Date().toISOString(),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
api.registerHttpRoute({
|
|
32
|
+
path: "/plugins/mobcode/config",
|
|
33
|
+
auth: "plugin",
|
|
34
|
+
match: "exact",
|
|
35
|
+
handler: async (ctx) => {
|
|
36
|
+
const includeSecrets = ctx.url.searchParams.get("includeSecrets") === "1";
|
|
37
|
+
return jsonResponse(builders.configSnapshot({ includeSecrets }));
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
api.registerHttpRoute({
|
|
42
|
+
path: "/plugins/mobcode/providers",
|
|
43
|
+
auth: "plugin",
|
|
44
|
+
match: "exact",
|
|
45
|
+
handler: async () => jsonResponse(await builders.providers()),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
api.registerHttpRoute({
|
|
49
|
+
path: "/plugins/mobcode/provider-models",
|
|
50
|
+
auth: "plugin",
|
|
51
|
+
match: "exact",
|
|
52
|
+
handler: async (ctx) => {
|
|
53
|
+
const providerId = ctx.url.searchParams.get("providerId")?.trim();
|
|
54
|
+
if (!providerId) {
|
|
55
|
+
return jsonResponse({ error: "providerId query param required" }, 400);
|
|
56
|
+
}
|
|
57
|
+
return jsonResponse(await builders.providerModels(providerId));
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
api.registerHttpRoute({
|
|
62
|
+
path: "/plugins/mobcode/messages",
|
|
63
|
+
auth: "plugin",
|
|
64
|
+
match: "exact",
|
|
65
|
+
handler: async (ctx) => {
|
|
66
|
+
const sessionKey = ctx.url.searchParams.get("sessionKey")?.trim();
|
|
67
|
+
if (!sessionKey) {
|
|
68
|
+
return jsonResponse({ error: "sessionKey query param required" }, 400);
|
|
69
|
+
}
|
|
70
|
+
await builders.ensureSessionMessages(sessionKey);
|
|
71
|
+
return jsonResponse(
|
|
72
|
+
await store.pageSessionMessages(sessionKey, {
|
|
73
|
+
limit: Number.parseInt(ctx.url.searchParams.get("limit") ?? "100", 10),
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
api.registerHttpRoute({
|
|
80
|
+
path: "/plugins/mobcode/approvals",
|
|
81
|
+
auth: "plugin",
|
|
82
|
+
match: "exact",
|
|
83
|
+
handler: async (ctx) =>
|
|
84
|
+
jsonResponse({
|
|
85
|
+
approvals: await store.listApprovals({
|
|
86
|
+
sessionKey: ctx.url.searchParams.get("sessionKey")?.trim(),
|
|
87
|
+
status: ctx.url.searchParams.get("status")?.trim() ?? "pending",
|
|
88
|
+
limit: Number.parseInt(ctx.url.searchParams.get("limit") ?? "100", 10),
|
|
89
|
+
}),
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
api.registerHttpRoute({
|
|
94
|
+
path: "/plugins/mobcode/artifacts",
|
|
95
|
+
auth: "plugin",
|
|
96
|
+
match: "exact",
|
|
97
|
+
handler: async (ctx) => {
|
|
98
|
+
const artifactId = ctx.url.searchParams.get("artifactId")?.trim();
|
|
99
|
+
if (!artifactId) {
|
|
100
|
+
return jsonResponse({ error: "artifactId query param required" }, 400);
|
|
101
|
+
}
|
|
102
|
+
const artifact = await store.readArtifactById(artifactId);
|
|
103
|
+
if (!artifact?.document) {
|
|
104
|
+
return jsonResponse({ error: "artifact not found" }, 404);
|
|
105
|
+
}
|
|
106
|
+
return jsonResponse({
|
|
107
|
+
ok: true,
|
|
108
|
+
artifactId: artifact.artifactId,
|
|
109
|
+
document: artifact.document,
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
api.registerHttpRoute({
|
|
115
|
+
path: "/plugins/mobcode/devices/register",
|
|
116
|
+
auth: "plugin",
|
|
117
|
+
match: "exact",
|
|
118
|
+
handler: async (ctx) => {
|
|
119
|
+
const body = await readJsonBody(ctx.request);
|
|
120
|
+
const token = typeof body?.token === "string" ? body.token.trim() : "";
|
|
121
|
+
const platform = typeof body?.platform === "string" ? body.platform.trim() : "";
|
|
122
|
+
if (!token || !platform) {
|
|
123
|
+
return jsonResponse({ error: "platform and token are required" }, 400);
|
|
124
|
+
}
|
|
125
|
+
const result = await store.upsertDevice({
|
|
126
|
+
platform,
|
|
127
|
+
token,
|
|
128
|
+
deviceId: typeof body?.deviceId === "string" ? body.deviceId.trim() : "",
|
|
129
|
+
appVersion: typeof body?.appVersion === "string" ? body.appVersion.trim() : "",
|
|
130
|
+
});
|
|
131
|
+
return jsonResponse({ ok: true, count: result.devices.length });
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
api.registerHttpRoute({
|
|
136
|
+
path: "/plugins/mobcode/client/register",
|
|
137
|
+
auth: "plugin",
|
|
138
|
+
match: "exact",
|
|
139
|
+
handler: async (ctx) => {
|
|
140
|
+
const body = await readJsonBody(ctx.request);
|
|
141
|
+
try {
|
|
142
|
+
return jsonResponse({
|
|
143
|
+
ok: true,
|
|
144
|
+
...(await store.registerClientSession({
|
|
145
|
+
clientId: typeof body?.clientId === "string" ? body.clientId.trim() : "",
|
|
146
|
+
sessionKey: typeof body?.sessionKey === "string" ? body.sessionKey.trim() : "",
|
|
147
|
+
platform: typeof body?.platform === "string" ? body.platform.trim() : "",
|
|
148
|
+
appVersion: typeof body?.appVersion === "string" ? body.appVersion.trim() : "",
|
|
149
|
+
capabilities: body?.capabilities && typeof body.capabilities === "object"
|
|
150
|
+
? body.capabilities
|
|
151
|
+
: {},
|
|
152
|
+
})),
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return jsonResponse({ error: error instanceof Error ? error.message : "invalid body" }, 400);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
async function tryImport(modulePath) {
|
|
2
|
+
try {
|
|
3
|
+
return await import(modulePath);
|
|
4
|
+
} catch {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function loadGatewayRuntime() {
|
|
10
|
+
return (
|
|
11
|
+
(await tryImport("openclaw/plugin-sdk/gateway-runtime")) ??
|
|
12
|
+
(await tryImport("../../openclaw/src/plugin-sdk/gateway-runtime.ts")) ??
|
|
13
|
+
(await tryImport("../../openclaw/src/plugin-sdk/gateway-runtime.js")) ??
|
|
14
|
+
(await tryImport("../openclaw/src/plugin-sdk/gateway-runtime.js"))
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function tryCreateOperatorApprovalsGatewayClient(params) {
|
|
19
|
+
const runtime = await loadGatewayRuntime();
|
|
20
|
+
if (!runtime?.createOperatorApprovalsGatewayClient) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return runtime.createOperatorApprovalsGatewayClient(params);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function requestThroughOperatorApprovalsGateway(params) {
|
|
27
|
+
let resolveHello;
|
|
28
|
+
let rejectHello;
|
|
29
|
+
let helloSettled = false;
|
|
30
|
+
const helloPromise = new Promise((resolve, reject) => {
|
|
31
|
+
resolveHello = resolve;
|
|
32
|
+
rejectHello = reject;
|
|
33
|
+
});
|
|
34
|
+
const timeoutId = setTimeout(() => {
|
|
35
|
+
if (helloSettled) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
helloSettled = true;
|
|
39
|
+
rejectHello(new Error("gateway hello timeout"));
|
|
40
|
+
}, 5000);
|
|
41
|
+
timeoutId.unref?.();
|
|
42
|
+
|
|
43
|
+
const client = await tryCreateOperatorApprovalsGatewayClient({
|
|
44
|
+
config: params.config,
|
|
45
|
+
gatewayUrl: params.gatewayUrl,
|
|
46
|
+
clientDisplayName: params.clientDisplayName ?? "MobCode Gateway RPC",
|
|
47
|
+
onHelloOk: () => {
|
|
48
|
+
if (helloSettled) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
helloSettled = true;
|
|
52
|
+
clearTimeout(timeoutId);
|
|
53
|
+
resolveHello(true);
|
|
54
|
+
},
|
|
55
|
+
onConnectError: (err) => {
|
|
56
|
+
params.onConnectError?.(err);
|
|
57
|
+
if (helloSettled) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
helloSettled = true;
|
|
61
|
+
clearTimeout(timeoutId);
|
|
62
|
+
rejectHello(err);
|
|
63
|
+
},
|
|
64
|
+
onClose: (_code, reason) => {
|
|
65
|
+
if (helloSettled) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
helloSettled = true;
|
|
69
|
+
clearTimeout(timeoutId);
|
|
70
|
+
rejectHello(new Error(reason || "gateway closed before hello"));
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
if (!client) {
|
|
74
|
+
clearTimeout(timeoutId);
|
|
75
|
+
throw new Error("operator approvals gateway runtime unavailable");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
client.start();
|
|
79
|
+
try {
|
|
80
|
+
await helloPromise;
|
|
81
|
+
return await client.request(params.method, params.requestParams);
|
|
82
|
+
} finally {
|
|
83
|
+
client.stop();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { buildStaticProvidersPayload } from "./provider-catalog.js";
|
|
2
|
+
import { buildProviderModelsPayloadFromInternet } from "./provider-model-fetch.js";
|
|
3
|
+
|
|
4
|
+
export async function tryBuildAvailableProvidersPayload() {
|
|
5
|
+
return buildStaticProvidersPayload();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function tryBuildProviderModelsPayload(api, providerId) {
|
|
9
|
+
return await buildProviderModelsPayloadFromInternet(api, providerId);
|
|
10
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { requestThroughOperatorApprovalsGateway } from "./openclaw-gateway-runtime.js";
|
|
2
|
+
|
|
3
|
+
async function tryImport(modulePath) {
|
|
4
|
+
try {
|
|
5
|
+
return await import(modulePath);
|
|
6
|
+
} catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function loadSessionUtilsRuntime() {
|
|
12
|
+
return (
|
|
13
|
+
(await tryImport("../../openclaw/src/gateway/session-utils.js")) ??
|
|
14
|
+
(await tryImport("../openclaw/src/gateway/session-utils.js"))
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function readSessionTranscriptMessagesFromInternals(sessionKey) {
|
|
19
|
+
const runtime = await loadSessionUtilsRuntime();
|
|
20
|
+
if (!runtime?.loadSessionEntry || !runtime?.readSessionMessages) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const normalizedSessionKey = String(sessionKey ?? "").trim();
|
|
25
|
+
if (!normalizedSessionKey) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const loaded = runtime.loadSessionEntry(normalizedSessionKey);
|
|
30
|
+
const entry = loaded?.entry;
|
|
31
|
+
const storePath = loaded?.storePath;
|
|
32
|
+
const sessionId =
|
|
33
|
+
typeof entry?.sessionId === "string" && entry.sessionId.trim()
|
|
34
|
+
? entry.sessionId.trim()
|
|
35
|
+
: "";
|
|
36
|
+
if (!sessionId) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rawMessages = runtime.readSessionMessages(sessionId, storePath, entry?.sessionFile);
|
|
41
|
+
return Array.isArray(rawMessages) ? rawMessages : [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function readSessionMessagesThroughChatHistory(config, sessionKey) {
|
|
45
|
+
const normalizedSessionKey = String(sessionKey ?? "").trim();
|
|
46
|
+
if (!normalizedSessionKey) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const payload = await requestThroughOperatorApprovalsGateway({
|
|
50
|
+
config,
|
|
51
|
+
method: "chat.history",
|
|
52
|
+
requestParams: {
|
|
53
|
+
sessionKey: normalizedSessionKey,
|
|
54
|
+
limit: 1000,
|
|
55
|
+
},
|
|
56
|
+
clientDisplayName: "MobCode History Backfill",
|
|
57
|
+
});
|
|
58
|
+
const messages = payload?.messages;
|
|
59
|
+
return Array.isArray(messages) ? messages : [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function readSessionTranscriptMessages(config, sessionKey) {
|
|
63
|
+
try {
|
|
64
|
+
const gatewayMessages = await readSessionMessagesThroughChatHistory(
|
|
65
|
+
config,
|
|
66
|
+
sessionKey,
|
|
67
|
+
);
|
|
68
|
+
if (gatewayMessages.length > 0) {
|
|
69
|
+
return gatewayMessages;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Fall back to adjacent-source runtime imports for development checkouts.
|
|
73
|
+
}
|
|
74
|
+
return readSessionTranscriptMessagesFromInternals(sessionKey);
|
|
75
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { listFeatureMatrix } from "./feature-matrix.js";
|
|
3
|
+
import {
|
|
4
|
+
buildConfigSnapshot,
|
|
5
|
+
} from "./config-utils.js";
|
|
6
|
+
import { registerMobcodeGatewayMethods } from "./gateway-methods.js";
|
|
7
|
+
import { registerMobcodeHttpRoutes } from "./http-routes.js";
|
|
8
|
+
import {
|
|
9
|
+
tryBuildAvailableProvidersPayload,
|
|
10
|
+
tryBuildProviderModelsPayload,
|
|
11
|
+
} from "./openclaw-introspection.js";
|
|
12
|
+
import { readSessionTranscriptMessages } from "./openclaw-session-reader.js";
|
|
13
|
+
import { registerMobcodeRuntimeObservers } from "./runtime-events.js";
|
|
14
|
+
import { MobcodeStateStore } from "./state-store.js";
|
|
15
|
+
import { registerMobcodeTools } from "./plugin-tools.js";
|
|
16
|
+
|
|
17
|
+
export function createMobcodePluginDefinition() {
|
|
18
|
+
return {
|
|
19
|
+
id: "mobcode",
|
|
20
|
+
name: "MobCode",
|
|
21
|
+
description: "MobCode mobile integration plugin for OpenClaw.",
|
|
22
|
+
register(api) {
|
|
23
|
+
const pluginConfig = api.pluginConfig ?? {};
|
|
24
|
+
const stateDir = api.runtime.state.resolveStateDir();
|
|
25
|
+
const store = new MobcodeStateStore({
|
|
26
|
+
rootDir: path.join(stateDir, "plugins", "mobcode"),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const builders = {
|
|
30
|
+
configSnapshot: (options) => buildConfigSnapshot(api, options),
|
|
31
|
+
providers: async () => await tryBuildAvailableProvidersPayload(api),
|
|
32
|
+
providerModels: async (providerId) =>
|
|
33
|
+
await tryBuildProviderModelsPayload(api, providerId),
|
|
34
|
+
ensureSessionMessages: async (sessionKey) => {
|
|
35
|
+
await store.ensureSessionIndexed(sessionKey, async () =>
|
|
36
|
+
readSessionTranscriptMessages(api.runtime.config.loadConfig(), sessionKey),
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
registerMobcodeGatewayMethods({
|
|
42
|
+
api,
|
|
43
|
+
store,
|
|
44
|
+
builders,
|
|
45
|
+
featureMatrix: listFeatureMatrix,
|
|
46
|
+
});
|
|
47
|
+
registerMobcodeHttpRoutes({
|
|
48
|
+
api,
|
|
49
|
+
store,
|
|
50
|
+
builders,
|
|
51
|
+
});
|
|
52
|
+
registerMobcodeRuntimeObservers({
|
|
53
|
+
api,
|
|
54
|
+
store,
|
|
55
|
+
pluginConfig,
|
|
56
|
+
});
|
|
57
|
+
registerMobcodeTools({
|
|
58
|
+
api,
|
|
59
|
+
store,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
function buildJsonTextContent(payload) {
|
|
2
|
+
return JSON.stringify(payload, null, 2);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function normalizeString(value) {
|
|
6
|
+
return String(value ?? "").trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeOpenParams(raw) {
|
|
10
|
+
if (raw == null) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
14
|
+
throw new Error("params must be an object when provided.");
|
|
15
|
+
}
|
|
16
|
+
return Object.fromEntries(
|
|
17
|
+
Object.entries(raw).map(([key, value]) => [String(key), value]),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readOptionalInt(value, label) {
|
|
22
|
+
if (value == null) return null;
|
|
23
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
24
|
+
return Math.trunc(value);
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === "string" && value.trim()) {
|
|
27
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
28
|
+
if (Number.isFinite(parsed)) {
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`${label} must be an integer when provided.`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertAllowedKeys(target, params, allowedKeys) {
|
|
36
|
+
const invalidKeys = Object.keys(params).filter((key) => !allowedKeys.includes(key));
|
|
37
|
+
if (invalidKeys.length === 0) return;
|
|
38
|
+
throw new Error(
|
|
39
|
+
`target=${target} does not support params: ${invalidKeys.join(", ")}. Allowed params: ${allowedKeys.join(", ")}.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateMobcodeOpenArgs(rawArgs) {
|
|
44
|
+
const target = normalizeString(rawArgs?.target).toLowerCase();
|
|
45
|
+
if (!target) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"target is required. Supported targets: browser_tab, file, terminal, docker, kubernetes, local_agent, openclaw.",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const allowedTargets = [
|
|
51
|
+
"browser_tab",
|
|
52
|
+
"file",
|
|
53
|
+
"terminal",
|
|
54
|
+
"docker",
|
|
55
|
+
"kubernetes",
|
|
56
|
+
"local_agent",
|
|
57
|
+
"openclaw",
|
|
58
|
+
];
|
|
59
|
+
if (!allowedTargets.includes(target)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Unsupported target "${target}". Supported targets: ${allowedTargets.join(", ")}.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const params = normalizeOpenParams(rawArgs?.params);
|
|
65
|
+
switch (target) {
|
|
66
|
+
case "browser_tab": {
|
|
67
|
+
assertAllowedKeys(target, params, ["url"]);
|
|
68
|
+
const url = normalizeString(params.url);
|
|
69
|
+
if (!url) {
|
|
70
|
+
throw new Error("target=browser_tab requires params.url as a non-empty string.");
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "file": {
|
|
75
|
+
assertAllowedKeys(target, params, ["path", "start_line", "end_line"]);
|
|
76
|
+
const path = normalizeString(params.path);
|
|
77
|
+
if (!path) {
|
|
78
|
+
throw new Error("target=file requires params.path as a non-empty string.");
|
|
79
|
+
}
|
|
80
|
+
readOptionalInt(params.start_line, "target=file expects params.start_line");
|
|
81
|
+
readOptionalInt(params.end_line, "target=file expects params.end_line");
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "terminal":
|
|
85
|
+
assertAllowedKeys(target, params, ["command"]);
|
|
86
|
+
break;
|
|
87
|
+
case "docker":
|
|
88
|
+
case "kubernetes":
|
|
89
|
+
case "local_agent":
|
|
90
|
+
case "openclaw":
|
|
91
|
+
assertAllowedKeys(target, params, ["section"]);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
return { target, params };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeArtifactDocument(rawArtifact) {
|
|
98
|
+
if (!rawArtifact || typeof rawArtifact !== "object" || Array.isArray(rawArtifact)) {
|
|
99
|
+
throw new Error("artifact object is required");
|
|
100
|
+
}
|
|
101
|
+
const artifactId = normalizeString(rawArtifact.artifactId ?? rawArtifact.artifact_id);
|
|
102
|
+
const kind = normalizeString(rawArtifact.kind);
|
|
103
|
+
const title = normalizeString(rawArtifact.title);
|
|
104
|
+
if (!artifactId) {
|
|
105
|
+
throw new Error("artifact.artifactId is required");
|
|
106
|
+
}
|
|
107
|
+
if (!kind) {
|
|
108
|
+
throw new Error("artifact.kind is required");
|
|
109
|
+
}
|
|
110
|
+
if (!title) {
|
|
111
|
+
throw new Error("artifact.title is required");
|
|
112
|
+
}
|
|
113
|
+
const initialScreenId = normalizeString(
|
|
114
|
+
rawArtifact.initialScreenId ?? rawArtifact.initial_screen_id ?? "main",
|
|
115
|
+
);
|
|
116
|
+
const screens = Array.isArray(rawArtifact.screens) ? rawArtifact.screens : [];
|
|
117
|
+
const actions = Array.isArray(rawArtifact.actions) ? rawArtifact.actions : [];
|
|
118
|
+
const media = Array.isArray(rawArtifact.media) ? rawArtifact.media : [];
|
|
119
|
+
return {
|
|
120
|
+
schemaVersion: Number(rawArtifact.schemaVersion ?? rawArtifact.schema_version ?? 1) || 1,
|
|
121
|
+
artifactId,
|
|
122
|
+
kind,
|
|
123
|
+
title,
|
|
124
|
+
...(normalizeString(rawArtifact.summary)
|
|
125
|
+
? { summary: normalizeString(rawArtifact.summary) }
|
|
126
|
+
: {}),
|
|
127
|
+
initialScreenId: initialScreenId || "main",
|
|
128
|
+
screens,
|
|
129
|
+
actions,
|
|
130
|
+
localStateSchema:
|
|
131
|
+
rawArtifact.localStateSchema && typeof rawArtifact.localStateSchema === "object"
|
|
132
|
+
? rawArtifact.localStateSchema
|
|
133
|
+
: rawArtifact.local_state_schema &&
|
|
134
|
+
typeof rawArtifact.local_state_schema === "object"
|
|
135
|
+
? rawArtifact.local_state_schema
|
|
136
|
+
: {},
|
|
137
|
+
...(rawArtifact.preview && typeof rawArtifact.preview === "object"
|
|
138
|
+
? { preview: rawArtifact.preview }
|
|
139
|
+
: {}),
|
|
140
|
+
media,
|
|
141
|
+
...(rawArtifact.metadata && typeof rawArtifact.metadata === "object"
|
|
142
|
+
? { metadata: rawArtifact.metadata }
|
|
143
|
+
: {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildArtifactRefBlock(document) {
|
|
148
|
+
return {
|
|
149
|
+
type: "artifact_ref",
|
|
150
|
+
artifactId: document.artifactId,
|
|
151
|
+
kind: document.kind,
|
|
152
|
+
title: document.title,
|
|
153
|
+
...(normalizeString(document.summary) ? { summary: document.summary } : {}),
|
|
154
|
+
document,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function registerMobcodeTools({ api, store }) {
|
|
159
|
+
api.registerTool(
|
|
160
|
+
(context) => ({
|
|
161
|
+
name: "present_artifact",
|
|
162
|
+
description:
|
|
163
|
+
"Publish or update a strict, runtime-validated rich artifact in chat. Use the same artifact document contract as MobCode local agent.",
|
|
164
|
+
parameters: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
item_id: { type: "string" },
|
|
168
|
+
op: {
|
|
169
|
+
type: "string",
|
|
170
|
+
enum: ["start", "patch", "done"],
|
|
171
|
+
},
|
|
172
|
+
artifact: {
|
|
173
|
+
type: "object",
|
|
174
|
+
description: "Artifact document payload.",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
required: ["artifact"],
|
|
178
|
+
additionalProperties: false,
|
|
179
|
+
},
|
|
180
|
+
execute: async (_toolCallId, rawArgs) => {
|
|
181
|
+
const args = rawArgs && typeof rawArgs === "object" ? rawArgs : {};
|
|
182
|
+
const sessionKey = normalizeString(context?.sessionKey);
|
|
183
|
+
if (!sessionKey) {
|
|
184
|
+
throw new Error("sessionKey is required for present_artifact.");
|
|
185
|
+
}
|
|
186
|
+
const document = normalizeArtifactDocument(args.artifact);
|
|
187
|
+
const saved = await store.upsertArtifact({
|
|
188
|
+
sessionKey,
|
|
189
|
+
runId: normalizeString(args._runId),
|
|
190
|
+
document,
|
|
191
|
+
});
|
|
192
|
+
const refBlock = buildArtifactRefBlock(document);
|
|
193
|
+
const itemId = normalizeString(args.item_id) || document.artifactId;
|
|
194
|
+
const op = normalizeString(args.op || "done").toLowerCase() || "done";
|
|
195
|
+
const payload = {
|
|
196
|
+
ok: true,
|
|
197
|
+
artifact: saved.document,
|
|
198
|
+
content_blocks: [refBlock],
|
|
199
|
+
action: {
|
|
200
|
+
type: "artifact",
|
|
201
|
+
op,
|
|
202
|
+
item_id: itemId,
|
|
203
|
+
block: refBlock,
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
return {
|
|
207
|
+
content: [
|
|
208
|
+
{
|
|
209
|
+
type: "text",
|
|
210
|
+
text: buildJsonTextContent(payload),
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
details: payload,
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
}),
|
|
217
|
+
{ name: "present_artifact" },
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
api.registerTool(
|
|
221
|
+
() => ({
|
|
222
|
+
name: "mobcode_open",
|
|
223
|
+
description:
|
|
224
|
+
"Open a MobCode app surface using target + params. Supported targets: browser_tab, file, terminal, docker, kubernetes, local_agent, openclaw.",
|
|
225
|
+
parameters: {
|
|
226
|
+
type: "object",
|
|
227
|
+
properties: {
|
|
228
|
+
target: {
|
|
229
|
+
type: "string",
|
|
230
|
+
enum: [
|
|
231
|
+
"browser_tab",
|
|
232
|
+
"file",
|
|
233
|
+
"terminal",
|
|
234
|
+
"docker",
|
|
235
|
+
"kubernetes",
|
|
236
|
+
"local_agent",
|
|
237
|
+
"openclaw",
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
params: {
|
|
241
|
+
type: "object",
|
|
242
|
+
description:
|
|
243
|
+
"Target-specific params. browser_tab: {url}. file: {path, start_line?, end_line?}. terminal: {command?}. docker/kubernetes/local_agent/openclaw: {section?}.",
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
required: ["target"],
|
|
247
|
+
additionalProperties: false,
|
|
248
|
+
},
|
|
249
|
+
execute: async (_toolCallId, rawArgs) => {
|
|
250
|
+
const payload = validateMobcodeOpenArgs(rawArgs);
|
|
251
|
+
const client = await store.resolveClientForOpenTarget(payload.target);
|
|
252
|
+
if (!client?.clientId) {
|
|
253
|
+
const offlineResult = {
|
|
254
|
+
ok: false,
|
|
255
|
+
status: "offline",
|
|
256
|
+
message: `No active MobCode client supports target=${payload.target}.`,
|
|
257
|
+
};
|
|
258
|
+
return {
|
|
259
|
+
content: [
|
|
260
|
+
{
|
|
261
|
+
type: "text",
|
|
262
|
+
text: buildJsonTextContent(offlineResult),
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
details: offlineResult,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const action = await store.enqueueClientAction({
|
|
269
|
+
clientId: client.clientId,
|
|
270
|
+
type: "mobcode_open",
|
|
271
|
+
payload,
|
|
272
|
+
});
|
|
273
|
+
const resolution = await store.waitForClientActionResolution(action.id, {
|
|
274
|
+
timeoutMs:
|
|
275
|
+
typeof rawArgs?.timeout_ms === "number" && Number.isFinite(rawArgs.timeout_ms)
|
|
276
|
+
? rawArgs.timeout_ms
|
|
277
|
+
: 20_000,
|
|
278
|
+
});
|
|
279
|
+
const status = String(resolution?.status ?? "").trim() || "timeout";
|
|
280
|
+
const result = {
|
|
281
|
+
ok: status === "done",
|
|
282
|
+
status,
|
|
283
|
+
clientId: client.clientId,
|
|
284
|
+
action,
|
|
285
|
+
...(resolution?.errorText ? { errorText: resolution.errorText } : {}),
|
|
286
|
+
};
|
|
287
|
+
return {
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: "text",
|
|
291
|
+
text: buildJsonTextContent(result),
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
details: result,
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
}),
|
|
298
|
+
{ name: "mobcode_open" },
|
|
299
|
+
);
|
|
300
|
+
}
|