@milaboratories/pl-mcp-server 0.2.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.
Files changed (78) hide show
  1. package/dist/index.cjs +3 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +2 -0
  4. package/dist/server.cjs +171 -0
  5. package/dist/server.cjs.map +1 -0
  6. package/dist/server.d.ts +83 -0
  7. package/dist/server.d.ts.map +1 -0
  8. package/dist/server.js +171 -0
  9. package/dist/server.js.map +1 -0
  10. package/dist/tools/await.cjs +89 -0
  11. package/dist/tools/await.cjs.map +1 -0
  12. package/dist/tools/await.js +89 -0
  13. package/dist/tools/await.js.map +1 -0
  14. package/dist/tools/block-state.cjs +71 -0
  15. package/dist/tools/block-state.cjs.map +1 -0
  16. package/dist/tools/block-state.js +71 -0
  17. package/dist/tools/block-state.js.map +1 -0
  18. package/dist/tools/blocks.cjs +123 -0
  19. package/dist/tools/blocks.cjs.map +1 -0
  20. package/dist/tools/blocks.js +123 -0
  21. package/dist/tools/blocks.js.map +1 -0
  22. package/dist/tools/connection.cjs +33 -0
  23. package/dist/tools/connection.cjs.map +1 -0
  24. package/dist/tools/connection.js +33 -0
  25. package/dist/tools/connection.js.map +1 -0
  26. package/dist/tools/data-query.cjs +186 -0
  27. package/dist/tools/data-query.cjs.map +1 -0
  28. package/dist/tools/data-query.js +186 -0
  29. package/dist/tools/data-query.js.map +1 -0
  30. package/dist/tools/logs.cjs +57 -0
  31. package/dist/tools/logs.cjs.map +1 -0
  32. package/dist/tools/logs.js +57 -0
  33. package/dist/tools/logs.js.map +1 -0
  34. package/dist/tools/ping.cjs +14 -0
  35. package/dist/tools/ping.cjs.map +1 -0
  36. package/dist/tools/ping.js +14 -0
  37. package/dist/tools/ping.js.map +1 -0
  38. package/dist/tools/projects.cjs +56 -0
  39. package/dist/tools/projects.cjs.map +1 -0
  40. package/dist/tools/projects.js +56 -0
  41. package/dist/tools/projects.js.map +1 -0
  42. package/dist/tools/sandbox.cjs +51 -0
  43. package/dist/tools/sandbox.cjs.map +1 -0
  44. package/dist/tools/sandbox.js +51 -0
  45. package/dist/tools/sandbox.js.map +1 -0
  46. package/dist/tools/screenshot.cjs +35 -0
  47. package/dist/tools/screenshot.cjs.map +1 -0
  48. package/dist/tools/screenshot.js +35 -0
  49. package/dist/tools/screenshot.js.map +1 -0
  50. package/dist/tools/tokens.cjs +82 -0
  51. package/dist/tools/tokens.cjs.map +1 -0
  52. package/dist/tools/tokens.js +82 -0
  53. package/dist/tools/tokens.js.map +1 -0
  54. package/dist/tools/types.cjs +22 -0
  55. package/dist/tools/types.cjs.map +1 -0
  56. package/dist/tools/types.js +21 -0
  57. package/dist/tools/types.js.map +1 -0
  58. package/dist/tools/ui-interaction.cjs +117 -0
  59. package/dist/tools/ui-interaction.cjs.map +1 -0
  60. package/dist/tools/ui-interaction.js +117 -0
  61. package/dist/tools/ui-interaction.js.map +1 -0
  62. package/package.json +56 -0
  63. package/src/index.ts +7 -0
  64. package/src/server.ts +271 -0
  65. package/src/tools/await.ts +151 -0
  66. package/src/tools/block-state.ts +115 -0
  67. package/src/tools/blocks.ts +222 -0
  68. package/src/tools/connection.ts +63 -0
  69. package/src/tools/data-query.ts +308 -0
  70. package/src/tools/logs.ts +97 -0
  71. package/src/tools/ping.ts +9 -0
  72. package/src/tools/projects.ts +84 -0
  73. package/src/tools/sandbox.ts +62 -0
  74. package/src/tools/screenshot.ts +48 -0
  75. package/src/tools/tokens.test.ts +239 -0
  76. package/src/tools/tokens.ts +84 -0
  77. package/src/tools/types.ts +34 -0
  78. package/src/tools/ui-interaction.ts +156 -0
@@ -0,0 +1,239 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { estimateTokens } from "./tokens";
3
+
4
+ // --- Test data ---
5
+
6
+ const flatObject = { name: "Alice", age: 30, active: true };
7
+
8
+ const nestedObject = {
9
+ user: { name: "Bob", address: { city: "NYC", zip: "10001" } },
10
+ };
11
+
12
+ const flatArray = [1, 2, 3, "four", true, null];
13
+
14
+ const nestedArray = [[1, 2], [3, [4, 5]], "deep"];
15
+
16
+ const mixedDeep = {
17
+ a: [{ b: [{ c: "leaf" }] }],
18
+ d: { e: { f: { g: 42 } } },
19
+ };
20
+
21
+ const typedArrayValue = new Uint8Array([0, 1, 2, 255]);
22
+
23
+ const largeFlat = Array.from({ length: 100 }, (_, i) => `item-${i}`);
24
+
25
+ const wideObject = Object.fromEntries(Array.from({ length: 50 }, (_, i) => [`key${i}`, i]));
26
+
27
+ const deepChain = (() => {
28
+ let obj: Record<string, unknown> = { value: "bottom" };
29
+ for (let i = 0; i < 20; i++) obj = { nested: obj };
30
+ return obj;
31
+ })();
32
+
33
+ const emptyContainers = { emptyObj: {}, emptyArr: [], emptyStr: "", nested: { also: {} } };
34
+
35
+ // Helper: actual token estimate from JSON.stringify
36
+ function actualTokens(v: unknown): number {
37
+ return Math.ceil(JSON.stringify(v).length / 4);
38
+ }
39
+
40
+ // --- Tests ---
41
+
42
+ describe("estimateTokens", () => {
43
+ describe("primitives", () => {
44
+ it("null → 1 token ('null' = 4 chars)", () => {
45
+ expect(estimateTokens(null)).toBe(1);
46
+ });
47
+
48
+ it("undefined → 1 token (treated as null)", () => {
49
+ expect(estimateTokens(undefined)).toBe(1);
50
+ });
51
+
52
+ it("empty string → 1 token ('\"\"' = 2 chars)", () => {
53
+ expect(estimateTokens("")).toBe(1);
54
+ });
55
+
56
+ it("short string matches actual", () => {
57
+ expect(estimateTokens("hello")).toBe(actualTokens("hello"));
58
+ });
59
+
60
+ it("number matches actual", () => {
61
+ expect(estimateTokens(42)).toBe(actualTokens(42));
62
+ expect(estimateTokens(3.14)).toBe(actualTokens(3.14));
63
+ expect(estimateTokens(0)).toBe(actualTokens(0));
64
+ });
65
+
66
+ it("boolean matches actual", () => {
67
+ expect(estimateTokens(true)).toBe(actualTokens(true));
68
+ expect(estimateTokens(false)).toBe(actualTokens(false));
69
+ });
70
+
71
+ it("bigint estimates like number", () => {
72
+ const t = estimateTokens(BigInt(12345));
73
+ expect(typeof t).toBe("number");
74
+ expect(t).toBeGreaterThan(0);
75
+ });
76
+ });
77
+
78
+ describe("typed arrays", () => {
79
+ it("Uint8Array matches actual", () => {
80
+ // JSON.stringify(new Uint8Array([0,1,2,255])) doesn't work directly,
81
+ // but our function estimates as if it were [0,1,2,255]
82
+ expect(estimateTokens(typedArrayValue)).toBe(actualTokens([0, 1, 2, 255]));
83
+ });
84
+
85
+ it("empty Uint8Array → brackets only", () => {
86
+ expect(estimateTokens(new Uint8Array([]))).toBe(actualTokens([]));
87
+ });
88
+
89
+ it("Int32Array", () => {
90
+ const arr = new Int32Array([100, 200, 300]);
91
+ expect(estimateTokens(arr)).toBe(actualTokens([100, 200, 300]));
92
+ });
93
+ });
94
+
95
+ describe("arrays", () => {
96
+ it("empty array matches actual", () => {
97
+ expect(estimateTokens([])).toBe(actualTokens([]));
98
+ });
99
+
100
+ it("flat number array matches actual", () => {
101
+ expect(estimateTokens([1, 2, 3])).toBe(actualTokens([1, 2, 3]));
102
+ });
103
+
104
+ it("mixed array matches actual", () => {
105
+ expect(estimateTokens(flatArray)).toBe(actualTokens(flatArray));
106
+ });
107
+
108
+ it("nested array matches actual", () => {
109
+ expect(estimateTokens(nestedArray)).toBe(actualTokens(nestedArray));
110
+ });
111
+ });
112
+
113
+ describe("objects", () => {
114
+ it("empty object matches actual", () => {
115
+ expect(estimateTokens({})).toBe(actualTokens({}));
116
+ });
117
+
118
+ it("flat object matches actual", () => {
119
+ expect(estimateTokens(flatObject)).toBe(actualTokens(flatObject));
120
+ });
121
+
122
+ it("nested object matches actual", () => {
123
+ expect(estimateTokens(nestedObject)).toBe(actualTokens(nestedObject));
124
+ });
125
+ });
126
+
127
+ describe("complex structures", () => {
128
+ it("mixed deep ≈ actual", () => {
129
+ expect(estimateTokens(mixedDeep)).toBe(actualTokens(mixedDeep));
130
+ });
131
+
132
+ it("deep chain ≈ actual", () => {
133
+ expect(estimateTokens(deepChain)).toBe(actualTokens(deepChain));
134
+ });
135
+
136
+ it("empty containers ≈ actual", () => {
137
+ expect(estimateTokens(emptyContainers)).toBe(actualTokens(emptyContainers));
138
+ });
139
+
140
+ it("large flat array ≈ actual", () => {
141
+ const est = estimateTokens(largeFlat) as number;
142
+ const act = actualTokens(largeFlat);
143
+ expect(Math.abs(est - act)).toBeLessThanOrEqual(1); // rounding
144
+ });
145
+
146
+ it("wide object ≈ actual", () => {
147
+ const est = estimateTokens(wideObject) as number;
148
+ const act = actualTokens(wideObject);
149
+ expect(Math.abs(est - act)).toBeLessThanOrEqual(1);
150
+ });
151
+ });
152
+
153
+ describe("node limit", () => {
154
+ it("respects small node limit on flat array", () => {
155
+ const arr = [1, 2, 3, 4, 5]; // 6 nodes: 1 array + 5 numbers
156
+ const result = estimateTokens(arr, 3);
157
+ expect(typeof result).toBe("string");
158
+ expect(result).toContain("truncated");
159
+ expect(result).toContain("3 nodes");
160
+ });
161
+
162
+ it("respects small node limit on object", () => {
163
+ const result = estimateTokens({ a: 1, b: 2, c: 3 }, 2);
164
+ expect(typeof result).toBe("string");
165
+ expect(result).toContain("truncated");
166
+ });
167
+
168
+ it("returns number when within limit", () => {
169
+ expect(typeof estimateTokens([1, 2], 10)).toBe("number");
170
+ });
171
+
172
+ it("node limit = 1 overflows on container with children", () => {
173
+ const result = estimateTokens([1], 1);
174
+ expect(typeof result).toBe("string");
175
+ expect(result).toContain("truncated");
176
+ });
177
+
178
+ it("node limit = 1 allows single primitive", () => {
179
+ expect(typeof estimateTokens(42, 1)).toBe("number");
180
+ });
181
+
182
+ it("truncated result includes partial token count", () => {
183
+ // [1, 2, 3] with limit 2: node1=array, node2=1, node3>limit
184
+ const result = estimateTokens([1, 2, 3], 2) as string;
185
+ expect(result).toMatch(/^>\d+/);
186
+ expect(result).toContain("truncated at 2 nodes");
187
+ });
188
+
189
+ it("deeply nested hits limit", () => {
190
+ const result = estimateTokens({ a: { b: { c: { d: 1 } } } }, 3);
191
+ expect(typeof result).toBe("string");
192
+ expect(result).toContain("truncated");
193
+ });
194
+
195
+ it("large flat array within default limit", () => {
196
+ expect(typeof estimateTokens(largeFlat)).toBe("number");
197
+ });
198
+
199
+ it("wide object within default limit", () => {
200
+ expect(typeof estimateTokens(wideObject)).toBe("number");
201
+ });
202
+ });
203
+
204
+ describe("accuracy vs JSON.stringify", () => {
205
+ it("estimate equals actual for simple cases", () => {
206
+ const cases = [null, 42, "test", true, false, [1, 2, 3], { a: 1 }, { x: "hello", y: [1, 2] }];
207
+ for (const c of cases) {
208
+ expect(estimateTokens(c)).toBe(actualTokens(c));
209
+ }
210
+ });
211
+
212
+ it("estimate within 1 token of actual for larger structures", () => {
213
+ const cases = [
214
+ flatObject,
215
+ nestedObject,
216
+ flatArray,
217
+ mixedDeep,
218
+ deepChain,
219
+ wideObject,
220
+ largeFlat,
221
+ ];
222
+ for (const c of cases) {
223
+ const est = estimateTokens(c) as number;
224
+ const act = actualTokens(c);
225
+ expect(Math.abs(est - act)).toBeLessThanOrEqual(1);
226
+ }
227
+ });
228
+ });
229
+
230
+ describe("edge cases", () => {
231
+ it("function treated as empty object", () => {
232
+ expect(typeof estimateTokens(() => {})).toBe("number");
233
+ });
234
+
235
+ it("symbol treated as empty object", () => {
236
+ expect(typeof estimateTokens(Symbol("test"))).toBe("number");
237
+ });
238
+ });
239
+ });
@@ -0,0 +1,84 @@
1
+ const NODE_LIMIT = 10_000;
2
+ const CHARS_PER_TOKEN = 4;
3
+
4
+ /**
5
+ * Estimate the number of LLM tokens needed to represent a value as JSON,
6
+ * without actually serializing it. Walks the object tree and sums up
7
+ * the JSON character lengths, then divides by ~4 chars/token.
8
+ *
9
+ * Returns token estimate, or a string like ">1234 (truncated at 10000 nodes)"
10
+ * if the object graph is too large to fully traverse.
11
+ */
12
+ export function estimateTokens(value: unknown, nodeLimit = NODE_LIMIT): number | string {
13
+ let nodes = 0;
14
+ let chars = 0;
15
+
16
+ /** Returns true if node limit exceeded. */
17
+ function walk(v: unknown): boolean {
18
+ if (++nodes > nodeLimit) return true;
19
+ if (v === null || v === undefined) {
20
+ chars += 4; // "null"
21
+ return false;
22
+ }
23
+ switch (typeof v) {
24
+ case "string":
25
+ chars += v.length + 2; // content + quotes
26
+ return false;
27
+ case "number":
28
+ chars += String(v).length;
29
+ return false;
30
+ case "boolean":
31
+ chars += v ? 4 : 5; // "true" or "false"
32
+ return false;
33
+ case "bigint":
34
+ chars += String(v).length;
35
+ return false;
36
+ default:
37
+ break;
38
+ }
39
+ if (v instanceof Uint8Array || ArrayBuffer.isView(v)) {
40
+ // serialized as array of numbers
41
+ const arr = v as Uint8Array;
42
+ chars += 2 + Math.max(0, arr.length - 1); // [] + commas
43
+ for (let i = 0; i < arr.length; i++) {
44
+ chars += String(arr[i]).length;
45
+ }
46
+ return false;
47
+ }
48
+ if (Array.isArray(v)) {
49
+ chars += 2; // []
50
+ if (v.length > 1) chars += v.length - 1; // commas
51
+ for (const item of v) {
52
+ if (walk(item)) return true;
53
+ }
54
+ return false;
55
+ }
56
+ if (typeof v === "object") {
57
+ const entries = Object.entries(v as Record<string, unknown>);
58
+ chars += 2; // {}
59
+ if (entries.length > 1) chars += entries.length - 1; // commas
60
+ for (const [k, val] of entries) {
61
+ chars += k.length + 3; // "key":
62
+ if (walk(val)) return true;
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+
68
+ const overflow = walk(value);
69
+ const tokens = Math.ceil(chars / CHARS_PER_TOKEN);
70
+ return overflow ? `>${tokens} (truncated at ${nodeLimit} nodes)` : tokens;
71
+ }
72
+
73
+ /** Summarize block outputs as concise key/ok/hasValue/tokensEstimate entries. */
74
+ export function summarizeOutputs(
75
+ outputs: Record<string, unknown> | undefined,
76
+ ): { key: string; ok: boolean; hasValue: boolean; tokensEstimate?: number | string }[] {
77
+ if (!outputs) return [];
78
+ return Object.entries(outputs).map(([key, out]) => {
79
+ const o = out as { ok?: boolean; value?: unknown } | undefined;
80
+ const hasValue = o?.value != null;
81
+ const tokensEstimate = hasValue ? estimateTokens(o!.value) : undefined;
82
+ return { key, ok: o?.ok ?? false, hasValue, tokensEstimate };
83
+ });
84
+ }
@@ -0,0 +1,34 @@
1
+ import type {
2
+ AuthorMarker,
3
+ MiddleLayer,
4
+ Project,
5
+ ProjectListEntry,
6
+ } from "@milaboratories/pl-middle-layer";
7
+ import type { PlMcpServerCallbacks } from "../server";
8
+
9
+ export type { AuthorMarker };
10
+
11
+ export interface ToolContext {
12
+ getMl: () => MiddleLayer | null;
13
+ requireMl: () => MiddleLayer;
14
+ resolveProject: (projectId: string) => Promise<ProjectListEntry>;
15
+ getOpenedProject: (projectId: string) => Promise<Project>;
16
+ callbacks: PlMcpServerCallbacks;
17
+ /** Returns an AuthorMarker with auto-incrementing localVersion for this MCP session. */
18
+ getAuthorMarker: () => AuthorMarker;
19
+ }
20
+
21
+ export function textResult(data: unknown) {
22
+ return {
23
+ content: [{ type: "text" as const, text: JSON.stringify(data) }],
24
+ };
25
+ }
26
+
27
+ /** Return an MCP error result with an actionable hint for the AI agent. */
28
+ export function errorResult(message: string, hint?: string) {
29
+ const text = hint ? `${message}\n\nHint: ${hint}` : message;
30
+ return {
31
+ content: [{ type: "text" as const, text }],
32
+ isError: true,
33
+ };
34
+ }
@@ -0,0 +1,156 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import type { ToolContext } from "./types";
4
+ import { errorResult, textResult } from "./types";
5
+
6
+ export function registerUIInteractionTools(server: McpServer, ctx: ToolContext): void {
7
+ server.registerTool(
8
+ "click",
9
+ {
10
+ description:
11
+ "Click at coordinates (x, y) in the application window. Use capture_screenshot to find element positions.",
12
+ inputSchema: {
13
+ x: z.number().describe("X coordinate"),
14
+ y: z.number().describe("Y coordinate"),
15
+ doubleClick: z.boolean().optional().describe("Double click"),
16
+ },
17
+ },
18
+ async ({ x, y, doubleClick }) => {
19
+ if (!ctx.callbacks.sendInputEvent) {
20
+ return errorResult(
21
+ "UI interaction is not available.",
22
+ "Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log",
23
+ );
24
+ }
25
+ const clickCount = doubleClick ? 2 : 1;
26
+ await ctx.callbacks.sendInputEvent({
27
+ type: "mouseDown",
28
+ x,
29
+ y,
30
+ button: "left",
31
+ clickCount,
32
+ });
33
+ await ctx.callbacks.sendInputEvent({ type: "mouseUp", x, y, button: "left", clickCount });
34
+ return textResult({ ok: true });
35
+ },
36
+ );
37
+
38
+ server.registerTool(
39
+ "type_text",
40
+ {
41
+ description: "Type text into the currently focused element",
42
+ inputSchema: {
43
+ text: z.string().describe("Text to type"),
44
+ },
45
+ },
46
+ async ({ text }) => {
47
+ if (!ctx.callbacks.sendInputEvent) {
48
+ return errorResult(
49
+ "UI interaction is not available.",
50
+ "Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log",
51
+ );
52
+ }
53
+ for (const char of text) {
54
+ await ctx.callbacks.sendInputEvent({ type: "keyDown", keyCode: char });
55
+ await ctx.callbacks.sendInputEvent({ type: "char", keyCode: char });
56
+ await ctx.callbacks.sendInputEvent({ type: "keyUp", keyCode: char });
57
+ }
58
+ return textResult({ ok: true });
59
+ },
60
+ );
61
+
62
+ server.registerTool(
63
+ "press_key",
64
+ {
65
+ description: "Press a keyboard key (Enter, Tab, Escape, Backspace, ArrowDown, ArrowUp, etc.)",
66
+ inputSchema: {
67
+ key: z
68
+ .string()
69
+ .describe("Key name (e.g. 'Enter', 'Tab', 'Escape', 'Backspace', 'ArrowDown')"),
70
+ modifiers: z
71
+ .array(z.enum(["shift", "control", "alt", "meta"]))
72
+ .optional()
73
+ .describe("Modifier keys to hold"),
74
+ },
75
+ },
76
+ async ({ key, modifiers }) => {
77
+ if (!ctx.callbacks.sendInputEvent) {
78
+ return errorResult(
79
+ "UI interaction is not available.",
80
+ "Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log",
81
+ );
82
+ }
83
+ await ctx.callbacks.sendInputEvent({
84
+ type: "keyDown",
85
+ keyCode: key,
86
+ ...(modifiers && {
87
+ shift: modifiers.includes("shift"),
88
+ control: modifiers.includes("control"),
89
+ alt: modifiers.includes("alt"),
90
+ meta: modifiers.includes("meta"),
91
+ }),
92
+ });
93
+ await ctx.callbacks.sendInputEvent({
94
+ type: "keyUp",
95
+ keyCode: key,
96
+ ...(modifiers && {
97
+ shift: modifiers.includes("shift"),
98
+ control: modifiers.includes("control"),
99
+ alt: modifiers.includes("alt"),
100
+ meta: modifiers.includes("meta"),
101
+ }),
102
+ });
103
+ return textResult({ ok: true });
104
+ },
105
+ );
106
+
107
+ server.registerTool(
108
+ "scroll",
109
+ {
110
+ description: "Scroll the page at a given position",
111
+ inputSchema: {
112
+ x: z.number().describe("X coordinate to scroll at"),
113
+ y: z.number().describe("Y coordinate to scroll at"),
114
+ deltaX: z.number().optional().default(0).describe("Horizontal scroll amount"),
115
+ deltaY: z.number().describe("Vertical scroll amount (negative = up, positive = down)"),
116
+ },
117
+ },
118
+ async ({ x, y, deltaX, deltaY }) => {
119
+ if (!ctx.callbacks.sendInputEvent) {
120
+ return errorResult(
121
+ "UI interaction is not available.",
122
+ "Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log",
123
+ );
124
+ }
125
+ await ctx.callbacks.sendInputEvent({
126
+ type: "mouseWheel",
127
+ x,
128
+ y,
129
+ deltaX: deltaX ?? 0,
130
+ deltaY,
131
+ });
132
+ return textResult({ ok: true });
133
+ },
134
+ );
135
+
136
+ server.registerTool(
137
+ "execute_js",
138
+ {
139
+ description:
140
+ "Execute JavaScript in the renderer process and return the result. Useful for querying DOM, reading text, or complex interactions.",
141
+ inputSchema: {
142
+ code: z.string().describe("JavaScript code to execute"),
143
+ },
144
+ },
145
+ async ({ code }) => {
146
+ if (!ctx.callbacks.executeJavaScript) {
147
+ return errorResult(
148
+ "JS execution is not available.",
149
+ "Make sure the MCP server is running inside Platforma Desktop and MCP connected properly. If everything is fine check Electron logs with get_app_log",
150
+ );
151
+ }
152
+ const result = await ctx.callbacks.executeJavaScript(code);
153
+ return textResult(result);
154
+ },
155
+ );
156
+ }