lua-cli 1.3.1 → 1.3.2-alpha.2

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.
@@ -197,6 +197,33 @@ export class ToolApi {
197
197
  });
198
198
  }
199
199
  }
200
+ // User Data API calls
201
+ export class UserDataApi {
202
+ static async getUserData(apiKey, agentId) {
203
+ const response = await httpClient.get(`${BASE_URLS.LOCAL}/developer/user/data/agent/${agentId}`, {
204
+ Authorization: `Bearer ${apiKey}`,
205
+ });
206
+ return response;
207
+ }
208
+ static async createUserData(apiKey, agentId, data) {
209
+ const response = await httpClient.put(`${BASE_URLS.LOCAL}/developer/user/data/agent/${agentId}`, data, {
210
+ Authorization: `Bearer ${apiKey}`,
211
+ });
212
+ return response.data.data;
213
+ }
214
+ static async updateUserData(apiKey, agentId, data) {
215
+ const response = await httpClient.put(`${BASE_URLS.LOCAL}/developer/user/data/agent/${agentId}`, data, {
216
+ Authorization: `Bearer ${apiKey}`,
217
+ });
218
+ return response.data.data;
219
+ }
220
+ static async deleteUserData(apiKey, agentId) {
221
+ const response = await httpClient.delete(`${BASE_URLS.LOCAL}/developer/user/data/agent/${agentId}`, {
222
+ Authorization: `Bearer ${apiKey}`,
223
+ });
224
+ return response;
225
+ }
226
+ }
200
227
  /**
201
228
  * Main API service that exports all API classes
202
229
  */
package/dist/skill.d.ts CHANGED
@@ -1,6 +1,20 @@
1
1
  import { ZodType } from "zod";
2
2
  import { LuaTool } from "./types/index.js";
3
3
  export { LuaTool };
4
+ /**
5
+ * Safe environment variable access function
6
+ * Gets injected at runtime with skill-specific environment variables
7
+ *
8
+ * @param key - The environment variable key to retrieve
9
+ * @returns The environment variable value or undefined if not found
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const baseUrl = env('BASE_URL');
14
+ * const apiKey = env('API_KEY');
15
+ * ```
16
+ */
17
+ export declare const env: (key: string) => string | undefined;
4
18
  export interface LuaSkillConfig {
5
19
  description: string;
6
20
  context: string;
@@ -25,6 +39,7 @@ export declare class LuaSkill {
25
39
  * ```
26
40
  */
27
41
  constructor(config: LuaSkillConfig);
28
- addTool<TInput extends ZodType, TOutput extends ZodType>(tool: LuaTool<TInput, TOutput>): void;
42
+ addTool<TInput extends ZodType>(tool: LuaTool<TInput>): void;
43
+ addTools(tools: LuaTool<any>[]): void;
29
44
  run(input: Record<string, any>): Promise<any>;
30
45
  }
package/dist/skill.js CHANGED
@@ -1,4 +1,46 @@
1
1
  import { assertValidToolName } from "./types/index.js";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import yaml from "js-yaml";
5
+ /**
6
+ * Safe environment variable access function
7
+ * Gets injected at runtime with skill-specific environment variables
8
+ *
9
+ * @param key - The environment variable key to retrieve
10
+ * @returns The environment variable value or undefined if not found
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const baseUrl = env('BASE_URL');
15
+ * const apiKey = env('API_KEY');
16
+ * ```
17
+ */
18
+ export const env = (key) => {
19
+ // Try to load environment variables from lua.skill.yaml
20
+ try {
21
+ // Look for lua.skill.yaml in current directory and parent directories
22
+ let currentDir = process.cwd();
23
+ let yamlPath = null;
24
+ while (currentDir !== path.dirname(currentDir)) {
25
+ const potentialPath = path.join(currentDir, 'lua.skill.yaml');
26
+ if (fs.existsSync(potentialPath)) {
27
+ yamlPath = potentialPath;
28
+ break;
29
+ }
30
+ currentDir = path.dirname(currentDir);
31
+ }
32
+ if (yamlPath) {
33
+ const yamlContent = fs.readFileSync(yamlPath, 'utf8');
34
+ const config = yaml.load(yamlContent);
35
+ return config?.skill?.env?.[key];
36
+ }
37
+ }
38
+ catch (error) {
39
+ // If we can't load the YAML, fall back to undefined
40
+ console.warn(`Warning: Could not load environment variables from lua.skill.yaml: ${error}`);
41
+ }
42
+ return undefined;
43
+ };
2
44
  export class LuaSkill {
3
45
  /**
4
46
  * Creates a new LuaSkill instance
@@ -25,6 +67,13 @@ export class LuaSkill {
25
67
  assertValidToolName(tool.name);
26
68
  this.tools.push(tool);
27
69
  }
70
+ addTools(tools) {
71
+ // Validate all tool names before adding them
72
+ for (const tool of tools) {
73
+ assertValidToolName(tool.name);
74
+ }
75
+ this.tools.push(...tools);
76
+ }
28
77
  async run(input) {
29
78
  const tool = this.tools.find(tool => tool.name === input.tool);
30
79
  if (!tool) {
@@ -62,11 +62,10 @@ export declare function validateToolName(name: string): boolean;
62
62
  * Throws an error if the tool name is invalid
63
63
  */
64
64
  export declare function assertValidToolName(name: string): void;
65
- export interface LuaTool<TInput extends ZodType = ZodType, TOutput extends ZodType = ZodType> {
65
+ export interface LuaTool<TInput extends ZodType = ZodType> {
66
66
  name: string;
67
67
  description: string;
68
68
  inputSchema: TInput;
69
- outputSchema: TOutput;
70
69
  execute: (input: any) => Promise<any>;
71
70
  }
72
71
  export interface LuaSkillConfig {
@@ -78,6 +77,7 @@ export declare class LuaSkill {
78
77
  private readonly description;
79
78
  private readonly context;
80
79
  constructor(config: LuaSkillConfig);
81
- addTool<TInput extends ZodType, TOutput extends ZodType>(tool: LuaTool<TInput, TOutput>): void;
80
+ addTool<TInput extends ZodType>(tool: LuaTool<TInput>): void;
81
+ addTools(tools: LuaTool<any>[]): void;
82
82
  run(input: Record<string, any>): Promise<any>;
83
83
  }
@@ -3,14 +3,8 @@
3
3
  * Provides methods to interact with user data stored in the Lua system
4
4
  */
5
5
  export declare class UserDataAPI {
6
- private apiKey;
7
- private agentId;
8
- constructor(apiKey?: string, agentId?: string);
9
- /**
10
- * Initialize the API with credentials
11
- * If not provided, will attempt to load from stored configuration
12
- */
13
- initialize(): Promise<void>;
6
+ userData: any;
7
+ constructor();
14
8
  /**
15
9
  * Get user data for the current agent
16
10
  * @returns Promise<UserDataResponse>
@@ -43,10 +37,3 @@ export declare class UserDataAPI {
43
37
  export declare const user: {
44
38
  data: UserDataAPI;
45
39
  };
46
- /**
47
- * Create a new UserDataAPI instance with custom credentials
48
- * @param apiKey - Optional API key
49
- * @param agentId - Optional agent ID
50
- * @returns UserDataAPI instance
51
- */
52
- export declare function createUserDataAPI(apiKey?: string, agentId?: string): UserDataAPI;
@@ -1,60 +1,17 @@
1
- import { readSkillConfig } from './utils/files.js';
2
- import { loadApiKey } from './services/auth.js';
3
1
  /**
4
2
  * User Data API for Lua CLI projects
5
3
  * Provides methods to interact with user data stored in the Lua system
6
4
  */
7
5
  export class UserDataAPI {
8
- constructor(apiKey, agentId) {
9
- this.apiKey = apiKey || '';
10
- this.agentId = agentId || '';
11
- }
12
- /**
13
- * Initialize the API with credentials
14
- * If not provided, will attempt to load from stored configuration
15
- */
16
- async initialize() {
17
- if (!this.apiKey) {
18
- this.apiKey = await loadApiKey() || '';
19
- }
20
- if (!this.agentId) {
21
- const config = await readSkillConfig();
22
- this.agentId = config.agent.agentId;
23
- }
24
- if (!this.apiKey || !this.agentId) {
25
- throw new Error('API key and agent ID are required. Please run "lua auth configure" first.');
26
- }
6
+ constructor() {
7
+ this.userData = {};
27
8
  }
28
9
  /**
29
10
  * Get user data for the current agent
30
11
  * @returns Promise<UserDataResponse>
31
12
  */
32
13
  async get() {
33
- await this.initialize();
34
- const response = await fetch(`http://localhost:3022/developer/user/data/agent/${this.agentId}`, {
35
- method: 'GET',
36
- headers: {
37
- 'accept': '*/*',
38
- 'Authorization': `Bearer ${this.apiKey}`
39
- }
40
- });
41
- if (!response.ok) {
42
- // Try alternative authentication method if Bearer fails
43
- if (response.status === 401) {
44
- const altResponse = await fetch(`http://localhost:3022/developer/user/data/agent/${this.agentId}`, {
45
- method: 'GET',
46
- headers: {
47
- 'accept': '*/*',
48
- 'Authorization': this.apiKey
49
- }
50
- });
51
- if (altResponse.ok) {
52
- return await altResponse.json();
53
- }
54
- }
55
- throw new Error(`Failed to get user data: ${response.status} ${response.statusText}`);
56
- }
57
- return await response.json();
14
+ return { success: true, data: this.userData };
58
15
  }
59
16
  /**
60
17
  * Create or update user data for the current agent
@@ -62,35 +19,8 @@ export class UserDataAPI {
62
19
  * @returns Promise<UserDataResponse>
63
20
  */
64
21
  async create(data) {
65
- await this.initialize();
66
- const response = await fetch(`http://localhost:3022/developer/user/data/agent/${this.agentId}`, {
67
- method: 'PUT',
68
- headers: {
69
- 'accept': '*/*',
70
- 'Authorization': `Bearer ${this.apiKey}`,
71
- 'Content-Type': 'application/json'
72
- },
73
- body: JSON.stringify(data)
74
- });
75
- if (!response.ok) {
76
- // Try alternative authentication method if Bearer fails
77
- if (response.status === 401) {
78
- const altResponse = await fetch(`http://localhost:3022/developer/user/data/agent/${this.agentId}`, {
79
- method: 'PUT',
80
- headers: {
81
- 'accept': '*/*',
82
- 'Authorization': this.apiKey,
83
- 'Content-Type': 'application/json'
84
- },
85
- body: JSON.stringify(data)
86
- });
87
- if (altResponse.ok) {
88
- return await altResponse.json();
89
- }
90
- }
91
- throw new Error(`Failed to create user data: ${response.status} ${response.statusText}`);
92
- }
93
- return await response.json();
22
+ this.userData = data;
23
+ return { success: true, data: this.userData };
94
24
  }
95
25
  /**
96
26
  * Update existing user data for the current agent
@@ -99,38 +29,16 @@ export class UserDataAPI {
99
29
  */
100
30
  async update(data) {
101
31
  // Update is the same as create for this API
102
- return this.create(data);
32
+ this.userData = { ...this.userData, ...data };
33
+ return { success: true, data: this.userData };
103
34
  }
104
35
  /**
105
36
  * Clear all user data for the current agent
106
37
  * @returns Promise<{success: boolean}>
107
38
  */
108
39
  async clear() {
109
- await this.initialize();
110
- const response = await fetch(`http://localhost:3022/developer/user/data/agent/${this.agentId}`, {
111
- method: 'DELETE',
112
- headers: {
113
- 'accept': '*/*',
114
- 'Authorization': `Bearer ${this.apiKey}`
115
- }
116
- });
117
- if (!response.ok) {
118
- // Try alternative authentication method if Bearer fails
119
- if (response.status === 401) {
120
- const altResponse = await fetch(`http://localhost:3022/developer/user/data/agent/${this.agentId}`, {
121
- method: 'DELETE',
122
- headers: {
123
- 'accept': '*/*',
124
- 'Authorization': this.apiKey
125
- }
126
- });
127
- if (altResponse.ok) {
128
- return await altResponse.json();
129
- }
130
- }
131
- throw new Error(`Failed to clear user data: ${response.status} ${response.statusText}`);
132
- }
133
- return await response.json();
40
+ this.userData = {};
41
+ return { success: true };
134
42
  }
135
43
  }
136
44
  /**
@@ -140,12 +48,3 @@ export class UserDataAPI {
140
48
  export const user = {
141
49
  data: new UserDataAPI()
142
50
  };
143
- /**
144
- * Create a new UserDataAPI instance with custom credentials
145
- * @param apiKey - Optional API key
146
- * @param agentId - Optional agent ID
147
- * @returns UserDataAPI instance
148
- */
149
- export function createUserDataAPI(apiKey, agentId) {
150
- return new UserDataAPI(apiKey, agentId);
151
- }
@@ -27,10 +27,30 @@ export function copyTemplateFiles(templateDir, targetDir) {
27
27
  function updatePackageJson(srcPath, destPath) {
28
28
  // Read the template package.json
29
29
  const templatePackageJson = JSON.parse(fs.readFileSync(srcPath, 'utf8'));
30
- // Get the current CLI version from the main package.json
31
- const mainPackageJsonPath = path.join(process.cwd(), 'package.json');
32
- const mainPackageJson = JSON.parse(fs.readFileSync(mainPackageJsonPath, 'utf8'));
33
- const currentCliVersion = mainPackageJson.version;
30
+ // Get the current CLI version from the CLI's own package.json
31
+ // We need to find the CLI's package.json, not the current working directory's
32
+ let currentCliVersion = '1.3.2-alpha.0'; // Default fallback version
33
+ try {
34
+ // Try to find the CLI's package.json by looking for it in common locations
35
+ const possiblePaths = [
36
+ path.join(__dirname, '..', '..', 'package.json'), // From dist/utils/
37
+ path.join(process.cwd(), 'package.json'), // Current directory (fallback)
38
+ path.join(process.env.HOME || '', '.npm-global', 'lib', 'node_modules', 'lua-cli', 'package.json'), // Global install
39
+ ];
40
+ for (const packagePath of possiblePaths) {
41
+ if (fs.existsSync(packagePath)) {
42
+ const mainPackageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
43
+ if (mainPackageJson.name === 'lua-cli') {
44
+ currentCliVersion = mainPackageJson.version;
45
+ break;
46
+ }
47
+ }
48
+ }
49
+ }
50
+ catch (error) {
51
+ // If we can't read any package.json, use the fallback version
52
+ console.log(`Using fallback CLI version: ${currentCliVersion}`);
53
+ }
34
54
  // Update the lua-cli dependency version
35
55
  if (templatePackageJson.dependencies && templatePackageJson.dependencies['lua-cli']) {
36
56
  templatePackageJson.dependencies['lua-cli'] = `^${currentCliVersion}`;
@@ -50,7 +70,15 @@ ${personaSection}${welcomeMessageSection}
50
70
  skill:
51
71
  name: "${skillName}"
52
72
  version: "0.0.1"
53
- ${skillIdSection}`;
73
+ ${skillIdSection} env:
74
+ # Example environment variables - customize these for your skill
75
+ API_URL: "https://api.example.com"
76
+ API_KEY: "your-api-key-here"
77
+ DEBUG_MODE: "false"
78
+ MAX_RETRIES: "3"
79
+ TIMEOUT_MS: "5000"
80
+ # Add your own environment variables below
81
+ # CUSTOM_VAR: "custom-value"`;
54
82
  fs.writeFileSync("lua.skill.yaml", yamlContent);
55
83
  }
56
84
  export function readSkillYaml() {
@@ -0,0 +1,96 @@
1
+ export interface SandboxOptions {
2
+ apiKey: string;
3
+ agentId: string;
4
+ customConsole?: any;
5
+ broadcastLog?: (logData: any) => void;
6
+ }
7
+ export interface ExecuteToolOptions extends SandboxOptions {
8
+ toolCode: string;
9
+ inputs: any;
10
+ }
11
+ /**
12
+ * Creates a VM sandbox context with all necessary globals and utilities
13
+ */
14
+ export declare function createSandbox(options: SandboxOptions): {
15
+ require: NodeJS.Require;
16
+ console: any;
17
+ Buffer: BufferConstructor;
18
+ setTimeout: typeof setTimeout;
19
+ setInterval: typeof setInterval;
20
+ clearTimeout: typeof clearTimeout;
21
+ clearInterval: typeof clearInterval;
22
+ process: NodeJS.Process;
23
+ global: typeof globalThis;
24
+ __dirname: string;
25
+ __filename: string;
26
+ module: {
27
+ exports: {};
28
+ };
29
+ exports: {};
30
+ fetch: typeof fetch;
31
+ URLSearchParams: {
32
+ new (init?: string[][] | Record<string, string> | string | URLSearchParams): URLSearchParams;
33
+ prototype: URLSearchParams;
34
+ };
35
+ URL: {
36
+ new (url: string | URL, base?: string | URL): URL;
37
+ prototype: URL;
38
+ canParse(url: string | URL, base?: string | URL): boolean;
39
+ createObjectURL(obj: Blob | MediaSource): string;
40
+ parse(url: string | URL, base?: string | URL): URL | null;
41
+ revokeObjectURL(url: string): void;
42
+ };
43
+ Headers: {
44
+ new (init?: HeadersInit): Headers;
45
+ prototype: Headers;
46
+ };
47
+ Request: {
48
+ new (input: RequestInfo | URL, init?: RequestInit): Request;
49
+ prototype: Request;
50
+ };
51
+ Response: {
52
+ new (body?: BodyInit | null, init?: ResponseInit): Response;
53
+ prototype: Response;
54
+ error(): Response;
55
+ json(data: any, init?: ResponseInit): Response;
56
+ redirect(url: string | URL, status?: number): Response;
57
+ };
58
+ Object: ObjectConstructor;
59
+ Array: ArrayConstructor;
60
+ String: StringConstructor;
61
+ Number: NumberConstructor;
62
+ Boolean: BooleanConstructor;
63
+ Date: DateConstructor;
64
+ Math: Math;
65
+ JSON: JSON;
66
+ Error: ErrorConstructor;
67
+ TypeError: TypeErrorConstructor;
68
+ ReferenceError: ReferenceErrorConstructor;
69
+ SyntaxError: SyntaxErrorConstructor;
70
+ globalThis: typeof globalThis;
71
+ undefined: undefined;
72
+ null: null;
73
+ Infinity: number;
74
+ NaN: number;
75
+ user: {
76
+ data: {
77
+ update: (data: any) => Promise<import("../services/api.js").ApiResponse<any>>;
78
+ get: () => Promise<any>;
79
+ create: (data: any) => Promise<any>;
80
+ };
81
+ };
82
+ env: (key: string) => string;
83
+ };
84
+ /**
85
+ * Executes a tool in a VM sandbox
86
+ */
87
+ export declare function executeTool(options: ExecuteToolOptions): Promise<any>;
88
+ /**
89
+ * Creates a custom console that broadcasts logs via WebSocket
90
+ */
91
+ export declare function createBroadcastConsole(broadcastLog: (logData: any) => void): {
92
+ log: (...args: any[]) => void;
93
+ error: (...args: any[]) => void;
94
+ warn: (...args: any[]) => void;
95
+ info: (...args: any[]) => void;
96
+ };
@@ -0,0 +1,161 @@
1
+ import { createRequire } from "module";
2
+ import vm from "vm";
3
+ import path from "path";
4
+ import { UserDataApi } from "../services/api.js";
5
+ import { readSkillConfig } from "./files.js";
6
+ /**
7
+ * Creates a VM sandbox context with all necessary globals and utilities
8
+ */
9
+ export function createSandbox(options) {
10
+ const { apiKey, agentId, customConsole, broadcastLog } = options;
11
+ // Extract environment variables from YAML config
12
+ const config = readSkillConfig();
13
+ const envVars = {};
14
+ if (config?.skill?.env) {
15
+ for (const [key, value] of Object.entries(config.skill.env)) {
16
+ envVars[key] = value;
17
+ }
18
+ }
19
+ // Create a CommonJS context for execution
20
+ const require = createRequire(process.cwd() + '/package.json');
21
+ const updateUserData = async (data) => {
22
+ return await UserDataApi.updateUserData(apiKey, agentId, data);
23
+ };
24
+ const getUserData = async () => {
25
+ return await UserDataApi.getUserData(apiKey, agentId);
26
+ };
27
+ const linkUserData = async (data) => {
28
+ return await UserDataApi.createUserData(apiKey, agentId, data);
29
+ };
30
+ // Create console object (use custom console if provided, otherwise default)
31
+ const consoleObj = customConsole || console;
32
+ // Create a sandbox context
33
+ const sandbox = {
34
+ require,
35
+ console: consoleObj,
36
+ Buffer,
37
+ setTimeout,
38
+ setInterval,
39
+ clearTimeout,
40
+ clearInterval,
41
+ process,
42
+ global: globalThis,
43
+ __dirname: process.cwd(),
44
+ __filename: path.join(process.cwd(), 'index.ts'),
45
+ module: { exports: {} },
46
+ exports: {},
47
+ // Web APIs
48
+ fetch: globalThis.fetch,
49
+ URLSearchParams: globalThis.URLSearchParams,
50
+ URL: globalThis.URL,
51
+ Headers: globalThis.Headers,
52
+ Request: globalThis.Request,
53
+ Response: globalThis.Response,
54
+ // Additional globals
55
+ Object,
56
+ Array,
57
+ String,
58
+ Number,
59
+ Boolean,
60
+ Date,
61
+ Math,
62
+ JSON,
63
+ Error,
64
+ TypeError,
65
+ ReferenceError,
66
+ SyntaxError,
67
+ globalThis,
68
+ undefined: undefined,
69
+ null: null,
70
+ Infinity: Infinity,
71
+ NaN: NaN,
72
+ user: {
73
+ data: {
74
+ update: updateUserData,
75
+ get: getUserData,
76
+ create: linkUserData
77
+ }
78
+ },
79
+ // Environment variables function
80
+ env: (key) => envVars[key]
81
+ };
82
+ return sandbox;
83
+ }
84
+ /**
85
+ * Executes a tool in a VM sandbox
86
+ */
87
+ export async function executeTool(options) {
88
+ const { toolCode, inputs } = options;
89
+ // Create sandbox
90
+ const sandbox = createSandbox(options);
91
+ // Create the CommonJS wrapper code
92
+ const commonJsWrapper = `
93
+ const executeFunction = ${toolCode};
94
+
95
+ // Export the function for testing
96
+ module.exports = async (input) => {
97
+ return await executeFunction(input);
98
+ };
99
+ `;
100
+ // Execute the code in the sandbox
101
+ const context = vm.createContext(sandbox);
102
+ vm.runInContext(commonJsWrapper, context);
103
+ // Get the exported function and execute it
104
+ const executeFunction = context.module.exports;
105
+ return await executeFunction(inputs);
106
+ }
107
+ /**
108
+ * Creates a custom console that broadcasts logs via WebSocket
109
+ */
110
+ export function createBroadcastConsole(broadcastLog) {
111
+ return {
112
+ log: (...args) => {
113
+ const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
114
+ broadcastLog({
115
+ type: 'log',
116
+ subType: 'info',
117
+ message: message,
118
+ timestamp: new Date().toISOString(),
119
+ id: Date.now().toString()
120
+ });
121
+ // Also log to server console
122
+ console.log(...args);
123
+ },
124
+ error: (...args) => {
125
+ const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
126
+ broadcastLog({
127
+ type: 'log',
128
+ subType: 'error',
129
+ message: message,
130
+ timestamp: new Date().toISOString(),
131
+ id: Date.now().toString()
132
+ });
133
+ // Also log to server console
134
+ console.error(...args);
135
+ },
136
+ warn: (...args) => {
137
+ const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
138
+ broadcastLog({
139
+ type: 'log',
140
+ subType: 'warn',
141
+ message: message,
142
+ timestamp: new Date().toISOString(),
143
+ id: Date.now().toString()
144
+ });
145
+ // Also log to server console
146
+ console.warn(...args);
147
+ },
148
+ info: (...args) => {
149
+ const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
150
+ broadcastLog({
151
+ type: 'log',
152
+ subType: 'info',
153
+ message: message,
154
+ timestamp: new Date().toISOString(),
155
+ id: Date.now().toString()
156
+ });
157
+ // Also log to server console
158
+ console.info(...args);
159
+ }
160
+ };
161
+ }