iobroker.javascript 9.2.4 → 9.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +6 -15
  2. package/admin/assets/AiChatPanel-C3bIw_D5.js +317 -0
  3. package/admin/assets/{ScriptEditor-CmEeMfC2.js → ScriptEditor-8UU88tT5.js} +1 -1
  4. package/admin/assets/{ScriptEditorVanillaMonaco-C22Gpet7.js → ScriptEditorVanillaMonaco-Bn8xJ3LV.js} +2 -2
  5. package/admin/assets/index-C0ZnUCQf.js +990 -0
  6. package/admin/assets/{index-BTQOdA7v.js → index-CO9RA-Wq.js} +4 -4
  7. package/admin/assets/{index-7mrTpzQy.js → index-CRGWWpGb.js} +1 -1
  8. package/admin/assets/{index-Dggem6AX.js → index-CsyuIWlD.js} +1 -1
  9. package/admin/assets/localSharedImportMap-Db5DxDmY.js +1 -0
  10. package/admin/assets/{stateHoverProvider-DxTNxyYN.js → stateHoverProvider-C82xsI7Q.js} +1 -1
  11. package/admin/assets/{virtual_mf-REMOTE_ENTRY_ID_iobroker_javascript__remoteEntry_js-CeYV4czE.js → virtual_mf-REMOTE_ENTRY_ID_iobroker_javascript__remoteEntry_js-Bw22wl4W.js} +2 -2
  12. package/admin/custom/assets/index-CMsuyEZj.js +964 -0
  13. package/admin/custom/assets/{index-B6NrrORQ.js → index-CuAhFQ4L.js} +147 -147
  14. package/admin/custom/assets/{index-DTbSiEk2.js → index-mkhHeJ3Q.js} +1 -1
  15. package/admin/custom/assets/localSharedImportMap-BsMINYMr.js +1 -0
  16. package/admin/custom/assets/{virtual_mf-REMOTE_ENTRY_ID_ConfigCustomJavascriptSet__customComponents_js-ClQrb2n1.js → virtual_mf-REMOTE_ENTRY_ID_ConfigCustomJavascriptSet__customComponents_js-BzLNCazi.js} +2 -2
  17. package/admin/custom/customComponents.js +1 -1
  18. package/admin/i18n/de.json +8 -0
  19. package/admin/i18n/en.json +8 -0
  20. package/admin/jsonConfig.json +70 -9
  21. package/admin/mf-manifest.json +1 -1
  22. package/admin/remoteEntry.js +1 -1
  23. package/admin/tab.html +2 -2
  24. package/build/lib/aiProviderResolver.js +32 -5
  25. package/build/lib/aiProviderResolver.js.map +1 -1
  26. package/build/lib/sandbox.js +20 -12
  27. package/build/lib/sandbox.js.map +1 -1
  28. package/build/main.js +336 -41
  29. package/build/main.js.map +1 -1
  30. package/build/types.d.ts +10 -4
  31. package/io-package.json +34 -28
  32. package/package.json +9 -9
  33. package/admin/assets/AiChatPanel-B4vPXgLz.js +0 -315
  34. package/admin/assets/index-qoSy77Bg.js +0 -990
  35. package/admin/assets/localSharedImportMap-CVlAGiIu.js +0 -1
  36. package/admin/custom/assets/index-CT5PwRfY.js +0 -964
  37. package/admin/custom/assets/localSharedImportMap-ChjFDKsq.js +0 -1
package/build/main.js CHANGED
@@ -316,6 +316,14 @@ class JavaScript extends adapter_core_1.Adapter {
316
316
  mirror;
317
317
  stopCounters = {};
318
318
  setStateCountCheckInterval = null;
319
+ /**
320
+ * Decrypted AI API keys cached from the central credential store (manager mode),
321
+ * keyed by credential ID (e.g. `system.credentials.anthropic`). Kept fresh by the
322
+ * subscriptions set up in `subscribeAiCredentials`.
323
+ */
324
+ aiCredentialCache = new Map();
325
+ /** Unsubscribe callbacks for the AI credential subscriptions (manager mode). */
326
+ aiCredentialUnsubscribers = [];
319
327
  globalScript = '';
320
328
  /** Generated declarations for global TypeScripts */
321
329
  globalDeclarations = '';
@@ -323,8 +331,11 @@ class JavaScript extends adapter_core_1.Adapter {
323
331
  // have access to, because it depends on the compilation order
324
332
  knownGlobalDeclarationsByScript = {};
325
333
  globalScriptLines = 0;
334
+ /** Running counter to build unique names for ad-hoc scripts started via the "execute" message */
335
+ executeCounter = 0;
326
336
  // compiler instance for typescript
327
337
  tsServer;
338
+ logCollectors = [];
328
339
  ignoreObjectChange = new Set();
329
340
  debugState = {
330
341
  scriptName: '',
@@ -761,6 +772,7 @@ class JavaScript extends adapter_core_1.Adapter {
761
772
  clearInterval(this.setStateCountCheckInterval);
762
773
  this.setStateCountCheckInterval = null;
763
774
  }
775
+ await this.unsubscribeAiCredentials();
764
776
  await this.stopAllScripts();
765
777
  }
766
778
  catch (err) {
@@ -810,6 +822,116 @@ class JavaScript extends adapter_core_1.Adapter {
810
822
  }
811
823
  await this.main();
812
824
  }
825
+ /** Read and decrypt a single AI credential's key from the central store; returns '' (and logs) on error. */
826
+ async readAiCredentialKey(id) {
827
+ if (!adapter_core_1.Credentials?.getCredentials) {
828
+ this.log.warn(`Cannot read AI credential "${id}": Credentials API is only with 7.2 js-controller available`);
829
+ return '';
830
+ }
831
+ try {
832
+ const cred = await adapter_core_1.Credentials.getCredentials(this, id);
833
+ return (cred?.values?.key || '').trim();
834
+ }
835
+ catch (e) {
836
+ this.log.warn(`Cannot read AI credential "${id}": ${e instanceof Error ? e.message : String(e)}`);
837
+ return '';
838
+ }
839
+ }
840
+ /**
841
+ * Resolve the API key (and base URL) for an AI provider.
842
+ *
843
+ * In `manual` mode the key comes from the encryptedNative adapter config.
844
+ * In `manager` mode the config only stores the ID of a credential in the central
845
+ * ioBroker credential store (`system.credentials.*`); the actual key is taken from the
846
+ * `aiCredentialCache` (kept fresh by `subscribeAiCredentials`) or, for credentials we are
847
+ * not subscribed to (e.g. a not-yet-saved selection in the settings dialog), read directly.
848
+ *
849
+ * The settings-dialog Test button may pass form values that are not saved yet
850
+ * (`messageApiKey` / `messageCredentialId` / `credentialType`); those win over the stored config.
851
+ */
852
+ async resolveAiCredentials(provider, opts = {}) {
853
+ const mode = opts.credentialType || this.config.credentialType || 'manual';
854
+ if (mode === 'manager') {
855
+ // The base URL is not a secret and is resolved the same way in both modes.
856
+ const { baseUrl } = (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider, opts.messageBaseUrl);
857
+ const id = (opts.messageCredentialId || (0, aiProviderResolver_1.getProviderCredentialId)(this.config, provider)).trim();
858
+ if (!id) {
859
+ return { apiKey: '', baseUrl };
860
+ }
861
+ // Prefer the cached value kept fresh by the credential subscription.
862
+ const cached = this.aiCredentialCache.get(id);
863
+ const apiKey = cached !== undefined ? cached : await this.readAiCredentialKey(id);
864
+ return { apiKey, baseUrl };
865
+ }
866
+ // Manual mode. The Test button sends the current form key (maybe empty) — let it win.
867
+ if (opts.messageApiKey !== undefined) {
868
+ return (0, aiProviderResolver_1.resolveTestCredentials)(this.config, provider, opts.messageApiKey, opts.messageBaseUrl);
869
+ }
870
+ return (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider, opts.messageBaseUrl);
871
+ }
872
+ /**
873
+ * In `manager` mode, subscribe to all configured AI credentials so that edits made in the
874
+ * admin credential manager (Settings → Credentials) are picked up live, without restarting
875
+ * the adapter (the `system.credentials.*` objects are global, not part of the instance config).
876
+ * The decrypted keys are cached and kept fresh by the subscription handlers.
877
+ */
878
+ async subscribeAiCredentials() {
879
+ // Always start from a clean state (idempotent — also used to re-subscribe).
880
+ await this.unsubscribeAiCredentials();
881
+ if (this.config.credentialType !== 'manager') {
882
+ return;
883
+ }
884
+ if (!adapter_core_1.Credentials?.subscribeCredentials) {
885
+ this.log.warn(`Cannot subscribe AI credential: Credentials API is only with 7.2 js-controller available`);
886
+ return;
887
+ }
888
+ // Collect the distinct credential IDs configured across all AI providers.
889
+ const ids = new Set();
890
+ for (const provider of ['openai', 'anthropic', 'gemini', 'deepseek', 'custom']) {
891
+ const id = (0, aiProviderResolver_1.getProviderCredentialId)(this.config, provider);
892
+ if (id) {
893
+ ids.add(id);
894
+ }
895
+ }
896
+ for (const id of ids) {
897
+ try {
898
+ const unsubscribe = await adapter_core_1.Credentials.subscribeCredentials(this, id, (changedId, cred) => {
899
+ if (cred) {
900
+ this.aiCredentialCache.set(changedId, (cred.values?.key || '').trim());
901
+ this.log.debug(`AI credential "${changedId}" updated`);
902
+ }
903
+ else {
904
+ // The credential was deleted
905
+ this.aiCredentialCache.delete(changedId);
906
+ this.log.debug(`AI credential "${changedId}" was deleted`);
907
+ }
908
+ });
909
+ this.aiCredentialUnsubscribers.push(unsubscribe);
910
+ // Prime the cache with the current value (the handler may only fire on later changes).
911
+ this.aiCredentialCache.set(id, await this.readAiCredentialKey(id));
912
+ }
913
+ catch (e) {
914
+ this.log.warn(`Cannot subscribe to AI credential "${id}": ${e instanceof Error ? e.message : String(e)}`);
915
+ }
916
+ }
917
+ if (this.aiCredentialUnsubscribers.length) {
918
+ this.log.debug(`Subscribed to ${this.aiCredentialUnsubscribers.length} AI credential(s)`);
919
+ }
920
+ }
921
+ /** Tear down all AI credential subscriptions and clear the cache. */
922
+ async unsubscribeAiCredentials() {
923
+ const unsubscribers = this.aiCredentialUnsubscribers;
924
+ this.aiCredentialUnsubscribers = [];
925
+ this.aiCredentialCache.clear();
926
+ for (const unsubscribe of unsubscribers) {
927
+ try {
928
+ await unsubscribe();
929
+ }
930
+ catch (e) {
931
+ this.log.warn(`Cannot unsubscribe from AI credential: ${e instanceof Error ? e.message : String(e)}`);
932
+ }
933
+ }
934
+ }
813
935
  onMessage(obj) {
814
936
  switch (obj?.command) {
815
937
  // process messageTo commands
@@ -999,23 +1121,28 @@ class JavaScript extends adapter_core_1.Adapter {
999
1121
  }
1000
1122
  case 'chatCompletion': {
1001
1123
  // Proxy chat completion requests to an OpenAI-compatible API endpoint.
1002
- // API keys are resolved server-side from encryptedNative config they never
1003
- // leave the adapter (frontend only sends `provider`, not the key).
1004
- if (obj.callback) {
1124
+ // API keys are resolved server-side from the encryptedNative config or the central
1125
+ // credentials manager — they never leave the adapter (frontend only sends `provider`).
1126
+ void (async () => {
1127
+ if (!obj.callback) {
1128
+ return;
1129
+ }
1005
1130
  const chatModel = (obj.message?.model || '').trim();
1006
1131
  const messages = obj.message?.messages;
1007
1132
  const tools = obj.message?.tools;
1008
1133
  const provider = (obj.message?.provider || 'openai').trim();
1009
- const { apiKey, baseUrl } = (0, aiProviderResolver_1.resolveProviderCredentials)(this.config, provider, obj.message?.baseUrl);
1134
+ const { apiKey, baseUrl } = await this.resolveAiCredentials(provider, {
1135
+ messageBaseUrl: obj.message?.baseUrl,
1136
+ });
1010
1137
  // Anthropic, Gemini, and DeepSeek always require an API key; OpenAI-compatible allows empty key with custom base URL
1011
1138
  if (!apiKey &&
1012
1139
  (provider === 'anthropic' || provider === 'gemini' || provider === 'deepseek' || !baseUrl)) {
1013
1140
  this.sendTo(obj.from, obj.command, { error: 'No API key provided' }, obj.callback);
1014
- break;
1141
+ return;
1015
1142
  }
1016
1143
  if (!chatModel || !messages) {
1017
1144
  this.sendTo(obj.from, obj.command, { error: 'Model and messages are required' }, obj.callback);
1018
- break;
1145
+ return;
1019
1146
  }
1020
1147
  let url;
1021
1148
  const chatHeaders = {
@@ -1070,7 +1197,7 @@ class JavaScript extends adapter_core_1.Adapter {
1070
1197
  const resolved = resolveRequestModule(url);
1071
1198
  if (!resolved) {
1072
1199
  this.sendTo(obj.from, obj.command, { error: `Invalid API URL: ${url}` }, obj.callback);
1073
- break;
1200
+ return;
1074
1201
  }
1075
1202
  const { module: requestModule, isHttps } = resolved;
1076
1203
  try {
@@ -1143,21 +1270,29 @@ class JavaScript extends adapter_core_1.Adapter {
1143
1270
  catch (error) {
1144
1271
  this.sendTo(obj.from, obj.command, { error: `Connection failed: ${error.toString()}` }, obj.callback);
1145
1272
  }
1146
- }
1273
+ })();
1147
1274
  break;
1148
1275
  }
1149
1276
  case 'testApiConnection': {
1150
1277
  // Test connection to an OpenAI-compatible API endpoint.
1151
1278
  // The settings-dialog Test button sends the current form value as `apiKey`
1152
1279
  // (so users can test before saving); otherwise we fall back to the stored key.
1153
- if (obj.callback) {
1280
+ void (async () => {
1281
+ if (!obj.callback) {
1282
+ return;
1283
+ }
1154
1284
  const provider = (obj.message?.provider || 'openai').trim();
1155
- const { apiKey, baseUrl } = (0, aiProviderResolver_1.resolveTestCredentials)(this.config, provider, obj.message?.apiKey, obj.message?.baseUrl);
1285
+ const { apiKey, baseUrl } = await this.resolveAiCredentials(provider, {
1286
+ messageApiKey: obj.message?.apiKey,
1287
+ messageBaseUrl: obj.message?.baseUrl,
1288
+ messageCredentialId: obj.message?.credentialId,
1289
+ credentialType: obj.message?.credentialType,
1290
+ });
1156
1291
  // Anthropic, Gemini, and DeepSeek always require an API key; OpenAI-compatible allows empty key with custom base URL
1157
1292
  if (!apiKey &&
1158
1293
  (provider === 'anthropic' || provider === 'gemini' || provider === 'deepseek' || !baseUrl)) {
1159
1294
  this.sendTo(obj.from, obj.command, { error: 'No API key provided' }, obj.callback);
1160
- break;
1295
+ return;
1161
1296
  }
1162
1297
  let url;
1163
1298
  const testHeaders = {
@@ -1187,7 +1322,7 @@ class JavaScript extends adapter_core_1.Adapter {
1187
1322
  const resolved = resolveRequestModule(url);
1188
1323
  if (!resolved) {
1189
1324
  this.sendTo(obj.from, obj.command, { error: `Invalid API URL: ${url}` }, obj.callback);
1190
- break;
1325
+ return;
1191
1326
  }
1192
1327
  const { module: requestModule, isHttps } = resolved;
1193
1328
  try {
@@ -1248,7 +1383,7 @@ class JavaScript extends adapter_core_1.Adapter {
1248
1383
  catch (error) {
1249
1384
  this.sendTo(obj.from, obj.command, { error: `Connection failed: ${error.toString()}` }, obj.callback);
1250
1385
  }
1251
- }
1386
+ })();
1252
1387
  break;
1253
1388
  }
1254
1389
  case 'getAvailableAiProviders': {
@@ -1301,6 +1436,19 @@ class JavaScript extends adapter_core_1.Adapter {
1301
1436
  }
1302
1437
  break;
1303
1438
  }
1439
+ case 'execute': {
1440
+ if (obj.callback) {
1441
+ void this.executeScript(obj.message)
1442
+ .then(result => this.sendTo(obj.from, obj.command, result, obj.callback))
1443
+ .catch(err => this.sendTo(obj.from, obj.command, {
1444
+ ok: false,
1445
+ error: `Internal error: ${err}`,
1446
+ logs: [],
1447
+ output: '',
1448
+ }, obj.callback));
1449
+ }
1450
+ break;
1451
+ }
1304
1452
  }
1305
1453
  }
1306
1454
  onLog(msg) {
@@ -1314,6 +1462,14 @@ class JavaScript extends adapter_core_1.Adapter {
1314
1462
  }
1315
1463
  }
1316
1464
  }
1465
+ // Special case if some script is executed now with "execute" command, and we see "script.js.__execute_X:" at the beginning
1466
+ if (this.logCollectors.length) {
1467
+ for (const logCollector of this.logCollectors) {
1468
+ if (msg.message.includes(`${logCollector.name}:`)) {
1469
+ logCollector.collector(msg.severity, msg.message);
1470
+ }
1471
+ }
1472
+ }
1317
1473
  }
1318
1474
  logError(scriptName, msg, e, offs) {
1319
1475
  const stack = e.stack ? e.stack.toString().split('\n') : e ? e.toString() : '';
@@ -1419,6 +1575,9 @@ class JavaScript extends adapter_core_1.Adapter {
1419
1575
  // Store allowSelfSignedCerts on the context, so sandbox HTTP functions can use it
1420
1576
  // without setting the global process.env.NODE_TLS_REJECT_UNAUTHORIZED (which affects all adapters in compact mode)
1421
1577
  this.context.allowSelfSignedCerts = this.config.allowSelfSignedCerts;
1578
+ // In `manager` credential mode, subscribe to the configured AI credentials so changes in the
1579
+ // central credential store are picked up live (the keys are cached for the AI sendTo handlers).
1580
+ await this.subscribeAiCredentials();
1422
1581
  const doc = await this.getObjectViewAsync('script', 'javascript', {});
1423
1582
  if (doc?.rows?.length) {
1424
1583
  // sort global scripts if configured
@@ -2194,7 +2353,14 @@ class JavaScript extends adapter_core_1.Adapter {
2194
2353
  return false;
2195
2354
  }
2196
2355
  }
2197
- execute(script, name, engineType, verbose, debug) {
2356
+ execute(script, name, engineType, verbose, debug,
2357
+ /**
2358
+ * Optional sink for the "execute" message API. When provided, the script runs in an
2359
+ * ephemeral diagnostic mode: every log line (the script's own `log()`/`console.*` output
2360
+ * AND all verbose internal operations) is forwarded to this collector instead of the
2361
+ * adapter log, and no `scriptProblem` state is written.
2362
+ */
2363
+ logCollector) {
2198
2364
  script.intervals = new Set();
2199
2365
  script.timeouts = new Set();
2200
2366
  script.schedules = [];
@@ -2206,12 +2372,14 @@ class JavaScript extends adapter_core_1.Adapter {
2206
2372
  script.subscribesFile = {};
2207
2373
  script.setStatePerMinuteCounter = 0;
2208
2374
  script.setStatePerMinuteProblemCounter = 0;
2209
- void this.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, {
2210
- val: false,
2211
- ack: true,
2212
- expire: 1000,
2213
- });
2214
- const sandbox = (0, sandbox_1.sandBox)(script, name, verbose, debug, this.context);
2375
+ if (!logCollector) {
2376
+ void this.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, {
2377
+ val: false,
2378
+ ack: true,
2379
+ expire: 1000,
2380
+ });
2381
+ }
2382
+ const sandbox = (0, sandbox_1.sandBox)(script, name, verbose, debug, this.context, logCollector);
2215
2383
  try {
2216
2384
  script.script.runInNewContext(sandbox, {
2217
2385
  filename: name,
@@ -2220,13 +2388,131 @@ class JavaScript extends adapter_core_1.Adapter {
2220
2388
  });
2221
2389
  }
2222
2390
  catch (err) {
2223
- void this.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, {
2224
- val: true,
2225
- ack: true,
2226
- c: 'execute',
2227
- });
2228
- this.logError(name, 'Error by run:', err);
2391
+ if (logCollector) {
2392
+ const e = err;
2393
+ const stack = (e?.stack ? e.stack.toString() : String(err))
2394
+ .split('\n')
2395
+ .map(line => this.fixLineNo(line))
2396
+ .join('\n');
2397
+ logCollector('error', `Error by run: ${stack}`);
2398
+ }
2399
+ else {
2400
+ void this.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, {
2401
+ val: true,
2402
+ ack: true,
2403
+ c: 'execute',
2404
+ });
2405
+ this.logError(name, 'Error by run:', err);
2406
+ }
2407
+ }
2408
+ }
2409
+ /**
2410
+ * Run an ad-hoc script sent via the `execute` message and return everything it logged.
2411
+ *
2412
+ * The script is compiled (JavaScript or TypeScript), executed with the same sandbox API as a
2413
+ * regular script (verbose by default, so internal operations like setState/subscribe are logged
2414
+ * too), left running for `timeout` ms to collect asynchronous output, and afterwards stopped and
2415
+ * fully cleaned up (timers, subscriptions, schedules). It is ephemeral: no script object or
2416
+ * states are created.
2417
+ *
2418
+ * Expected `message`:
2419
+ * - `source` / `code` (string, required) – the script source
2420
+ * - `engineType` (string, optional) – `TypeScript/ts` to compile as TypeScript, otherwise JavaScript
2421
+ * - `verbose` (boolean, optional, default `true`) – log internal sandbox operations
2422
+ * - `logLevel` (silly|debug|info|warn|error, optional, default `silly`) – minimum severity to return
2423
+ * - `timeout` (number ms, optional, default 5000, clamped to 0…60000) – collection window
2424
+ * - `maxLogs` (number, optional, default 5000) – cap on returned log lines
2425
+ */
2426
+ async executeScript(message) {
2427
+ const LEVELS = ['silly', 'debug', 'info', 'warn', 'error'];
2428
+ const source = message?.source ?? message?.code;
2429
+ const engineTypeStr = (message?.engineType || '').toString().toLowerCase();
2430
+ const isTypeScript = engineTypeStr.startsWith('typescript') || engineTypeStr === 'ts';
2431
+ const engineType = isTypeScript ? 'TypeScript/ts' : 'Javascript/js';
2432
+ const empty = (error) => ({ ok: false, error, engineType, runtime: 0, truncated: false, logs: [], output: '' });
2433
+ if (typeof source !== 'string' || !source.trim()) {
2434
+ return empty('No source code provided');
2435
+ }
2436
+ if (this.context.debugMode) {
2437
+ return empty('Cannot execute a script while a debug session is active');
2438
+ }
2439
+ let timeout = parseInt(message?.timeout, 10);
2440
+ if (isNaN(timeout)) {
2441
+ timeout = 5000;
2442
+ }
2443
+ timeout = Math.max(0, Math.min(timeout, 60000));
2444
+ const verbose = message?.verbose !== false;
2445
+ const minLevel = message?.logLevel
2446
+ ? LEVELS.includes(message?.logLevel)
2447
+ ? message.logLevel
2448
+ : 'silly'
2449
+ : 'silly';
2450
+ let maxLogs = parseInt(message?.maxLogs, 10);
2451
+ if (isNaN(maxLogs) || maxLogs <= 0) {
2452
+ maxLogs = 5000;
2453
+ }
2454
+ const name = `${SCRIPT_CODE_MARKER}__execute_${++this.executeCounter}`;
2455
+ // Compile the source the same way regular scripts are compiled
2456
+ let createdScript;
2457
+ if (isTypeScript) {
2458
+ const transformedSource = (0, typescriptTools_1.transformScriptBeforeCompilation)(source, false);
2459
+ const filename = (0, typescriptTools_1.scriptIdToTSFilename)(name);
2460
+ let tsCompiled;
2461
+ try {
2462
+ tsCompiled = this.tsServer.compile(filename, transformedSource);
2463
+ }
2464
+ catch (err) {
2465
+ return empty(`TypeScript compilation failed: ${err}`);
2466
+ }
2467
+ if (!tsCompiled.success) {
2468
+ const errors = tsCompiled.diagnostics.map(diag => diag.annotatedSource).join('\n');
2469
+ return empty(`TypeScript compilation failed:\n${errors}`);
2470
+ }
2471
+ createdScript = this.createVM(`${this.globalScript}\n${tsCompiled.result || ''}`, name, false);
2472
+ }
2473
+ else {
2474
+ createdScript = this.createVM(`${this.globalScript}\n${source}`, name, true);
2229
2475
  }
2476
+ if (!createdScript) {
2477
+ return empty('Compilation failed');
2478
+ }
2479
+ const logs = [];
2480
+ let truncated = false;
2481
+ const collector = (severity, msg) => {
2482
+ if (logs.length >= maxLogs) {
2483
+ truncated = true;
2484
+ return;
2485
+ }
2486
+ logs.push({ ts: Date.now(), severity, message: msg });
2487
+ };
2488
+ this.logCollectors.push({ name, collector });
2489
+ this.updateLogSubscriptions();
2490
+ this.scripts[name] = createdScript;
2491
+ this.execute(createdScript, name, engineType, verbose, false, collector);
2492
+ // Let asynchronous output (timeouts, awaited code, triggered subscriptions) accumulate
2493
+ if (timeout) {
2494
+ await new Promise(resolve => setTimeout(resolve, timeout));
2495
+ }
2496
+ // Stop and clean up the ephemeral script (timers, subscriptions, schedules, …)
2497
+ await this.stopScript(name, true);
2498
+ const pos = this.logCollectors.findIndex(it => it.name === name);
2499
+ if (pos !== -1) {
2500
+ this.logCollectors.splice(pos, 1);
2501
+ }
2502
+ this.updateLogSubscriptions();
2503
+ const minIdx = LEVELS.indexOf(minLevel);
2504
+ const filtered = logs.filter(entry => {
2505
+ const idx = LEVELS.indexOf(entry.severity);
2506
+ return idx < 0 || idx >= minIdx;
2507
+ });
2508
+ return {
2509
+ ok: true,
2510
+ engineType,
2511
+ runtime: timeout,
2512
+ truncated,
2513
+ logs: filtered,
2514
+ output: filtered.map(entry => `[${entry.severity}] ${entry.message}`).join('\n'),
2515
+ };
2230
2516
  }
2231
2517
  /**
2232
2518
  * Finds the index of `id` in a sorted array using binary search – O(log n).
@@ -2302,35 +2588,44 @@ class JavaScript extends adapter_core_1.Adapter {
2302
2588
  }
2303
2589
  // Analyze if logs are still required or not
2304
2590
  updateLogSubscriptions() {
2305
- let found = false;
2306
- // go through all scripts and check if some script still requires logs
2307
- Object.keys(this.logSubscriptions).forEach(scriptName => {
2308
- if (!this.logSubscriptions?.[scriptName] || !this.logSubscriptions[scriptName].length) {
2309
- delete this.logSubscriptions[scriptName];
2310
- }
2311
- else {
2312
- found = true;
2313
- }
2314
- });
2591
+ let found = '';
2592
+ if (this.logCollectors.length) {
2593
+ found = this.logCollectors[0].name;
2594
+ }
2595
+ else {
2596
+ // go through all scripts and check if some script still requires logs
2597
+ Object.keys(this.logSubscriptions).forEach(scriptName => {
2598
+ if (!this.logSubscriptions?.[scriptName] || !this.logSubscriptions[scriptName].length) {
2599
+ delete this.logSubscriptions[scriptName];
2600
+ }
2601
+ else {
2602
+ found = scriptName;
2603
+ }
2604
+ });
2605
+ }
2315
2606
  if (this.requireLog) {
2316
2607
  if (found && !this.logSubscribed) {
2317
2608
  this.logSubscribed = true;
2318
2609
  void this.requireLog(this.logSubscribed);
2319
- this.log.info(`Subscribed to log messages (found logSubscriptions)`);
2610
+ this.log.info(`Subscribed to log messages (at least because of ${found})`);
2320
2611
  }
2321
2612
  else if (!found && this.logSubscribed) {
2322
2613
  this.logSubscribed = false;
2323
2614
  void this.requireLog(this.logSubscribed);
2324
- this.log.info(`Unsubscribed from log messages (not found logSubscriptions)`);
2615
+ this.log.info(`Unsubscribed from log messages (not found any subscribers)`);
2325
2616
  }
2326
2617
  }
2327
2618
  }
2328
- async stopScript(name) {
2619
+ async stopScript(name, silent) {
2329
2620
  if (!this.scripts[name]) {
2330
2621
  return false;
2331
2622
  }
2332
- this.log.info(`${name}: Stopping script`);
2333
- await this.setState(`scriptEnabled.${name.substring(SCRIPT_CODE_MARKER.length)}`, false, true);
2623
+ // `silent` is used for ephemeral scripts started via the "execute" message – they have no
2624
+ // `scriptEnabled` state and should not appear in the adapter log.
2625
+ if (!silent) {
2626
+ this.log.info(`${name}: Stopping script`);
2627
+ await this.setState(`scriptEnabled.${name.substring(SCRIPT_CODE_MARKER.length)}`, false, true);
2628
+ }
2334
2629
  if (this.messageBusHandlers[name]) {
2335
2630
  delete this.messageBusHandlers[name];
2336
2631
  }