@promptbook/node 0.112.0-11 → 0.112.0-12

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 (75) hide show
  1. package/esm/index.es.js +2402 -1154
  2. package/esm/index.es.js.map +1 -1
  3. package/esm/src/book-2.0/agent-source/AgentBasicInformation.d.ts +1 -1
  4. package/esm/src/book-components/Chat/Chat/ChatToolCallModal.test.d.ts +2 -0
  5. package/esm/src/book-components/Chat/Chat/renderToolCallDetails.d.ts +4 -0
  6. package/esm/src/book-components/Chat/utils/getToolCallChipletInfo.timeout.test.d.ts +1 -0
  7. package/esm/src/book-components/Chat/utils/timeoutToolCallPresentation.d.ts +123 -0
  8. package/esm/src/book-components/Chat/utils/timeoutToolCallPresentation.test.d.ts +1 -0
  9. package/esm/src/commitments/USE_CALENDAR/USE_CALENDAR.d.ts +42 -0
  10. package/esm/src/commitments/USE_CALENDAR/USE_CALENDAR.test.d.ts +1 -0
  11. package/esm/src/commitments/USE_CALENDAR/UseCalendarToolNames.d.ts +13 -0
  12. package/esm/src/commitments/USE_CALENDAR/UseCalendarWallet.d.ts +9 -0
  13. package/esm/src/commitments/USE_CALENDAR/calendarReference.d.ts +68 -0
  14. package/esm/src/commitments/USE_CALENDAR/callGoogleCalendarApi.d.ts +19 -0
  15. package/esm/src/commitments/USE_CALENDAR/createUseCalendarToolFunctions.d.ts +8 -0
  16. package/esm/src/commitments/USE_CALENDAR/createUseCalendarTools.d.ts +7 -0
  17. package/esm/src/commitments/USE_CALENDAR/getUseCalendarToolTitles.d.ts +7 -0
  18. package/esm/src/commitments/USE_CALENDAR/normalizeConfiguredCalendars.d.ts +19 -0
  19. package/esm/src/commitments/USE_CALENDAR/resolveUseCalendarToolRuntimeOrWalletCredentialResult.d.ts +34 -0
  20. package/esm/src/commitments/_common/toolRuntimeContext.d.ts +9 -0
  21. package/esm/src/commitments/index.d.ts +2 -1
  22. package/esm/src/executables/apps/locateVscode.d.ts +7 -0
  23. package/esm/src/executables/apps/locateVscode.test.d.ts +1 -0
  24. package/esm/src/executables/browsers/locateBrowser.d.ts +10 -0
  25. package/esm/src/executables/browsers/locateBrowser.test.d.ts +1 -0
  26. package/esm/src/executables/browsers/locateChrome.d.ts +7 -0
  27. package/esm/src/executables/browsers/locateChrome.test.d.ts +1 -0
  28. package/esm/src/executables/browsers/locateDefaultSystemBrowser.d.ts +10 -0
  29. package/esm/src/executables/browsers/locateDefaultSystemBrowser.test.d.ts +1 -0
  30. package/esm/src/executables/browsers/locateEdge.d.ts +7 -0
  31. package/esm/src/executables/browsers/locateEdge.test.d.ts +1 -0
  32. package/esm/src/executables/browsers/locateFirefox.d.ts +7 -0
  33. package/esm/src/executables/browsers/locateFirefox.test.d.ts +1 -0
  34. package/esm/src/executables/browsers/locateInternetExplorer.d.ts +7 -0
  35. package/esm/src/executables/browsers/locateInternetExplorer.test.d.ts +1 -0
  36. package/esm/src/executables/browsers/locateSafari.d.ts +7 -0
  37. package/esm/src/version.d.ts +1 -1
  38. package/package.json +2 -2
  39. package/umd/index.umd.js +2407 -1158
  40. package/umd/index.umd.js.map +1 -1
  41. package/umd/src/book-2.0/agent-source/AgentBasicInformation.d.ts +1 -1
  42. package/umd/src/book-components/Chat/Chat/ChatToolCallModal.test.d.ts +2 -0
  43. package/umd/src/book-components/Chat/Chat/renderToolCallDetails.d.ts +4 -0
  44. package/umd/src/book-components/Chat/utils/getToolCallChipletInfo.timeout.test.d.ts +1 -0
  45. package/umd/src/book-components/Chat/utils/timeoutToolCallPresentation.d.ts +123 -0
  46. package/umd/src/book-components/Chat/utils/timeoutToolCallPresentation.test.d.ts +1 -0
  47. package/umd/src/commitments/USE_CALENDAR/USE_CALENDAR.d.ts +42 -0
  48. package/umd/src/commitments/USE_CALENDAR/USE_CALENDAR.test.d.ts +1 -0
  49. package/umd/src/commitments/USE_CALENDAR/UseCalendarToolNames.d.ts +13 -0
  50. package/umd/src/commitments/USE_CALENDAR/UseCalendarWallet.d.ts +9 -0
  51. package/umd/src/commitments/USE_CALENDAR/calendarReference.d.ts +68 -0
  52. package/umd/src/commitments/USE_CALENDAR/callGoogleCalendarApi.d.ts +19 -0
  53. package/umd/src/commitments/USE_CALENDAR/createUseCalendarToolFunctions.d.ts +8 -0
  54. package/umd/src/commitments/USE_CALENDAR/createUseCalendarTools.d.ts +7 -0
  55. package/umd/src/commitments/USE_CALENDAR/getUseCalendarToolTitles.d.ts +7 -0
  56. package/umd/src/commitments/USE_CALENDAR/normalizeConfiguredCalendars.d.ts +19 -0
  57. package/umd/src/commitments/USE_CALENDAR/resolveUseCalendarToolRuntimeOrWalletCredentialResult.d.ts +34 -0
  58. package/umd/src/commitments/_common/toolRuntimeContext.d.ts +9 -0
  59. package/umd/src/commitments/index.d.ts +2 -1
  60. package/umd/src/executables/apps/locateVscode.d.ts +7 -0
  61. package/umd/src/executables/apps/locateVscode.test.d.ts +1 -0
  62. package/umd/src/executables/browsers/locateBrowser.d.ts +10 -0
  63. package/umd/src/executables/browsers/locateBrowser.test.d.ts +1 -0
  64. package/umd/src/executables/browsers/locateChrome.d.ts +7 -0
  65. package/umd/src/executables/browsers/locateChrome.test.d.ts +1 -0
  66. package/umd/src/executables/browsers/locateDefaultSystemBrowser.d.ts +10 -0
  67. package/umd/src/executables/browsers/locateDefaultSystemBrowser.test.d.ts +1 -0
  68. package/umd/src/executables/browsers/locateEdge.d.ts +7 -0
  69. package/umd/src/executables/browsers/locateEdge.test.d.ts +1 -0
  70. package/umd/src/executables/browsers/locateFirefox.d.ts +7 -0
  71. package/umd/src/executables/browsers/locateFirefox.test.d.ts +1 -0
  72. package/umd/src/executables/browsers/locateInternetExplorer.d.ts +7 -0
  73. package/umd/src/executables/browsers/locateInternetExplorer.test.d.ts +1 -0
  74. package/umd/src/executables/browsers/locateSafari.d.ts +7 -0
  75. package/umd/src/version.d.ts +1 -1
package/umd/index.umd.js CHANGED
@@ -1,8 +1,8 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('colors'), require('fs/promises'), require('path'), require('spacetrim'), require('jszip'), require('crypto'), require('crypto-js'), require('crypto-js/enc-hex'), require('@mozilla/readability'), require('jsdom'), require('showdown'), require('os'), require('configchecker'), require('locate-app'), require('playwright'), require('child_process'), require('waitasecond'), require('dotenv'), require('crypto-js/sha256'), require('rxjs'), require('mime-types'), require('moment'), require('papaparse'), require('@openai/agents'), require('bottleneck'), require('openai')) :
3
- typeof define === 'function' && define.amd ? define(['exports', 'colors', 'fs/promises', 'path', 'spacetrim', 'jszip', 'crypto', 'crypto-js', 'crypto-js/enc-hex', '@mozilla/readability', 'jsdom', 'showdown', 'os', 'configchecker', 'locate-app', 'playwright', 'child_process', 'waitasecond', 'dotenv', 'crypto-js/sha256', 'rxjs', 'mime-types', 'moment', 'papaparse', '@openai/agents', 'bottleneck', 'openai'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["promptbook-node"] = {}, global.colors, global.promises, global.path, global.spaceTrim$1, global.JSZip, global.crypto, global.cryptoJs, global.hexEncoder, global.readability, global.jsdom, global.showdown, global.os, global.configchecker, global.locateApp$1, global.playwright, global.child_process, global.waitasecond, global.dotenv, global.sha256, global.rxjs, global.mimeTypes, global.moment, global.papaparse, global.agents, global.Bottleneck, global.OpenAI));
5
- })(this, (function (exports, colors, promises, path, spaceTrim$1, JSZip, crypto, cryptoJs, hexEncoder, readability, jsdom, showdown, os, configchecker, locateApp$1, playwright, child_process, waitasecond, dotenv, sha256, rxjs, mimeTypes, moment, papaparse, agents, Bottleneck, OpenAI) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('colors'), require('fs/promises'), require('path'), require('spacetrim'), require('jszip'), require('crypto'), require('crypto-js'), require('crypto-js/enc-hex'), require('@mozilla/readability'), require('jsdom'), require('showdown'), require('os'), require('configchecker'), require('playwright'), require('child_process'), require('waitasecond'), require('dotenv'), require('crypto-js/sha256'), require('rxjs'), require('mime-types'), require('moment'), require('papaparse'), require('@openai/agents'), require('bottleneck'), require('openai')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', 'colors', 'fs/promises', 'path', 'spacetrim', 'jszip', 'crypto', 'crypto-js', 'crypto-js/enc-hex', '@mozilla/readability', 'jsdom', 'showdown', 'os', 'configchecker', 'playwright', 'child_process', 'waitasecond', 'dotenv', 'crypto-js/sha256', 'rxjs', 'mime-types', 'moment', 'papaparse', '@openai/agents', 'bottleneck', 'openai'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["promptbook-node"] = {}, global.colors, global.promises, global.path, global.spaceTrim$1, global.JSZip, global.crypto, global.cryptoJs, global.hexEncoder, global.readability, global.jsdom, global.showdown, global.os, global.configchecker, global.playwright, global.child_process, global.waitasecond, global.dotenv, global.sha256, global.rxjs, global.mimeTypes, global.moment, global.papaparse, global.agents, global.Bottleneck, global.OpenAI));
5
+ })(this, (function (exports, colors, promises, path, spaceTrim$1, JSZip, crypto, cryptoJs, hexEncoder, readability, jsdom, showdown, os, configchecker, playwright, child_process, waitasecond, dotenv, sha256, rxjs, mimeTypes, moment, papaparse, agents, Bottleneck, OpenAI) { 'use strict';
6
6
 
7
7
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
8
8
 
@@ -48,7 +48,7 @@
48
48
  * @generated
49
49
  * @see https://github.com/webgptorg/promptbook
50
50
  */
51
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-11';
51
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-12';
52
52
  /**
53
53
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
54
54
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -14813,117 +14813,449 @@
14813
14813
  };
14814
14814
 
14815
14815
  /**
14816
- * Creates one standard abort error for cancelled retry loops.
14816
+ * Normalize options for `execCommand` and `execCommands`
14817
14817
  *
14818
- * @private utility for Agents Server runtime retries
14819
- */
14820
- function createAbortError$1() {
14821
- const error = new Error('Operation was aborted.');
14822
- error.name = 'AbortError';
14823
- return error;
14824
- }
14825
- /**
14826
- * Throws when the supplied signal is already aborted.
14818
+ * Note: `$` is used to indicate that this function behaves differently according to `process.platform`
14827
14819
  *
14828
- * @private utility for Agents Server runtime retries
14820
+ * @private internal utility of `execCommand` and `execCommands`
14829
14821
  */
14830
- function assertNotAborted(signal) {
14831
- if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
14832
- throw createAbortError$1();
14822
+ function $execCommandNormalizeOptions(options) {
14823
+ var _a, _b, _c, _d;
14824
+ let command;
14825
+ let cwd;
14826
+ let crashOnError;
14827
+ let args = [];
14828
+ let timeout;
14829
+ let isVerbose;
14830
+ let env;
14831
+ if (typeof options === 'string') {
14832
+ // TODO: [1] DRY default values
14833
+ command = options;
14834
+ cwd = process.cwd();
14835
+ crashOnError = true;
14836
+ timeout = Infinity; // <- TODO: [⏳]
14837
+ isVerbose = DEFAULT_IS_VERBOSE;
14838
+ env = undefined;
14833
14839
  }
14834
- }
14835
- /**
14836
- * Waits for a duration while respecting cancellation.
14837
- *
14838
- * @private utility for Agents Server runtime retries
14839
- */
14840
- async function sleepWithAbort(delayMs, signal) {
14841
- if (delayMs <= 0) {
14842
- assertNotAborted(signal);
14843
- return;
14840
+ else {
14841
+ /*
14842
+ TODO:
14843
+ if ((options as any).commands !== undefined) {
14844
+ commands = (options as any).commands;
14845
+ } else {
14846
+ commands = [(options as any).command];
14847
+ }
14848
+ */
14849
+ // TODO: [1] DRY default values
14850
+ command = options.command;
14851
+ cwd = (_a = options.cwd) !== null && _a !== void 0 ? _a : process.cwd();
14852
+ crashOnError = (_b = options.crashOnError) !== null && _b !== void 0 ? _b : true;
14853
+ timeout = (_c = options.timeout) !== null && _c !== void 0 ? _c : Infinity;
14854
+ isVerbose = (_d = options.isVerbose) !== null && _d !== void 0 ? _d : DEFAULT_IS_VERBOSE;
14855
+ env = options.env;
14844
14856
  }
14845
- await new Promise((resolve, reject) => {
14846
- const timeout = setTimeout(() => {
14847
- signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
14848
- resolve();
14849
- }, delayMs);
14850
- const onAbort = () => {
14851
- clearTimeout(timeout);
14852
- signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
14853
- reject(createAbortError$1());
14854
- };
14855
- signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
14856
- });
14857
+ // TODO: /(-[a-zA-Z0-9-]+\s+[^\s]*)|[^\s]*/g
14858
+ const _ = Array.from(command.matchAll(/(".*")|([^\s]*)/g))
14859
+ .map(([match]) => match)
14860
+ .filter((arg) => arg !== '');
14861
+ if (_.length > 1) {
14862
+ [command, ...args] = _;
14863
+ }
14864
+ if (options.args) {
14865
+ args = [...args, ...options.args];
14866
+ }
14867
+ let humanReadableCommand = !['npx', 'npm'].includes(command) ? command : args[0];
14868
+ if (['ts-node'].includes(humanReadableCommand)) {
14869
+ humanReadableCommand += ` ${args[1]}`;
14870
+ }
14871
+ if (/^win/.test(process.platform) && ['npm', 'npx'].includes(command)) {
14872
+ command = `${command}.cmd`;
14873
+ }
14874
+ return { command, humanReadableCommand, args, cwd, crashOnError, timeout, isVerbose, env };
14857
14875
  }
14876
+ // TODO: This should show type error> execCommandNormalizeOptions({ command: '', commands: [''] });
14877
+
14858
14878
  /**
14859
- * Resolves the retry wait duration for one failed attempt.
14879
+ * Run one command in a shell
14860
14880
  *
14861
- * @private utility for Agents Server runtime retries
14862
- */
14863
- function resolveBackoffDelayMs(options) {
14864
- const exponentialDelay = options.initialDelayMs * Math.pow(options.backoffFactor, Math.max(0, options.attempt - 1));
14865
- const boundedDelay = Math.min(exponentialDelay, options.maxDelayMs);
14866
- const jitterDelay = boundedDelay * options.jitterRatio * Math.max(0, options.random());
14867
- return Math.max(0, Math.round(boundedDelay + jitterDelay));
14868
- }
14869
- /**
14870
- * Retries one async operation with exponential backoff and jitter.
14871
14881
  *
14872
- * @private utility for Agents Server runtime retries
14882
+ * Note: There are 2 similar functions in the codebase:
14883
+ * - `$execCommand` which runs a single command
14884
+ * - `$execCommands` which runs multiple commands
14885
+ * Note: `$` is used to indicate that this function is not a pure function - it runs a command in a shell
14886
+ *
14887
+ * @public exported from `@promptbook/node`
14873
14888
  */
14874
- async function retryWithBackoff(operation, options) {
14875
- var _a, _b, _c;
14876
- const startedAt = Date.now();
14877
- const totalAttempts = Math.max(1, options.retries + 1);
14878
- const random = (_a = options.random) !== null && _a !== void 0 ? _a : Math.random;
14879
- const sleep = (_b = options.sleep) !== null && _b !== void 0 ? _b : sleepWithAbort;
14880
- for (let attempt = 1; attempt <= totalAttempts; attempt++) {
14881
- assertNotAborted(options.signal);
14882
- try {
14883
- const value = await operation(attempt);
14884
- return {
14885
- value,
14886
- attempts: attempt,
14887
- durationMs: Date.now() - startedAt,
14888
- };
14889
+ function $execCommand(options) {
14890
+ if (!$isRunningInNode()) {
14891
+ throw new EnvironmentMismatchError('Function `$execCommand` can run only in Node environment.js');
14892
+ }
14893
+ return new Promise((resolve, reject) => {
14894
+ // eslint-disable-next-line prefer-const
14895
+ const { command, humanReadableCommand, args, cwd, crashOnError, timeout, isVerbose = DEFAULT_IS_VERBOSE, env, } = $execCommandNormalizeOptions(options);
14896
+ if (timeout !== Infinity) {
14897
+ // TODO: In waitasecond forTime(Infinity) should be equivalent to forEver()
14898
+ waitasecond.forTime(timeout).then(() => {
14899
+ if (crashOnError) {
14900
+ reject(new Error(`Command "${humanReadableCommand}" exceeded time limit of ${timeout}ms`));
14901
+ }
14902
+ else {
14903
+ console.warn(`Command "${humanReadableCommand}" exceeded time limit of ${timeout}ms but continues running`);
14904
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
14905
+ resolve('Command exceeded time limit');
14906
+ }
14907
+ });
14889
14908
  }
14890
- catch (error) {
14891
- const isLastAttempt = attempt >= totalAttempts;
14892
- const isRetryable = options.shouldRetry ? options.shouldRetry(error, attempt) : true;
14893
- if (isLastAttempt || !isRetryable) {
14894
- throw error;
14909
+ if (isVerbose) {
14910
+ console.info(colors__default["default"].yellow(cwd) + ' ' + colors__default["default"].green(command) + ' ' + colors__default["default"].blue(args.join(' ')));
14911
+ }
14912
+ try {
14913
+ const commandProcess = child_process.spawn(command, args, {
14914
+ cwd,
14915
+ shell: true,
14916
+ env: env ? { ...process.env, ...env } : process.env,
14917
+ });
14918
+ if (isVerbose) {
14919
+ commandProcess.on('message', (message) => {
14920
+ console.info({ message });
14921
+ });
14895
14922
  }
14896
- const delayMs = resolveBackoffDelayMs({
14897
- attempt,
14898
- initialDelayMs: options.initialDelayMs,
14899
- maxDelayMs: options.maxDelayMs,
14900
- backoffFactor: options.backoffFactor,
14901
- jitterRatio: options.jitterRatio,
14902
- random,
14923
+ const output = [];
14924
+ commandProcess.stdout.on('data', (stdout) => {
14925
+ output.push(stdout.toString());
14926
+ if (isVerbose) {
14927
+ console.info(stdout.toString());
14928
+ }
14903
14929
  });
14904
- (_c = options.onRetry) === null || _c === void 0 ? void 0 : _c.call(options, {
14905
- attempt,
14906
- retries: options.retries,
14907
- delayMs,
14908
- error,
14930
+ commandProcess.stderr.on('data', (stderr) => {
14931
+ output.push(stderr.toString());
14932
+ if (isVerbose && stderr.toString().trim()) {
14933
+ console.warn(stderr.toString());
14934
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
14935
+ }
14936
+ });
14937
+ const finishWithCode = (code) => {
14938
+ if (code !== 0) {
14939
+ if (crashOnError) {
14940
+ reject(new Error(output.join('\n').trim() ||
14941
+ `Command "${humanReadableCommand}" exited with code ${code}`));
14942
+ }
14943
+ else {
14944
+ if (isVerbose) {
14945
+ console.warn(`Command "${humanReadableCommand}" exited with code ${code}`);
14946
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
14947
+ }
14948
+ resolve(spaceTrim$1.spaceTrim(output.join('\n')));
14949
+ }
14950
+ }
14951
+ else {
14952
+ resolve(spaceTrim$1.spaceTrim(output.join('\n')));
14953
+ }
14954
+ };
14955
+ commandProcess.on('close', finishWithCode);
14956
+ commandProcess.on('exit', finishWithCode);
14957
+ commandProcess.on('disconnect', () => {
14958
+ // Note: Unexpected disconnection should always result in rejection
14959
+ reject(new Error(`Command "${humanReadableCommand}" disconnected`));
14960
+ });
14961
+ commandProcess.on('error', (error) => {
14962
+ if (crashOnError) {
14963
+ reject(new Error(`Command "${humanReadableCommand}" failed: \n${error.message}`));
14964
+ }
14965
+ else {
14966
+ if (isVerbose) {
14967
+ console.warn(error);
14968
+ // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
14969
+ }
14970
+ resolve(spaceTrim$1.spaceTrim(output.join('\n')));
14971
+ }
14909
14972
  });
14910
- await sleep(delayMs, options.signal);
14911
14973
  }
14912
- }
14913
- throw new Error('Retry loop exited unexpectedly.');
14974
+ catch (error) {
14975
+ // Note: Unexpected error in sync code should always result in rejection
14976
+ reject(error);
14977
+ }
14978
+ });
14914
14979
  }
14915
-
14916
- const DEFAULT_BROWSER_USER_DATA_DIR = path.join(os.tmpdir(), 'promptbook', 'browser', 'user-data');
14917
14980
  /**
14918
- * Default remote browser connect timeout in milliseconds.
14981
+ * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
14919
14982
  */
14920
- const DEFAULT_REMOTE_CONNECT_TIMEOUT_MS = 10000;
14983
+
14921
14984
  /**
14922
- * Default retry count for remote browser connection establishment.
14985
+ * Attempts to locate the specified application on a Linux system using the 'which' command.
14986
+ * Returns the path to the executable if found, or null otherwise.
14987
+ *
14988
+ * @private within the repository
14923
14989
  */
14924
- const DEFAULT_REMOTE_CONNECT_RETRIES = 2;
14990
+ async function locateAppOnLinux({ linuxWhich, }) {
14991
+ try {
14992
+ const result = await $execCommand({ crashOnError: true, command: `which ${linuxWhich}` });
14993
+ return result.trim();
14994
+ }
14995
+ catch (error) {
14996
+ assertsError(error);
14997
+ return null;
14998
+ }
14999
+ }
14925
15000
  /**
14926
- * Default initial retry delay for remote browser connection.
15001
+ * TODO: [🧠][♿] Maybe export through `@promptbook/node`
15002
+ * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
15003
+ */
15004
+
15005
+ /**
15006
+ * Checks if the file is executable
15007
+ *
15008
+ * @private within the repository
15009
+ */
15010
+ async function isExecutable(path, fs) {
15011
+ try {
15012
+ await fs.access(path, fs.constants.X_OK);
15013
+ return true;
15014
+ }
15015
+ catch (error) {
15016
+ return false;
15017
+ }
15018
+ }
15019
+ /**
15020
+ * Note: Not [~🟢~] because it is not directly dependent on `fs
15021
+ * TODO: [🖇] What about symlinks?
15022
+ */
15023
+
15024
+ // Note: Module `userhome` has no types available, so it is imported using `require`
15025
+ // @see https://stackoverflow.com/questions/37000981/how-to-import-node-module-in-typescript-without-type-definitions
15026
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
15027
+ const userhome = require('userhome');
15028
+ /**
15029
+ * Attempts to locate the specified application on a macOS system by checking standard application paths and using mdfind.
15030
+ * Returns the path to the executable if found, or null otherwise.
15031
+ *
15032
+ * @private within the repository
15033
+ */
15034
+ async function locateAppOnMacOs({ macOsName, }) {
15035
+ try {
15036
+ const toExec = `/Contents/MacOS/${macOsName}`;
15037
+ const regPath = `/Applications/${macOsName}.app` + toExec;
15038
+ const altPath = userhome(regPath.slice(1));
15039
+ if (await isExecutable(regPath, $provideFilesystemForNode())) {
15040
+ return regPath;
15041
+ }
15042
+ else if (await isExecutable(altPath, $provideFilesystemForNode())) {
15043
+ return altPath;
15044
+ }
15045
+ const result = await $execCommand({
15046
+ crashOnError: true,
15047
+ command: `mdfind 'kMDItemDisplayName == "${macOsName}" && kMDItemKind == Application'`,
15048
+ });
15049
+ return result.trim() + toExec;
15050
+ }
15051
+ catch (error) {
15052
+ assertsError(error);
15053
+ return null;
15054
+ }
15055
+ }
15056
+ /**
15057
+ * TODO: [🧠][♿] Maybe export through `@promptbook/node`
15058
+ * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
15059
+ */
15060
+
15061
+ /**
15062
+ * Attempts to locate the specified application on a Windows system by searching common installation directories.
15063
+ * Returns the path to the executable if found, or null otherwise.
15064
+ *
15065
+ * @private within the repository
15066
+ */
15067
+ async function locateAppOnWindows({ appName, windowsSuffix, }) {
15068
+ try {
15069
+ const prefixes = [
15070
+ process.env.LOCALAPPDATA,
15071
+ path.join(process.env.LOCALAPPDATA || '', 'Programs'),
15072
+ process.env.PROGRAMFILES,
15073
+ process.env['PROGRAMFILES(X86)'],
15074
+ ];
15075
+ for (const prefix of prefixes) {
15076
+ const path = prefix + windowsSuffix;
15077
+ if (await isExecutable(path, $provideFilesystemForNode())) {
15078
+ return path;
15079
+ }
15080
+ }
15081
+ throw new Error(`Can not locate app ${appName} on Windows.`);
15082
+ }
15083
+ catch (error) {
15084
+ assertsError(error);
15085
+ return null;
15086
+ }
15087
+ }
15088
+ /**
15089
+ * TODO: [🧠][♿] Maybe export through `@promptbook/node`
15090
+ * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
15091
+ */
15092
+
15093
+ /**
15094
+ * Locates an application on the system
15095
+ *
15096
+ * @private within the repository
15097
+ */
15098
+ function locateApp(options) {
15099
+ if (!$isRunningInNode()) {
15100
+ throw new EnvironmentMismatchError('Locating apps works only in Node.js environment');
15101
+ }
15102
+ const { appName, linuxWhich, windowsSuffix, macOsName } = options;
15103
+ if (process.platform === 'win32') {
15104
+ if (windowsSuffix) {
15105
+ return locateAppOnWindows({ appName, windowsSuffix });
15106
+ }
15107
+ else {
15108
+ throw new Error(`${appName} is not available on Windows.`);
15109
+ }
15110
+ }
15111
+ else if (process.platform === 'darwin') {
15112
+ if (macOsName) {
15113
+ return locateAppOnMacOs({ macOsName });
15114
+ }
15115
+ else {
15116
+ throw new Error(`${appName} is not available on macOS.`);
15117
+ }
15118
+ }
15119
+ else {
15120
+ if (linuxWhich) {
15121
+ return locateAppOnLinux({ linuxWhich });
15122
+ }
15123
+ else {
15124
+ throw new Error(`${appName} is not available on Linux.`);
15125
+ }
15126
+ }
15127
+ }
15128
+ /**
15129
+ * TODO: [🧠][♿] Maybe export through `@promptbook/node`
15130
+ * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
15131
+ */
15132
+
15133
+ /**
15134
+ * @@@
15135
+ *
15136
+ * @private within the repository
15137
+ */
15138
+ function locateChrome() {
15139
+ return locateApp({
15140
+ appName: 'Chrome',
15141
+ linuxWhich: 'google-chrome',
15142
+ windowsSuffix: '\\Google\\Chrome\\Application\\chrome.exe',
15143
+ macOsName: 'Google Chrome',
15144
+ });
15145
+ }
15146
+
15147
+ /**
15148
+ * Creates one standard abort error for cancelled retry loops.
15149
+ *
15150
+ * @private utility for Agents Server runtime retries
15151
+ */
15152
+ function createAbortError$1() {
15153
+ const error = new Error('Operation was aborted.');
15154
+ error.name = 'AbortError';
15155
+ return error;
15156
+ }
15157
+ /**
15158
+ * Throws when the supplied signal is already aborted.
15159
+ *
15160
+ * @private utility for Agents Server runtime retries
15161
+ */
15162
+ function assertNotAborted(signal) {
15163
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
15164
+ throw createAbortError$1();
15165
+ }
15166
+ }
15167
+ /**
15168
+ * Waits for a duration while respecting cancellation.
15169
+ *
15170
+ * @private utility for Agents Server runtime retries
15171
+ */
15172
+ async function sleepWithAbort(delayMs, signal) {
15173
+ if (delayMs <= 0) {
15174
+ assertNotAborted(signal);
15175
+ return;
15176
+ }
15177
+ await new Promise((resolve, reject) => {
15178
+ const timeout = setTimeout(() => {
15179
+ signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
15180
+ resolve();
15181
+ }, delayMs);
15182
+ const onAbort = () => {
15183
+ clearTimeout(timeout);
15184
+ signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
15185
+ reject(createAbortError$1());
15186
+ };
15187
+ signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', onAbort, { once: true });
15188
+ });
15189
+ }
15190
+ /**
15191
+ * Resolves the retry wait duration for one failed attempt.
15192
+ *
15193
+ * @private utility for Agents Server runtime retries
15194
+ */
15195
+ function resolveBackoffDelayMs(options) {
15196
+ const exponentialDelay = options.initialDelayMs * Math.pow(options.backoffFactor, Math.max(0, options.attempt - 1));
15197
+ const boundedDelay = Math.min(exponentialDelay, options.maxDelayMs);
15198
+ const jitterDelay = boundedDelay * options.jitterRatio * Math.max(0, options.random());
15199
+ return Math.max(0, Math.round(boundedDelay + jitterDelay));
15200
+ }
15201
+ /**
15202
+ * Retries one async operation with exponential backoff and jitter.
15203
+ *
15204
+ * @private utility for Agents Server runtime retries
15205
+ */
15206
+ async function retryWithBackoff(operation, options) {
15207
+ var _a, _b, _c;
15208
+ const startedAt = Date.now();
15209
+ const totalAttempts = Math.max(1, options.retries + 1);
15210
+ const random = (_a = options.random) !== null && _a !== void 0 ? _a : Math.random;
15211
+ const sleep = (_b = options.sleep) !== null && _b !== void 0 ? _b : sleepWithAbort;
15212
+ for (let attempt = 1; attempt <= totalAttempts; attempt++) {
15213
+ assertNotAborted(options.signal);
15214
+ try {
15215
+ const value = await operation(attempt);
15216
+ return {
15217
+ value,
15218
+ attempts: attempt,
15219
+ durationMs: Date.now() - startedAt,
15220
+ };
15221
+ }
15222
+ catch (error) {
15223
+ const isLastAttempt = attempt >= totalAttempts;
15224
+ const isRetryable = options.shouldRetry ? options.shouldRetry(error, attempt) : true;
15225
+ if (isLastAttempt || !isRetryable) {
15226
+ throw error;
15227
+ }
15228
+ const delayMs = resolveBackoffDelayMs({
15229
+ attempt,
15230
+ initialDelayMs: options.initialDelayMs,
15231
+ maxDelayMs: options.maxDelayMs,
15232
+ backoffFactor: options.backoffFactor,
15233
+ jitterRatio: options.jitterRatio,
15234
+ random,
15235
+ });
15236
+ (_c = options.onRetry) === null || _c === void 0 ? void 0 : _c.call(options, {
15237
+ attempt,
15238
+ retries: options.retries,
15239
+ delayMs,
15240
+ error,
15241
+ });
15242
+ await sleep(delayMs, options.signal);
15243
+ }
15244
+ }
15245
+ throw new Error('Retry loop exited unexpectedly.');
15246
+ }
15247
+
15248
+ const DEFAULT_BROWSER_USER_DATA_DIR = path.join(os.tmpdir(), 'promptbook', 'browser', 'user-data');
15249
+ /**
15250
+ * Default remote browser connect timeout in milliseconds.
15251
+ */
15252
+ const DEFAULT_REMOTE_CONNECT_TIMEOUT_MS = 10000;
15253
+ /**
15254
+ * Default retry count for remote browser connection establishment.
15255
+ */
15256
+ const DEFAULT_REMOTE_CONNECT_RETRIES = 2;
15257
+ /**
15258
+ * Default initial retry delay for remote browser connection.
14927
15259
  */
14928
15260
  const DEFAULT_REMOTE_CONNECT_BACKOFF_INITIAL_MS = 250;
14929
15261
  /**
@@ -15183,7 +15515,7 @@
15183
15515
  args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
15184
15516
  };
15185
15517
  try {
15186
- const chromePath = await locateApp$1.locateChrome();
15518
+ const chromePath = await locateChrome();
15187
15519
  launchOptions.executablePath = chromePath;
15188
15520
  }
15189
15521
  catch (error) {
@@ -21366,27 +21698,154 @@
21366
21698
  */
21367
21699
 
21368
21700
  /**
21369
- * Lightweight email token matcher used for `USE EMAIL` first-line parsing.
21701
+ * Base Google Calendar API URL.
21370
21702
  *
21371
- * @private internal USE EMAIL constant
21703
+ * @private constant of callGoogleCalendarApi
21372
21704
  */
21373
- const EMAIL_TOKEN_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/;
21705
+ const GOOGLE_CALENDAR_API_BASE_URL = 'https://www.googleapis.com/calendar/v3';
21374
21706
  /**
21375
- * Parses `USE EMAIL` commitment content into optional sender email + additional instructions.
21707
+ * Runs one Google Calendar API request and parses JSON response payload.
21376
21708
  *
21377
- * Examples:
21378
- * - `agent@example.com`
21379
- * - `agent@example.com Keep emails concise`
21380
- * - `Keep emails concise`
21709
+ * @private function of UseCalendarCommitmentDefinition
21710
+ */
21711
+ async function callGoogleCalendarApi(accessToken, options) {
21712
+ const url = new URL(options.path, GOOGLE_CALENDAR_API_BASE_URL);
21713
+ if (options.query) {
21714
+ for (const [key, value] of Object.entries(options.query)) {
21715
+ if (value && value.trim()) {
21716
+ url.searchParams.set(key, value);
21717
+ }
21718
+ }
21719
+ }
21720
+ const response = await fetch(url.toString(), {
21721
+ method: options.method,
21722
+ headers: {
21723
+ Authorization: `Bearer ${accessToken}`,
21724
+ Accept: 'application/json',
21725
+ 'Content-Type': 'application/json',
21726
+ },
21727
+ body: options.body ? JSON.stringify(options.body) : undefined,
21728
+ });
21729
+ const textPayload = await response.text();
21730
+ const parsedPayload = tryParseJson$2(textPayload);
21731
+ if (options.allowNotFound && response.status === 404) {
21732
+ return null;
21733
+ }
21734
+ if (!response.ok) {
21735
+ throw new Error(spaceTrim$1.spaceTrim(`
21736
+ Google Calendar API request failed (${response.status} ${response.statusText}):
21737
+ ${extractGoogleCalendarApiErrorMessage(parsedPayload, textPayload)}
21738
+ `));
21739
+ }
21740
+ return parsedPayload;
21741
+ }
21742
+ /**
21743
+ * Parses raw text into JSON when possible.
21381
21744
  *
21382
- * @private internal utility of USE EMAIL commitment
21745
+ * @private function of callGoogleCalendarApi
21383
21746
  */
21384
- function parseUseEmailCommitmentContent(content) {
21747
+ function tryParseJson$2(rawText) {
21748
+ if (!rawText.trim()) {
21749
+ return {};
21750
+ }
21751
+ try {
21752
+ return JSON.parse(rawText);
21753
+ }
21754
+ catch (_a) {
21755
+ return rawText;
21756
+ }
21757
+ }
21758
+ /**
21759
+ * Extracts a user-friendly Google Calendar API error message.
21760
+ *
21761
+ * @private function of callGoogleCalendarApi
21762
+ */
21763
+ function extractGoogleCalendarApiErrorMessage(parsedPayload, fallbackText) {
21764
+ if (parsedPayload && typeof parsedPayload === 'object') {
21765
+ const payload = parsedPayload;
21766
+ const errorPayload = payload.error;
21767
+ if (errorPayload && typeof errorPayload === 'object') {
21768
+ const normalizedErrorPayload = errorPayload;
21769
+ const message = typeof normalizedErrorPayload.message === 'string' ? normalizedErrorPayload.message : '';
21770
+ const errors = Array.isArray(normalizedErrorPayload.errors) ? normalizedErrorPayload.errors : [];
21771
+ const flattenedErrors = errors
21772
+ .map((errorEntry) => {
21773
+ if (!errorEntry || typeof errorEntry !== 'object') {
21774
+ return '';
21775
+ }
21776
+ const normalizedErrorEntry = errorEntry;
21777
+ const detailMessage = typeof normalizedErrorEntry.message === 'string' ? normalizedErrorEntry.message : '';
21778
+ const reason = typeof normalizedErrorEntry.reason === 'string' ? normalizedErrorEntry.reason : '';
21779
+ return [detailMessage, reason].filter(Boolean).join(' | ');
21780
+ })
21781
+ .filter(Boolean);
21782
+ if (message || flattenedErrors.length > 0) {
21783
+ return [message, ...flattenedErrors].filter(Boolean).join(' | ');
21784
+ }
21785
+ }
21786
+ }
21787
+ return fallbackText || 'Unknown Google Calendar API error';
21788
+ }
21789
+
21790
+ /**
21791
+ * Hostnames accepted for Google Calendar references.
21792
+ *
21793
+ * @private internal USE CALENDAR constant
21794
+ */
21795
+ const GOOGLE_CALENDAR_HOSTNAMES = new Set(['calendar.google.com', 'www.calendar.google.com']);
21796
+ /**
21797
+ * Default Google Calendar OAuth scopes when commitment content does not list any.
21798
+ *
21799
+ * @private internal USE CALENDAR constant
21800
+ */
21801
+ const DEFAULT_GOOGLE_CALENDAR_SCOPES = ['https://www.googleapis.com/auth/calendar'];
21802
+ /**
21803
+ * Parses one Google Calendar URL/reference into canonical details.
21804
+ *
21805
+ * Supported input forms:
21806
+ * - `https://calendar.google.com/...`
21807
+ * - `calendar.google.com/...`
21808
+ *
21809
+ * @private internal utility of USE CALENDAR commitment
21810
+ */
21811
+ function parseGoogleCalendarReference(rawReference) {
21812
+ const trimmedReference = rawReference.trim();
21813
+ if (!trimmedReference) {
21814
+ return null;
21815
+ }
21816
+ const normalizedReference = trimmedReference.startsWith('calendar.google.com/')
21817
+ ? `https://${trimmedReference}`
21818
+ : trimmedReference;
21819
+ let parsedUrl;
21820
+ try {
21821
+ parsedUrl = new URL(normalizedReference);
21822
+ }
21823
+ catch (_a) {
21824
+ return null;
21825
+ }
21826
+ if (!GOOGLE_CALENDAR_HOSTNAMES.has(parsedUrl.hostname.toLowerCase())) {
21827
+ return null;
21828
+ }
21829
+ parsedUrl.protocol = 'https:';
21830
+ parsedUrl.hash = '';
21831
+ return {
21832
+ provider: 'google',
21833
+ url: parsedUrl.toString(),
21834
+ calendarId: parseGoogleCalendarIdFromUrl(parsedUrl) || 'primary',
21835
+ scopes: [...DEFAULT_GOOGLE_CALENDAR_SCOPES],
21836
+ };
21837
+ }
21838
+ /**
21839
+ * Parses `USE CALENDAR` commitment content into calendar reference + optional instructions.
21840
+ *
21841
+ * @private internal utility of USE CALENDAR commitment
21842
+ */
21843
+ function parseUseCalendarCommitmentContent(content) {
21385
21844
  const trimmedContent = spaceTrim$1.spaceTrim(content);
21386
21845
  if (!trimmedContent) {
21387
21846
  return {
21388
- senderEmail: null,
21389
- senderEmailRaw: null,
21847
+ calendar: null,
21848
+ calendarUrlRaw: null,
21390
21849
  instructions: '',
21391
21850
  };
21392
21851
  }
@@ -21396,718 +21855,1505 @@
21396
21855
  .filter(Boolean);
21397
21856
  if (lines.length === 0) {
21398
21857
  return {
21399
- senderEmail: null,
21400
- senderEmailRaw: null,
21858
+ calendar: null,
21859
+ calendarUrlRaw: null,
21401
21860
  instructions: '',
21402
21861
  };
21403
21862
  }
21863
+ let calendarReferenceRaw = null;
21864
+ let calendarReference = null;
21404
21865
  const firstLine = lines[0] || '';
21405
- const senderMatch = firstLine.match(EMAIL_TOKEN_PATTERN);
21406
- const senderEmailRaw = (senderMatch === null || senderMatch === void 0 ? void 0 : senderMatch[0]) || null;
21407
- const senderEmail = senderEmailRaw && isValidEmail(senderEmailRaw) ? senderEmailRaw : null;
21408
- let firstLineWithoutSender = firstLine;
21409
- if (senderEmailRaw) {
21410
- const matchIndex = firstLine.indexOf(senderEmailRaw);
21411
- const prefix = firstLine.slice(0, matchIndex).trim();
21412
- const suffix = firstLine.slice(matchIndex + senderEmailRaw.length).trim();
21413
- firstLineWithoutSender = [prefix, suffix].filter(Boolean).join(' ').trim();
21866
+ const firstLineTokens = firstLine.split(/\s+/).filter(Boolean);
21867
+ for (const token of firstLineTokens) {
21868
+ const cleanedToken = token.replace(/[),.;:!?]+$/g, '');
21869
+ const parsedReference = parseGoogleCalendarReference(cleanedToken);
21870
+ if (!parsedReference) {
21871
+ continue;
21872
+ }
21873
+ calendarReferenceRaw = cleanedToken;
21874
+ calendarReference = parsedReference;
21875
+ break;
21414
21876
  }
21415
- const instructionLines = [firstLineWithoutSender, ...lines.slice(1)].filter(Boolean);
21416
- const instructions = instructionLines.join('\n').trim();
21877
+ if (!calendarReference) {
21878
+ const firstUrl = findFirstUrlToken(trimmedContent);
21879
+ if (firstUrl) {
21880
+ calendarReferenceRaw = firstUrl;
21881
+ calendarReference = parseGoogleCalendarReference(firstUrl);
21882
+ }
21883
+ }
21884
+ const scopes = extractGoogleCalendarScopes(trimmedContent);
21885
+ if (calendarReference) {
21886
+ calendarReference = {
21887
+ ...calendarReference,
21888
+ scopes: scopes.length > 0 ? scopes : [...DEFAULT_GOOGLE_CALENDAR_SCOPES],
21889
+ tokenRef: extractTokenRef(trimmedContent),
21890
+ };
21891
+ }
21892
+ const instructionLines = [...lines];
21893
+ if (instructionLines.length > 0 && calendarReferenceRaw) {
21894
+ instructionLines[0] = removeTokenFromLine(instructionLines[0] || '', calendarReferenceRaw);
21895
+ }
21896
+ const filteredInstructionLines = instructionLines.filter((line) => !/^\s*SCOPES?\s+/i.test(line) && !line.trim().startsWith('https://www.googleapis.com/auth/'));
21417
21897
  return {
21418
- senderEmail,
21419
- senderEmailRaw,
21420
- instructions,
21898
+ calendar: calendarReference,
21899
+ calendarUrlRaw: calendarReferenceRaw,
21900
+ instructions: filteredInstructionLines.join('\n').trim(),
21421
21901
  };
21422
21902
  }
21423
-
21424
21903
  /**
21425
- * Client-side safe wrapper for sending emails.
21904
+ * Attempts to resolve one concrete Google Calendar id from URL.
21426
21905
  *
21427
- * This function proxies requests to the Agents Server API endpoint for email queuing,
21428
- * making it safe to use in browser environments.
21906
+ * @private internal utility of USE CALENDAR commitment
21907
+ */
21908
+ function parseGoogleCalendarIdFromUrl(url) {
21909
+ const rawCalendarId = url.searchParams.get('cid') || url.searchParams.get('src') || url.searchParams.get('calendarId');
21910
+ if (!rawCalendarId) {
21911
+ return null;
21912
+ }
21913
+ const decodedCalendarId = decodeURIComponent(rawCalendarId).trim();
21914
+ return decodedCalendarId || null;
21915
+ }
21916
+ /**
21917
+ * Finds the first URL-looking token in text.
21429
21918
  *
21430
- * @param args Email payload forwarded to the server-side `send_email` tool
21431
- * @param agentsServerUrl The base URL of the agents server (defaults to current origin)
21432
- * @returns Result string from the server-side send_email tool
21919
+ * @private utility of USE CALENDAR commitment
21920
+ */
21921
+ function findFirstUrlToken(text) {
21922
+ const match = text.match(/https?:\/\/[^\s)]+/i);
21923
+ return match ? match[0] || null : null;
21924
+ }
21925
+ /**
21926
+ * Extracts Google Calendar OAuth scopes declared in commitment text.
21433
21927
  *
21434
- * @private internal utility for USE EMAIL commitment
21928
+ * @private utility of USE CALENDAR commitment
21435
21929
  */
21436
- async function sendEmailViaBrowser(args, agentsServerUrl) {
21437
- try {
21438
- const baseUrl = agentsServerUrl || (typeof window !== 'undefined' ? window.location.origin : '');
21439
- if (!baseUrl) {
21440
- throw new Error('Agents server URL is required in non-browser environments');
21441
- }
21442
- const apiUrl = new URL('/api/send-email', baseUrl);
21443
- const response = await fetch(apiUrl.toString(), {
21444
- method: 'POST',
21445
- headers: {
21446
- 'Content-Type': 'application/json',
21447
- },
21448
- body: JSON.stringify(args),
21449
- });
21450
- if (!response.ok) {
21451
- const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
21452
- throw new Error(`Failed to send email: ${errorData.error || response.statusText}`);
21453
- }
21454
- const data = await response.json();
21455
- if (!data.success) {
21456
- throw new Error(`Email sending failed: ${data.error || 'Unknown error'}`);
21457
- }
21458
- return typeof data.result === 'string' ? data.result : JSON.stringify(data.result);
21459
- }
21460
- catch (error) {
21461
- const errorMessage = error instanceof Error ? error.message : String(error);
21462
- throw new Error(`Error sending email via browser: ${errorMessage}`);
21930
+ function extractGoogleCalendarScopes(content) {
21931
+ const scopesFromUrls = content.match(/https:\/\/www\.googleapis\.com\/auth\/[A-Za-z0-9._/-]+/gim) || [];
21932
+ const scopesFromKeywordLines = content
21933
+ .split(/\r?\n/)
21934
+ .map((line) => line.trim())
21935
+ .filter((line) => /^\s*SCOPES?\s+/i.test(line))
21936
+ .flatMap((line) => line
21937
+ .replace(/^\s*SCOPES?\s+/i, '')
21938
+ .split(/[,\s]+/)
21939
+ .map((scope) => scope.trim())
21940
+ .filter(Boolean))
21941
+ .filter((scope) => scope.startsWith('https://www.googleapis.com/auth/'));
21942
+ const uniqueScopes = new Set();
21943
+ for (const scope of [...scopesFromUrls, ...scopesFromKeywordLines]) {
21944
+ uniqueScopes.add(scope);
21463
21945
  }
21946
+ return [...uniqueScopes];
21464
21947
  }
21465
-
21466
21948
  /**
21467
- * Tool name used by USE EMAIL.
21949
+ * Extracts optional token reference marker from commitment text.
21468
21950
  *
21469
- * @private internal USE EMAIL constant
21951
+ * @private utility of USE CALENDAR commitment
21470
21952
  */
21471
- const SEND_EMAIL_TOOL_NAME = 'send_email';
21953
+ function extractTokenRef(content) {
21954
+ var _a;
21955
+ const tokenRefMatch = content.match(/@[\w.-]+/);
21956
+ const tokenRef = (_a = tokenRefMatch === null || tokenRefMatch === void 0 ? void 0 : tokenRefMatch[0]) === null || _a === void 0 ? void 0 : _a.trim();
21957
+ return tokenRef || undefined;
21958
+ }
21472
21959
  /**
21473
- * Wallet service used for SMTP credentials required by USE EMAIL.
21960
+ * Removes one specific token from one instruction line.
21474
21961
  *
21475
- * @private internal USE EMAIL constant
21962
+ * @private utility of USE CALENDAR commitment
21476
21963
  */
21477
- const USE_EMAIL_SMTP_WALLET_SERVICE = 'smtp';
21964
+ function removeTokenFromLine(line, token) {
21965
+ const tokens = line.split(/\s+/).filter(Boolean);
21966
+ const filteredTokens = tokens.filter((lineToken) => lineToken.replace(/[),.;:!?]+$/g, '') !== token);
21967
+ return filteredTokens.join(' ').trim();
21968
+ }
21478
21969
  /**
21479
- * Wallet key used for SMTP credentials required by USE EMAIL.
21970
+ * Note: [💞] Ignore a discrepancy between file name and entity name
21971
+ */
21972
+
21973
+ /**
21974
+ * Wallet metadata used by USE CALENDAR when resolving Google Calendar credentials.
21480
21975
  *
21481
- * @private internal USE EMAIL constant
21976
+ * @private constant of UseCalendarCommitmentDefinition
21482
21977
  */
21483
- const USE_EMAIL_SMTP_WALLET_KEY = 'use-email-smtp-credentials';
21978
+ const UseCalendarWallet = {
21979
+ service: 'google_calendar',
21980
+ key: 'use-calendar-google-token',
21981
+ };
21982
+
21484
21983
  /**
21485
- * USE EMAIL commitment definition.
21984
+ * Internal error used to signal missing wallet credentials.
21486
21985
  *
21487
- * The `USE EMAIL` commitment enables outbound email sending through the `send_email` tool.
21986
+ * @private class of resolveUseCalendarToolRuntimeOrWalletCredentialResult
21987
+ */
21988
+ class CalendarWalletCredentialRequiredError extends Error {
21989
+ constructor(options) {
21990
+ super('Google Calendar token is missing in wallet. Request it from user and store as ACCESS_TOKEN (service google_calendar, key use-calendar-google-token).');
21991
+ this.name = 'CalendarWalletCredentialRequiredError';
21992
+ this.service = UseCalendarWallet.service;
21993
+ this.key = UseCalendarWallet.key;
21994
+ this.calendarReference = options.calendarReference;
21995
+ }
21996
+ }
21997
+ /**
21998
+ * Resolves calendar runtime or returns a wallet-credential-required payload when missing.
21488
21999
  *
21489
- * @private [🪔] Maybe export the commitments through some package
22000
+ * @private function of UseCalendarCommitmentDefinition
21490
22001
  */
21491
- class UseEmailCommitmentDefinition extends BaseCommitmentDefinition {
21492
- constructor() {
21493
- super('USE EMAIL', ['EMAIL', 'MAIL']);
22002
+ function resolveUseCalendarToolRuntimeOrWalletCredentialResult(args) {
22003
+ try {
22004
+ return resolveUseCalendarToolRuntime(args);
21494
22005
  }
21495
- get requiresContent() {
21496
- return false;
22006
+ catch (error) {
22007
+ if (error instanceof CalendarWalletCredentialRequiredError) {
22008
+ return {
22009
+ walletResult: JSON.stringify(createCalendarWalletCredentialRequiredResult(error)),
22010
+ };
22011
+ }
22012
+ throw error;
21497
22013
  }
21498
- /**
21499
- * Short one-line description of USE EMAIL.
21500
- */
21501
- get description() {
21502
- return 'Enable outbound email sending through a wallet-backed SMTP configuration.';
22014
+ }
22015
+ /**
22016
+ * Resolves runtime calendar + token for a USE CALENDAR tool call.
22017
+ *
22018
+ * @private function of resolveUseCalendarToolRuntimeOrWalletCredentialResult
22019
+ */
22020
+ function resolveUseCalendarToolRuntime(args) {
22021
+ var _a, _b;
22022
+ const runtimeContext = (readToolRuntimeContextFromToolArgs(args) ||
22023
+ {});
22024
+ const configuredCalendars = normalizeConfiguredCalendars$1((_a = runtimeContext.calendars) === null || _a === void 0 ? void 0 : _a.connections);
22025
+ const calendarArgument = normalizeOptionalText$1(args.calendarUrl);
22026
+ let calendarReference = null;
22027
+ if (calendarArgument) {
22028
+ calendarReference = parseGoogleCalendarReference(calendarArgument);
22029
+ if (!calendarReference) {
22030
+ throw new Error(`Calendar URL "${calendarArgument}" is invalid.`);
22031
+ }
21503
22032
  }
21504
- /**
21505
- * Icon for this commitment.
21506
- */
21507
- get icon() {
21508
- return '📧';
22033
+ else if (configuredCalendars.length === 1) {
22034
+ calendarReference = configuredCalendars[0] || null;
21509
22035
  }
21510
- /**
21511
- * Markdown documentation for USE EMAIL commitment.
21512
- */
21513
- get documentation() {
21514
- return spaceTrim$1.spaceTrim(`
21515
- # USE EMAIL
21516
-
21517
- Enables the agent to send outbound emails through SMTP.
21518
-
21519
- ## Key aspects
21520
-
21521
- - The agent sends email via the \`send_email\` tool.
21522
- - SMTP credentials are expected from wallet records (\`ACCESS_TOKEN\`, service \`smtp\`, key \`use-email-smtp-credentials\`).
21523
- - Commitment content can optionally begin with a default sender email address:
21524
- - \`USE EMAIL agent@example.com\`
21525
- - Remaining commitment content is treated as optional email-writing instructions.
21526
-
21527
- ## Examples
21528
-
21529
- \`\`\`book
21530
- Writing Agent
21531
- USE EMAIL agent@example.com
21532
- RULE Write emails to customers according to the instructions from user.
21533
- \`\`\`
21534
-
21535
- \`\`\`book
21536
- Formal Email Assistant
21537
- USE EMAIL agent@example.com Keep emails concise and formal.
21538
- \`\`\`
21539
- `);
22036
+ else if (configuredCalendars.length > 1) {
22037
+ throw new Error('Calendar is ambiguous. Provide "calendarUrl" argument with one calendar from USE CALENDAR commitments.');
21540
22038
  }
21541
- applyToAgentModelRequirements(requirements, content) {
21542
- const parsedCommitment = parseUseEmailCommitmentContent(content);
21543
- const extraInstructions = formatOptionalInstructionBlock('Email instructions', parsedCommitment.instructions);
21544
- const senderInstruction = parsedCommitment.senderEmail
21545
- ? `- Default sender address from commitment: "${parsedCommitment.senderEmail}".`
21546
- : '';
21547
- const updatedTools = addUseEmailTools(requirements.tools || []);
21548
- return this.appendToSystemMessage({
21549
- ...requirements,
21550
- tools: updatedTools,
21551
- _metadata: {
21552
- ...requirements._metadata,
21553
- useEmail: true,
21554
- ...(parsedCommitment.senderEmail ? { useEmailSender: parsedCommitment.senderEmail } : {}),
21555
- },
21556
- }, spaceTrim$1.spaceTrim((block) => `
21557
- Email tool:
21558
- - Use "${SEND_EMAIL_TOOL_NAME}" to send outbound emails.
21559
- - Prefer \`message\` argument compatible with Promptbook \`Message\` type.
21560
- - Include subject in \`message.metadata.subject\` (or use legacy \`subject\` argument).
21561
- - USE EMAIL credentials are read from wallet records (ACCESS_TOKEN, service "${USE_EMAIL_SMTP_WALLET_SERVICE}", key "${USE_EMAIL_SMTP_WALLET_KEY}").
21562
- - Wallet secret must contain SMTP credentials in JSON format with fields \`host\`, \`port\`, \`secure\`, \`username\`, \`password\`.
21563
- - If credentials are missing, ask user to add wallet credentials.
21564
- ${block(senderInstruction)}
21565
- ${block(extraInstructions)}
21566
- `));
22039
+ else {
22040
+ throw new Error('Calendar is required. Provide "calendarUrl" argument in the tool call.');
21567
22041
  }
21568
- /**
21569
- * Gets human-readable titles for tool functions provided by this commitment.
21570
- */
21571
- getToolTitles() {
21572
- return {
21573
- [SEND_EMAIL_TOOL_NAME]: 'Send email',
21574
- };
22042
+ if (!calendarReference) {
22043
+ throw new Error('Calendar is required but was not resolved.');
21575
22044
  }
21576
- /**
21577
- * Gets the browser-safe `send_email` implementation.
21578
- *
21579
- * Node.js runtime overrides this via `getAllCommitmentsToolFunctionsForNode`.
21580
- */
21581
- getToolFunctions() {
21582
- return {
21583
- async [SEND_EMAIL_TOOL_NAME](args) {
21584
- return sendEmailViaBrowser(args);
21585
- },
21586
- };
22045
+ const accessToken = normalizeOptionalText$1((_b = runtimeContext.calendars) === null || _b === void 0 ? void 0 : _b.googleAccessToken) || '';
22046
+ if (!accessToken) {
22047
+ throw new CalendarWalletCredentialRequiredError({
22048
+ calendarReference,
22049
+ });
22050
+ }
22051
+ if (configuredCalendars.length > 0) {
22052
+ const allowedCalendarUrls = new Set(configuredCalendars.map((configuredCalendar) => configuredCalendar.url));
22053
+ if (!allowedCalendarUrls.has(calendarReference.url)) {
22054
+ throw new Error(`Calendar "${calendarReference.url}" is not configured by USE CALENDAR for this agent.`);
22055
+ }
21587
22056
  }
22057
+ return {
22058
+ calendarReference,
22059
+ accessToken,
22060
+ };
21588
22061
  }
21589
22062
  /**
21590
- * Adds USE EMAIL tool definition while keeping already registered tools untouched.
22063
+ * Normalizes optional calendar list from runtime context.
21591
22064
  *
21592
- * @private utility of USE EMAIL commitment
22065
+ * @private function of resolveUseCalendarToolRuntimeOrWalletCredentialResult
21593
22066
  */
21594
- function addUseEmailTools(existingTools) {
21595
- if (existingTools.some((tool) => tool.name === SEND_EMAIL_TOOL_NAME)) {
21596
- return [...existingTools];
22067
+ function normalizeConfiguredCalendars$1(rawCalendars) {
22068
+ if (!Array.isArray(rawCalendars)) {
22069
+ return [];
21597
22070
  }
21598
- return [
21599
- ...existingTools,
21600
- {
21601
- name: SEND_EMAIL_TOOL_NAME,
21602
- description: 'Send an outbound email through configured SMTP credentials. Prefer providing Message-like payload in `message`.',
21603
- parameters: {
21604
- type: 'object',
21605
- properties: {
21606
- message: {
21607
- type: 'object',
21608
- description: 'Preferred input payload compatible with Promptbook Message type. Use metadata.subject for subject line.',
21609
- },
21610
- to: {
21611
- type: 'string',
21612
- description: 'Legacy alias for recipients (use comma-separated emails or JSON array encoded as string).',
21613
- },
21614
- cc: {
21615
- type: 'string',
21616
- description: 'Optional CC recipients (use comma-separated emails or JSON array encoded as string).',
21617
- },
21618
- subject: {
21619
- type: 'string',
21620
- description: 'Legacy alias for subject.',
21621
- },
22071
+ const calendars = [];
22072
+ const knownCalendars = new Set();
22073
+ for (const rawCalendar of rawCalendars) {
22074
+ if (!rawCalendar || typeof rawCalendar !== 'object') {
22075
+ continue;
22076
+ }
22077
+ const calendar = rawCalendar;
22078
+ const rawUrl = normalizeOptionalText$1(calendar.url);
22079
+ if (!rawUrl) {
22080
+ continue;
22081
+ }
22082
+ const parsedReference = parseGoogleCalendarReference(rawUrl);
22083
+ if (!parsedReference) {
22084
+ continue;
22085
+ }
22086
+ const key = `${parsedReference.provider}|${parsedReference.url}`;
22087
+ if (knownCalendars.has(key)) {
22088
+ continue;
22089
+ }
22090
+ knownCalendars.add(key);
22091
+ const scopes = Array.isArray(calendar.scopes)
22092
+ ? calendar.scopes
22093
+ .filter((scope) => typeof scope === 'string')
22094
+ .map((scope) => scope.trim())
22095
+ .filter(Boolean)
22096
+ : [];
22097
+ calendars.push({
22098
+ ...parsedReference,
22099
+ scopes: scopes.length > 0 ? scopes : parsedReference.scopes,
22100
+ });
22101
+ }
22102
+ return calendars;
22103
+ }
22104
+ /**
22105
+ * Converts missing-wallet errors into structured tool result payloads.
22106
+ *
22107
+ * @private function of resolveUseCalendarToolRuntimeOrWalletCredentialResult
22108
+ */
22109
+ function createCalendarWalletCredentialRequiredResult(error) {
22110
+ return {
22111
+ action: 'calendar-auth',
22112
+ status: 'wallet-credential-required',
22113
+ recordType: 'ACCESS_TOKEN',
22114
+ service: error.service,
22115
+ key: error.key,
22116
+ isUserScoped: false,
22117
+ isGlobal: false,
22118
+ provider: 'google',
22119
+ calendarUrl: error.calendarReference.url,
22120
+ scopes: error.calendarReference.scopes,
22121
+ message: error.message,
22122
+ };
22123
+ }
22124
+ /**
22125
+ * Normalizes unknown text input to trimmed non-empty string.
22126
+ *
22127
+ * @private function of resolveUseCalendarToolRuntimeOrWalletCredentialResult
22128
+ */
22129
+ function normalizeOptionalText$1(value) {
22130
+ if (typeof value !== 'string') {
22131
+ return undefined;
22132
+ }
22133
+ const trimmedValue = value.trim();
22134
+ return trimmedValue || undefined;
22135
+ }
22136
+
22137
+ /**
22138
+ * Names of tools used by the USE CALENDAR commitment.
22139
+ *
22140
+ * @private constant of UseCalendarCommitmentDefinition
22141
+ */
22142
+ const UseCalendarToolNames = {
22143
+ listEvents: 'calendar_list_events',
22144
+ getEvent: 'calendar_get_event',
22145
+ createEvent: 'calendar_create_event',
22146
+ updateEvent: 'calendar_update_event',
22147
+ deleteEvent: 'calendar_delete_event',
22148
+ inviteGuests: 'calendar_invite_guests',
22149
+ };
22150
+
22151
+ /**
22152
+ * Gets Google Calendar tool function implementations.
22153
+ *
22154
+ * @private function of UseCalendarCommitmentDefinition
22155
+ */
22156
+ function createUseCalendarToolFunctions() {
22157
+ return {
22158
+ async [UseCalendarToolNames.listEvents](args) {
22159
+ return withUseCalendarRuntime(args, async ({ calendarReference, accessToken }) => {
22160
+ const query = {};
22161
+ if (normalizeOptionalText(args.timeMin)) {
22162
+ query.timeMin = args.timeMin.trim();
22163
+ }
22164
+ if (normalizeOptionalText(args.timeMax)) {
22165
+ query.timeMax = args.timeMax.trim();
22166
+ }
22167
+ if (normalizeOptionalText(args.query)) {
22168
+ query.q = args.query.trim();
22169
+ }
22170
+ if (typeof args.maxResults === 'number' && Number.isFinite(args.maxResults) && args.maxResults > 0) {
22171
+ query.maxResults = String(Math.floor(args.maxResults));
22172
+ }
22173
+ if (args.singleEvents !== undefined) {
22174
+ query.singleEvents = args.singleEvents ? 'true' : 'false';
22175
+ }
22176
+ if (args.orderBy === 'startTime' || args.orderBy === 'updated') {
22177
+ query.orderBy = args.orderBy;
22178
+ }
22179
+ if (normalizeOptionalText(args.timeZone)) {
22180
+ query.timeZone = args.timeZone.trim();
22181
+ }
22182
+ const payload = await callGoogleCalendarApi(accessToken, {
22183
+ method: 'GET',
22184
+ path: `/calendars/${encodeGoogleCalendarId(calendarReference.calendarId)}/events`,
22185
+ query,
22186
+ });
22187
+ const events = ((payload === null || payload === void 0 ? void 0 : payload.items) || []).map((event) => mapGoogleCalendarEvent(event));
22188
+ return JSON.stringify({
22189
+ provider: calendarReference.provider,
22190
+ calendarUrl: calendarReference.url,
22191
+ calendarId: calendarReference.calendarId,
22192
+ events,
22193
+ nextPageToken: (payload === null || payload === void 0 ? void 0 : payload.nextPageToken) || null,
22194
+ nextSyncToken: (payload === null || payload === void 0 ? void 0 : payload.nextSyncToken) || null,
22195
+ });
22196
+ });
22197
+ },
22198
+ async [UseCalendarToolNames.getEvent](args) {
22199
+ return withUseCalendarRuntime(args, async ({ calendarReference, accessToken }) => {
22200
+ const eventId = normalizeRequiredText(args.eventId, 'eventId');
22201
+ const payload = await callGoogleCalendarApi(accessToken, {
22202
+ method: 'GET',
22203
+ path: `/calendars/${encodeGoogleCalendarId(calendarReference.calendarId)}/events/${encodeURIComponent(eventId)}`,
22204
+ });
22205
+ return JSON.stringify({
22206
+ provider: calendarReference.provider,
22207
+ calendarUrl: calendarReference.url,
22208
+ calendarId: calendarReference.calendarId,
22209
+ event: mapGoogleCalendarEvent(payload || {}),
22210
+ });
22211
+ });
22212
+ },
22213
+ async [UseCalendarToolNames.createEvent](args) {
22214
+ return withUseCalendarRuntime(args, async ({ calendarReference, accessToken }) => {
22215
+ const requestBody = createGoogleCalendarEventPayload({
22216
+ summary: normalizeRequiredText(args.summary, 'summary'),
22217
+ description: normalizeOptionalText(args.description),
22218
+ location: normalizeOptionalText(args.location),
22219
+ start: normalizeRequiredText(args.start, 'start'),
22220
+ end: normalizeRequiredText(args.end, 'end'),
22221
+ timeZone: normalizeOptionalText(args.timeZone),
22222
+ attendees: normalizeAttendees(args.attendees),
22223
+ reminderMinutes: normalizeReminderMinutes(args.reminderMinutes),
22224
+ });
22225
+ const payload = await callGoogleCalendarApi(accessToken, {
22226
+ method: 'POST',
22227
+ path: `/calendars/${encodeGoogleCalendarId(calendarReference.calendarId)}/events`,
22228
+ query: createSendUpdatesQuery(args.sendUpdates),
22229
+ body: requestBody,
22230
+ });
22231
+ return JSON.stringify({
22232
+ provider: calendarReference.provider,
22233
+ calendarUrl: calendarReference.url,
22234
+ calendarId: calendarReference.calendarId,
22235
+ event: mapGoogleCalendarEvent(payload || {}),
22236
+ });
22237
+ });
22238
+ },
22239
+ async [UseCalendarToolNames.updateEvent](args) {
22240
+ return withUseCalendarRuntime(args, async ({ calendarReference, accessToken }) => {
22241
+ const eventId = normalizeRequiredText(args.eventId, 'eventId');
22242
+ const requestBody = createGoogleCalendarEventPayload({
22243
+ summary: normalizeOptionalText(args.summary),
22244
+ description: normalizeOptionalText(args.description),
22245
+ location: normalizeOptionalText(args.location),
22246
+ start: normalizeOptionalText(args.start),
22247
+ end: normalizeOptionalText(args.end),
22248
+ timeZone: normalizeOptionalText(args.timeZone),
22249
+ attendees: normalizeAttendees(args.attendees),
22250
+ reminderMinutes: normalizeReminderMinutes(args.reminderMinutes),
22251
+ });
22252
+ const payload = await callGoogleCalendarApi(accessToken, {
22253
+ method: 'PATCH',
22254
+ path: `/calendars/${encodeGoogleCalendarId(calendarReference.calendarId)}/events/${encodeURIComponent(eventId)}`,
22255
+ query: createSendUpdatesQuery(args.sendUpdates),
22256
+ body: requestBody,
22257
+ });
22258
+ return JSON.stringify({
22259
+ provider: calendarReference.provider,
22260
+ calendarUrl: calendarReference.url,
22261
+ calendarId: calendarReference.calendarId,
22262
+ event: mapGoogleCalendarEvent(payload || {}),
22263
+ });
22264
+ });
22265
+ },
22266
+ async [UseCalendarToolNames.deleteEvent](args) {
22267
+ return withUseCalendarRuntime(args, async ({ calendarReference, accessToken }) => {
22268
+ const eventId = normalizeRequiredText(args.eventId, 'eventId');
22269
+ await callGoogleCalendarApi(accessToken, {
22270
+ method: 'DELETE',
22271
+ path: `/calendars/${encodeGoogleCalendarId(calendarReference.calendarId)}/events/${encodeURIComponent(eventId)}`,
22272
+ query: createSendUpdatesQuery(args.sendUpdates),
22273
+ });
22274
+ return JSON.stringify({
22275
+ provider: calendarReference.provider,
22276
+ calendarUrl: calendarReference.url,
22277
+ calendarId: calendarReference.calendarId,
22278
+ eventId,
22279
+ status: 'deleted',
22280
+ });
22281
+ });
22282
+ },
22283
+ async [UseCalendarToolNames.inviteGuests](args) {
22284
+ return withUseCalendarRuntime(args, async ({ calendarReference, accessToken }) => {
22285
+ const eventId = normalizeRequiredText(args.eventId, 'eventId');
22286
+ const guests = normalizeAttendees(args.guests);
22287
+ if (guests.length === 0) {
22288
+ throw new Error('Tool "calendar_invite_guests" requires non-empty "guests".');
22289
+ }
22290
+ const existingEvent = await callGoogleCalendarApi(accessToken, {
22291
+ method: 'GET',
22292
+ path: `/calendars/${encodeGoogleCalendarId(calendarReference.calendarId)}/events/${encodeURIComponent(eventId)}`,
22293
+ });
22294
+ const existingAttendees = ((existingEvent === null || existingEvent === void 0 ? void 0 : existingEvent.attendees) || [])
22295
+ .map((attendee) => normalizeOptionalText(attendee.email))
22296
+ .filter((email) => Boolean(email));
22297
+ const mergedAttendees = [...new Set([...existingAttendees, ...guests])];
22298
+ const payload = await callGoogleCalendarApi(accessToken, {
22299
+ method: 'PATCH',
22300
+ path: `/calendars/${encodeGoogleCalendarId(calendarReference.calendarId)}/events/${encodeURIComponent(eventId)}`,
22301
+ query: createSendUpdatesQuery(args.sendUpdates),
21622
22302
  body: {
21623
- type: 'string',
21624
- description: 'Legacy alias for markdown body content.',
22303
+ attendees: mergedAttendees.map((email) => ({ email })),
21625
22304
  },
21626
- },
21627
- required: [],
21628
- },
22305
+ });
22306
+ return JSON.stringify({
22307
+ provider: calendarReference.provider,
22308
+ calendarUrl: calendarReference.url,
22309
+ calendarId: calendarReference.calendarId,
22310
+ event: mapGoogleCalendarEvent(payload || {}),
22311
+ invitedGuests: guests,
22312
+ });
22313
+ });
21629
22314
  },
21630
- ];
22315
+ };
21631
22316
  }
21632
22317
  /**
21633
- * Note: [💞] Ignore a discrepancy between file name and entity name
22318
+ * Executes one tool operation with resolved USE CALENDAR runtime.
22319
+ *
22320
+ * @private function of createUseCalendarToolFunctions
21634
22321
  */
21635
-
22322
+ async function withUseCalendarRuntime(args, operation) {
22323
+ const runtime = resolveUseCalendarToolRuntimeOrWalletCredentialResult(args);
22324
+ if ('walletResult' in runtime) {
22325
+ return runtime.walletResult;
22326
+ }
22327
+ return operation(runtime);
22328
+ }
21636
22329
  /**
21637
- * USE IMAGE GENERATOR commitment definition
22330
+ * Encodes one Google calendar id for URL path usage.
21638
22331
  *
21639
- * The `USE IMAGE GENERATOR` commitment indicates that the agent can output
21640
- * markdown placeholders for UI-driven image generation.
22332
+ * @private function of createUseCalendarToolFunctions
22333
+ */
22334
+ function encodeGoogleCalendarId(calendarId) {
22335
+ return encodeURIComponent(calendarId);
22336
+ }
22337
+ /**
22338
+ * Normalizes one required textual input.
21641
22339
  *
21642
- * Example usage in agent source:
22340
+ * @private function of createUseCalendarToolFunctions
22341
+ */
22342
+ function normalizeRequiredText(value, fieldName) {
22343
+ const normalizedValue = normalizeOptionalText(value);
22344
+ if (!normalizedValue) {
22345
+ throw new Error(`Tool "${fieldName}" requires non-empty value.`);
22346
+ }
22347
+ return normalizedValue;
22348
+ }
22349
+ /**
22350
+ * Normalizes unknown text input to trimmed non-empty string.
21643
22351
  *
21644
- * ```book
21645
- * USE IMAGE GENERATOR
21646
- * USE IMAGE GENERATOR Create realistic images of nature
21647
- * ```
22352
+ * @private function of createUseCalendarToolFunctions
22353
+ */
22354
+ function normalizeOptionalText(value) {
22355
+ if (typeof value !== 'string') {
22356
+ return undefined;
22357
+ }
22358
+ const trimmedValue = value.trim();
22359
+ return trimmedValue || undefined;
22360
+ }
22361
+ /**
22362
+ * Normalizes optional attendee list from tool input.
21648
22363
  *
21649
- * @private [🪔] Maybe export the commitments through some package
22364
+ * @private function of createUseCalendarToolFunctions
21650
22365
  */
21651
- class UseImageGeneratorCommitmentDefinition extends BaseCommitmentDefinition {
21652
- constructor(type = 'USE IMAGE GENERATOR') {
21653
- super(type);
22366
+ function normalizeAttendees(value) {
22367
+ if (!Array.isArray(value)) {
22368
+ return [];
21654
22369
  }
21655
- get requiresContent() {
21656
- return false;
22370
+ const normalizedAttendees = value
22371
+ .filter((attendee) => typeof attendee === 'string')
22372
+ .map((attendee) => attendee.trim())
22373
+ .filter(Boolean);
22374
+ return [...new Set(normalizedAttendees)];
22375
+ }
22376
+ /**
22377
+ * Normalizes optional reminder-minute offsets from tool input.
22378
+ *
22379
+ * @private function of createUseCalendarToolFunctions
22380
+ */
22381
+ function normalizeReminderMinutes(value) {
22382
+ if (!Array.isArray(value)) {
22383
+ return [];
21657
22384
  }
21658
- /**
21659
- * Short one-line description of USE IMAGE GENERATOR.
21660
- */
21661
- get description() {
21662
- return 'Enable the agent to output markdown image placeholders that the UI turns into generated images.';
22385
+ const reminderMinutes = value
22386
+ .filter((minute) => typeof minute === 'number' && Number.isFinite(minute))
22387
+ .map((minute) => Math.max(0, Math.floor(minute)));
22388
+ return [...new Set(reminderMinutes)];
22389
+ }
22390
+ /**
22391
+ * Builds optional `sendUpdates` query for mutating Google Calendar requests.
22392
+ *
22393
+ * @private function of createUseCalendarToolFunctions
22394
+ */
22395
+ function createSendUpdatesQuery(sendUpdates) {
22396
+ if (sendUpdates === 'all' || sendUpdates === 'externalOnly' || sendUpdates === 'none') {
22397
+ return { sendUpdates };
21663
22398
  }
21664
- /**
21665
- * Icon for this commitment.
21666
- */
21667
- get icon() {
21668
- return '🖼️';
22399
+ return {};
22400
+ }
22401
+ /**
22402
+ * Creates one Google Calendar event payload from normalized tool arguments.
22403
+ *
22404
+ * @private function of createUseCalendarToolFunctions
22405
+ */
22406
+ function createGoogleCalendarEventPayload(options) {
22407
+ const payload = {};
22408
+ if (options.summary) {
22409
+ payload.summary = options.summary;
21669
22410
  }
21670
- /**
21671
- * Markdown documentation for USE IMAGE GENERATOR commitment.
21672
- */
21673
- get documentation() {
21674
- return spaceTrim$1.spaceTrim(`
21675
- # USE IMAGE GENERATOR
21676
-
21677
- Enables the agent to output markdown image placeholders that trigger image generation in the user interface.
21678
-
21679
- ## Key aspects
21680
-
21681
- - The content following \`USE IMAGE GENERATOR\` is an arbitrary text that the agent should know (e.g. style instructions or safety guidelines).
21682
- - The agent does **not** call an image-generation tool directly.
21683
- - The agent inserts markdown notation: \`![alt](?image-prompt=...)\`.
21684
- - The user interface detects the notation and generates the image asynchronously.
21685
-
21686
- ## Examples
21687
-
21688
- \`\`\`book
21689
- Visual Artist
21690
-
21691
- PERSONA You are a creative visual artist.
21692
- USE IMAGE GENERATOR
21693
- RULE Always describe the generated image to the user.
21694
- \`\`\`
21695
-
21696
- \`\`\`book
21697
- Interior Designer
21698
-
21699
- PERSONA You are an interior designer who helps users visualize their space.
21700
- USE IMAGE GENERATOR Professional interior design renders.
21701
- ACTION Add one generated image placeholder whenever a user asks for a visual.
21702
- \`\`\`
21703
- `);
22411
+ if (options.description) {
22412
+ payload.description = options.description;
21704
22413
  }
21705
- applyToAgentModelRequirements(requirements, content) {
21706
- const extraInstructions = formatOptionalInstructionBlock('Image instructions', content);
21707
- return this.appendToSystemMessage({
21708
- ...requirements,
21709
- _metadata: {
21710
- ...requirements._metadata,
21711
- useImageGenerator: content || true,
21712
- },
21713
- }, spaceTrim$1.spaceTrim((block) => `
21714
- Image generation:
21715
- - You do not generate images directly and you do not call any image tool.
21716
- - When the user asks for an image, include markdown notation in your message:
21717
- \`![<alt text>](?image-prompt=<prompt>)\`
21718
- - Keep \`<alt text>\` short and descriptive.
21719
- - Keep \`<prompt>\` detailed so the generated image matches the request.
21720
- - You can include normal explanatory text before and after the notation.
21721
- ${block(extraInstructions)}
21722
- `));
22414
+ if (options.location) {
22415
+ payload.location = options.location;
22416
+ }
22417
+ if (options.start) {
22418
+ payload.start = createGoogleCalendarDateValue(options.start, options.timeZone);
22419
+ }
22420
+ if (options.end) {
22421
+ payload.end = createGoogleCalendarDateValue(options.end, options.timeZone);
22422
+ }
22423
+ if (options.attendees && options.attendees.length > 0) {
22424
+ payload.attendees = options.attendees.map((email) => ({ email }));
21723
22425
  }
22426
+ if (options.reminderMinutes && options.reminderMinutes.length > 0) {
22427
+ payload.reminders = {
22428
+ useDefault: false,
22429
+ overrides: options.reminderMinutes.map((minutes) => ({
22430
+ method: 'popup',
22431
+ minutes,
22432
+ })),
22433
+ };
22434
+ }
22435
+ return payload;
21724
22436
  }
21725
22437
  /**
21726
- * Note: [💞] Ignore a discrepancy between file name and entity name
22438
+ * Converts date/dateTime input into a Google Calendar-compatible date object.
22439
+ *
22440
+ * @private function of createUseCalendarToolFunctions
21727
22441
  */
22442
+ function createGoogleCalendarDateValue(value, timeZone) {
22443
+ const isDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(value);
22444
+ if (isDateOnly) {
22445
+ return {
22446
+ date: value,
22447
+ };
22448
+ }
22449
+ return {
22450
+ dateTime: value,
22451
+ ...(timeZone ? { timeZone } : {}),
22452
+ };
22453
+ }
22454
+ /**
22455
+ * Maps raw Google Calendar event payload to a compact tool result object.
22456
+ *
22457
+ * @private function of createUseCalendarToolFunctions
22458
+ */
22459
+ function mapGoogleCalendarEvent(event) {
22460
+ return {
22461
+ id: event.id || null,
22462
+ summary: event.summary || null,
22463
+ description: event.description || null,
22464
+ location: event.location || null,
22465
+ status: event.status || null,
22466
+ htmlLink: event.htmlLink || null,
22467
+ start: event.start || null,
22468
+ end: event.end || null,
22469
+ organizer: event.organizer || null,
22470
+ attendees: (event.attendees || []).map((attendee) => ({
22471
+ email: attendee.email || null,
22472
+ responseStatus: attendee.responseStatus || null,
22473
+ })),
22474
+ };
22475
+ }
21728
22476
 
21729
22477
  /**
21730
- * USE MCP commitment definition
22478
+ * Shared calendar URL argument description used in USE CALENDAR tool schemas.
21731
22479
  *
21732
- * The `USE MCP` commitment allows to specify an MCP server URL which the agent will connect to
21733
- * for retrieving additional instructions and actions.
22480
+ * @private constant of createUseCalendarTools
22481
+ */
22482
+ const CALENDAR_URL_PARAMETER_DESCRIPTION = 'Google Calendar URL configured by USE CALENDAR (for example "https://calendar.google.com/...").';
22483
+ /**
22484
+ * Adds USE CALENDAR tool definitions while keeping already registered tools untouched.
21734
22485
  *
21735
- * The content following `USE MCP` is the URL of the MCP server.
22486
+ * @private function of UseCalendarCommitmentDefinition
22487
+ */
22488
+ function createUseCalendarTools(existingTools) {
22489
+ const updatedTools = [...existingTools];
22490
+ const addToolIfMissing = (tool) => {
22491
+ if (!updatedTools.some((existingTool) => existingTool.name === tool.name)) {
22492
+ updatedTools.push(tool);
22493
+ }
22494
+ };
22495
+ addToolIfMissing({
22496
+ name: UseCalendarToolNames.listEvents,
22497
+ description: 'List events from a configured calendar for a time range.',
22498
+ parameters: {
22499
+ type: 'object',
22500
+ properties: {
22501
+ calendarUrl: {
22502
+ type: 'string',
22503
+ description: CALENDAR_URL_PARAMETER_DESCRIPTION,
22504
+ },
22505
+ timeMin: {
22506
+ type: 'string',
22507
+ description: 'Inclusive event start bound in ISO datetime.',
22508
+ },
22509
+ timeMax: {
22510
+ type: 'string',
22511
+ description: 'Exclusive event end bound in ISO datetime.',
22512
+ },
22513
+ query: {
22514
+ type: 'string',
22515
+ description: 'Optional free-text event search query.',
22516
+ },
22517
+ maxResults: {
22518
+ type: 'integer',
22519
+ description: 'Maximum number of events to return.',
22520
+ },
22521
+ singleEvents: {
22522
+ type: 'boolean',
22523
+ description: 'Expand recurring events into individual instances.',
22524
+ },
22525
+ orderBy: {
22526
+ type: 'string',
22527
+ description: 'Optional ordering ("startTime" or "updated").',
22528
+ },
22529
+ timeZone: {
22530
+ type: 'string',
22531
+ description: 'Optional IANA timezone for response rendering.',
22532
+ },
22533
+ },
22534
+ required: [],
22535
+ },
22536
+ });
22537
+ addToolIfMissing({
22538
+ name: UseCalendarToolNames.getEvent,
22539
+ description: 'Get one event by id from a configured calendar.',
22540
+ parameters: {
22541
+ type: 'object',
22542
+ properties: {
22543
+ calendarUrl: {
22544
+ type: 'string',
22545
+ description: CALENDAR_URL_PARAMETER_DESCRIPTION,
22546
+ },
22547
+ eventId: {
22548
+ type: 'string',
22549
+ description: 'Google Calendar event id.',
22550
+ },
22551
+ },
22552
+ required: ['eventId'],
22553
+ },
22554
+ });
22555
+ addToolIfMissing({
22556
+ name: UseCalendarToolNames.createEvent,
22557
+ description: 'Create one event in a configured calendar.',
22558
+ parameters: {
22559
+ type: 'object',
22560
+ properties: {
22561
+ calendarUrl: {
22562
+ type: 'string',
22563
+ description: CALENDAR_URL_PARAMETER_DESCRIPTION,
22564
+ },
22565
+ summary: {
22566
+ type: 'string',
22567
+ description: 'Event title/summary.',
22568
+ },
22569
+ description: {
22570
+ type: 'string',
22571
+ description: 'Optional event description.',
22572
+ },
22573
+ location: {
22574
+ type: 'string',
22575
+ description: 'Optional event location.',
22576
+ },
22577
+ start: {
22578
+ type: 'string',
22579
+ description: 'Event start as ISO datetime or date.',
22580
+ },
22581
+ end: {
22582
+ type: 'string',
22583
+ description: 'Event end as ISO datetime or date.',
22584
+ },
22585
+ timeZone: {
22586
+ type: 'string',
22587
+ description: 'Optional timezone for datetime values.',
22588
+ },
22589
+ attendees: {
22590
+ type: 'array',
22591
+ description: 'Optional guest email list.',
22592
+ },
22593
+ reminderMinutes: {
22594
+ type: 'array',
22595
+ description: 'Optional popup reminder minute offsets.',
22596
+ },
22597
+ sendUpdates: {
22598
+ type: 'string',
22599
+ description: 'Guest update policy ("all", "externalOnly", "none").',
22600
+ },
22601
+ },
22602
+ required: ['summary', 'start', 'end'],
22603
+ },
22604
+ });
22605
+ addToolIfMissing({
22606
+ name: UseCalendarToolNames.updateEvent,
22607
+ description: 'Update one existing event in a configured calendar.',
22608
+ parameters: {
22609
+ type: 'object',
22610
+ properties: {
22611
+ calendarUrl: {
22612
+ type: 'string',
22613
+ description: CALENDAR_URL_PARAMETER_DESCRIPTION,
22614
+ },
22615
+ eventId: {
22616
+ type: 'string',
22617
+ description: 'Google Calendar event id.',
22618
+ },
22619
+ summary: {
22620
+ type: 'string',
22621
+ description: 'Updated event summary.',
22622
+ },
22623
+ description: {
22624
+ type: 'string',
22625
+ description: 'Updated event description.',
22626
+ },
22627
+ location: {
22628
+ type: 'string',
22629
+ description: 'Updated event location.',
22630
+ },
22631
+ start: {
22632
+ type: 'string',
22633
+ description: 'Updated event start as ISO datetime or date.',
22634
+ },
22635
+ end: {
22636
+ type: 'string',
22637
+ description: 'Updated event end as ISO datetime or date.',
22638
+ },
22639
+ timeZone: {
22640
+ type: 'string',
22641
+ description: 'Optional timezone for datetime values.',
22642
+ },
22643
+ attendees: {
22644
+ type: 'array',
22645
+ description: 'Optional replacement guest email list.',
22646
+ },
22647
+ reminderMinutes: {
22648
+ type: 'array',
22649
+ description: 'Optional replacement popup reminder minute offsets.',
22650
+ },
22651
+ sendUpdates: {
22652
+ type: 'string',
22653
+ description: 'Guest update policy ("all", "externalOnly", "none").',
22654
+ },
22655
+ },
22656
+ required: ['eventId'],
22657
+ },
22658
+ });
22659
+ addToolIfMissing({
22660
+ name: UseCalendarToolNames.deleteEvent,
22661
+ description: 'Delete one event from a configured calendar.',
22662
+ parameters: {
22663
+ type: 'object',
22664
+ properties: {
22665
+ calendarUrl: {
22666
+ type: 'string',
22667
+ description: CALENDAR_URL_PARAMETER_DESCRIPTION,
22668
+ },
22669
+ eventId: {
22670
+ type: 'string',
22671
+ description: 'Google Calendar event id.',
22672
+ },
22673
+ sendUpdates: {
22674
+ type: 'string',
22675
+ description: 'Guest update policy ("all", "externalOnly", "none").',
22676
+ },
22677
+ },
22678
+ required: ['eventId'],
22679
+ },
22680
+ });
22681
+ addToolIfMissing({
22682
+ name: UseCalendarToolNames.inviteGuests,
22683
+ description: 'Add guests to an existing event in a configured calendar.',
22684
+ parameters: {
22685
+ type: 'object',
22686
+ properties: {
22687
+ calendarUrl: {
22688
+ type: 'string',
22689
+ description: CALENDAR_URL_PARAMETER_DESCRIPTION,
22690
+ },
22691
+ eventId: {
22692
+ type: 'string',
22693
+ description: 'Google Calendar event id.',
22694
+ },
22695
+ guests: {
22696
+ type: 'array',
22697
+ description: 'Guest email list to add to the event.',
22698
+ },
22699
+ sendUpdates: {
22700
+ type: 'string',
22701
+ description: 'Guest update policy ("all", "externalOnly", "none").',
22702
+ },
22703
+ },
22704
+ required: ['eventId', 'guests'],
22705
+ },
22706
+ });
22707
+ return updatedTools;
22708
+ }
22709
+
22710
+ /**
22711
+ * Gets human-readable tool labels for USE CALENDAR functions.
21736
22712
  *
21737
- * Example usage in agent source:
22713
+ * @private function of UseCalendarCommitmentDefinition
22714
+ */
22715
+ function getUseCalendarToolTitles() {
22716
+ return {
22717
+ [UseCalendarToolNames.listEvents]: 'List calendar events',
22718
+ [UseCalendarToolNames.getEvent]: 'Get calendar event',
22719
+ [UseCalendarToolNames.createEvent]: 'Create calendar event',
22720
+ [UseCalendarToolNames.updateEvent]: 'Update calendar event',
22721
+ [UseCalendarToolNames.deleteEvent]: 'Delete calendar event',
22722
+ [UseCalendarToolNames.inviteGuests]: 'Invite calendar guests',
22723
+ };
22724
+ }
22725
+
22726
+ /**
22727
+ * Normalizes unknown metadata payload into a typed list of configured calendars.
21738
22728
  *
21739
- * ```book
21740
- * USE MCP http://mcp-server-url.com
21741
- * ```
22729
+ * @private internal utility of USE CALENDAR commitment
22730
+ */
22731
+ function normalizeConfiguredCalendars(rawCalendars) {
22732
+ if (!Array.isArray(rawCalendars)) {
22733
+ return [];
22734
+ }
22735
+ const uniqueCalendars = new Set();
22736
+ const calendars = [];
22737
+ for (const rawCalendar of rawCalendars) {
22738
+ if (!rawCalendar || typeof rawCalendar !== 'object') {
22739
+ continue;
22740
+ }
22741
+ const calendar = rawCalendar;
22742
+ const provider = normalizeProvider(calendar.provider);
22743
+ const url = normalizeText(calendar.url);
22744
+ const calendarId = normalizeText(calendar.calendarId);
22745
+ if (!provider || !url || !calendarId) {
22746
+ continue;
22747
+ }
22748
+ const uniqueKey = `${provider}|${url}`;
22749
+ if (uniqueCalendars.has(uniqueKey)) {
22750
+ continue;
22751
+ }
22752
+ uniqueCalendars.add(uniqueKey);
22753
+ const scopes = Array.isArray(calendar.scopes)
22754
+ ? calendar.scopes
22755
+ .filter((scope) => typeof scope === 'string')
22756
+ .map((scope) => scope.trim())
22757
+ .filter(Boolean)
22758
+ : [];
22759
+ calendars.push({
22760
+ provider,
22761
+ url,
22762
+ calendarId,
22763
+ scopes,
22764
+ ...(normalizeText(calendar.tokenRef) ? { tokenRef: normalizeText(calendar.tokenRef) } : {}),
22765
+ });
22766
+ }
22767
+ return calendars;
22768
+ }
22769
+ /**
22770
+ * Normalizes optional provider text to one supported value.
22771
+ *
22772
+ * @private function of normalizeConfiguredCalendars
22773
+ */
22774
+ function normalizeProvider(value) {
22775
+ if (typeof value !== 'string') {
22776
+ return null;
22777
+ }
22778
+ const normalizedProvider = value.trim().toLowerCase();
22779
+ if (normalizedProvider === 'google') {
22780
+ return 'google';
22781
+ }
22782
+ return null;
22783
+ }
22784
+ /**
22785
+ * Normalizes unknown text input to trimmed non-empty string.
22786
+ *
22787
+ * @private function of normalizeConfiguredCalendars
22788
+ */
22789
+ function normalizeText(value) {
22790
+ return typeof value === 'string' ? value.trim() : '';
22791
+ }
22792
+
22793
+ /**
22794
+ * USE CALENDAR commitment definition.
22795
+ *
22796
+ * `USE CALENDAR` enables calendar tooling so the agent can read and manage events
22797
+ * in one configured Google Calendar.
22798
+ *
22799
+ * Authentication is expected through runtime context provided by the host app UI.
22800
+ * Hosts can provide manual wallet tokens or host-managed OAuth tokens.
21742
22801
  *
21743
22802
  * @private [🪔] Maybe export the commitments through some package
21744
22803
  */
21745
- class UseMcpCommitmentDefinition extends BaseCommitmentDefinition {
22804
+ class UseCalendarCommitmentDefinition extends BaseCommitmentDefinition {
21746
22805
  constructor() {
21747
- super('USE MCP', ['MCP']);
22806
+ super('USE CALENDAR', ['CALENDAR']);
21748
22807
  }
21749
22808
  /**
21750
- * Short one-line description of USE MCP.
22809
+ * Short one-line description of USE CALENDAR.
21751
22810
  */
21752
22811
  get description() {
21753
- return 'Connects the agent to an external MCP server for additional capabilities.';
22812
+ return 'Enable calendar tools for reading and managing events through Google Calendar.';
21754
22813
  }
21755
22814
  /**
21756
22815
  * Icon for this commitment.
21757
22816
  */
21758
22817
  get icon() {
21759
- return '🔌';
22818
+ return '📅';
21760
22819
  }
21761
22820
  /**
21762
- * Markdown documentation for USE MCP commitment.
22821
+ * Markdown documentation for USE CALENDAR commitment.
21763
22822
  */
21764
22823
  get documentation() {
21765
22824
  return spaceTrim$1.spaceTrim(`
21766
- # USE MCP
22825
+ # USE CALENDAR
21767
22826
 
21768
- Connects the agent to an external Model Context Protocol (MCP) server.
22827
+ Enables the agent to access and manage one Google Calendar.
21769
22828
 
21770
22829
  ## Key aspects
21771
22830
 
21772
- - The content following \`USE MCP\` must be a valid URL
21773
- - Multiple MCP servers can be connected by using multiple \`USE MCP\` commitments
21774
- - The agent will have access to tools and resources provided by the MCP server
22831
+ - The first URL in the commitment should point to a Google Calendar URL.
22832
+ - Optional \`SCOPES\` lines can provide explicit OAuth scopes.
22833
+ - Optional extra instructions can follow calendar reference lines.
22834
+ - Runtime provides Google Calendar OAuth token (manual wallet token or host-managed OAuth token).
22835
+ - Tools support listing events, reading one event, creating events, updating events, deleting events, and inviting guests.
21775
22836
 
21776
- ## Example
22837
+ ## Examples
21777
22838
 
21778
22839
  \`\`\`book
21779
- Company Lawyer
22840
+ Scheduling Assistant
21780
22841
 
21781
- PERSONA You are a company lawyer.
21782
- USE MCP http://legal-db.example.com
22842
+ PERSONA You coordinate meetings and schedules.
22843
+ USE CALENDAR https://calendar.google.com/calendar/u/0/r
22844
+ \`\`\`
22845
+
22846
+ \`\`\`book
22847
+ Executive Assistant
22848
+
22849
+ USE CALENDAR https://calendar.google.com/calendar/u/0/r
22850
+ SCOPES https://www.googleapis.com/auth/calendar.readonly
22851
+ RULE Ask for confirmation before deleting events.
21783
22852
  \`\`\`
21784
22853
  `);
21785
22854
  }
21786
22855
  applyToAgentModelRequirements(requirements, content) {
21787
- const mcpServerUrl = content.trim();
21788
- if (!mcpServerUrl) {
21789
- return requirements;
21790
- }
21791
- const existingMcpServers = requirements.mcpServers || [];
21792
- // Avoid duplicates
21793
- if (existingMcpServers.includes(mcpServerUrl)) {
21794
- return requirements;
21795
- }
21796
- return {
22856
+ var _a;
22857
+ const parsedCommitment = parseUseCalendarCommitmentContent(content);
22858
+ const existingConfiguredCalendars = normalizeConfiguredCalendars((_a = requirements._metadata) === null || _a === void 0 ? void 0 : _a.useCalendars);
22859
+ if (parsedCommitment.calendar) {
22860
+ addConfiguredCalendarIfMissing(existingConfiguredCalendars, parsedCommitment.calendar);
22861
+ }
22862
+ const calendarsList = existingConfiguredCalendars.length > 0
22863
+ ? existingConfiguredCalendars
22864
+ .map((calendar) => [
22865
+ `- ${calendar.provider}: ${calendar.url}`,
22866
+ calendar.scopes.length > 0 ? ` scopes: ${calendar.scopes.join(', ')}` : '',
22867
+ ]
22868
+ .filter(Boolean)
22869
+ .join('\n'))
22870
+ .join('\n')
22871
+ : '- Calendar is resolved from runtime context';
22872
+ const extraInstructions = formatOptionalInstructionBlock('Calendar instructions', parsedCommitment.instructions);
22873
+ return this.appendToSystemMessage({
21797
22874
  ...requirements,
21798
- mcpServers: [...existingMcpServers, mcpServerUrl],
21799
- };
22875
+ tools: createUseCalendarTools(requirements.tools || []),
22876
+ _metadata: {
22877
+ ...requirements._metadata,
22878
+ useCalendar: true,
22879
+ useCalendars: existingConfiguredCalendars,
22880
+ },
22881
+ }, spaceTrim$1.spaceTrim((block) => `
22882
+ Calendar tools:
22883
+ - You can inspect and manage events in configured calendars.
22884
+ - Supported operations include read, create, update, delete, invite guests, and reminders.
22885
+ - Configured calendars:
22886
+ ${block(calendarsList)}
22887
+ - USE CALENDAR credentials are read from wallet records (ACCESS_TOKEN, service "${UseCalendarWallet.service}", key "${UseCalendarWallet.key}").
22888
+ - If credentials are missing, ask user to connect calendar credentials in host UI and/or add them to wallet.
22889
+ ${block(extraInstructions)}
22890
+ `));
21800
22891
  }
22892
+ /**
22893
+ * Gets human-readable titles for tool functions provided by this commitment.
22894
+ */
22895
+ getToolTitles() {
22896
+ return getUseCalendarToolTitles();
22897
+ }
22898
+ /**
22899
+ * Gets calendar tool function implementations.
22900
+ */
22901
+ getToolFunctions() {
22902
+ return createUseCalendarToolFunctions();
22903
+ }
22904
+ }
22905
+ /**
22906
+ * Adds calendar into configured calendars list if it is not already present.
22907
+ *
22908
+ * @private function of UseCalendarCommitmentDefinition
22909
+ */
22910
+ function addConfiguredCalendarIfMissing(configuredCalendars, calendarReference) {
22911
+ if (configuredCalendars.some((calendar) => calendar.provider === calendarReference.provider && calendar.url === calendarReference.url)) {
22912
+ return;
22913
+ }
22914
+ configuredCalendars.push({
22915
+ provider: calendarReference.provider,
22916
+ url: calendarReference.url,
22917
+ calendarId: calendarReference.calendarId,
22918
+ scopes: [...calendarReference.scopes],
22919
+ ...(calendarReference.tokenRef ? { tokenRef: calendarReference.tokenRef } : {}),
22920
+ });
21801
22921
  }
21802
22922
  /**
21803
22923
  * Note: [💞] Ignore a discrepancy between file name and entity name
21804
22924
  */
21805
22925
 
21806
22926
  /**
21807
- * USE POPUP commitment definition
22927
+ * Lightweight email token matcher used for `USE EMAIL` first-line parsing.
21808
22928
  *
21809
- * The `USE POPUP` commitment indicates that the agent can open a popup window with a specific website.
21810
- * This is useful, for example, when the agent writes a post on Facebook but wants the user to post it on Facebook.
22929
+ * @private internal USE EMAIL constant
22930
+ */
22931
+ const EMAIL_TOKEN_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/;
22932
+ /**
22933
+ * Parses `USE EMAIL` commitment content into optional sender email + additional instructions.
21811
22934
  *
21812
- * Example usage in agent source:
22935
+ * Examples:
22936
+ * - `agent@example.com`
22937
+ * - `agent@example.com Keep emails concise`
22938
+ * - `Keep emails concise`
21813
22939
  *
21814
- * ```book
21815
- * USE POPUP Allow to open Facebook and Linkedin
21816
- * ```
22940
+ * @private internal utility of USE EMAIL commitment
22941
+ */
22942
+ function parseUseEmailCommitmentContent(content) {
22943
+ const trimmedContent = spaceTrim$1.spaceTrim(content);
22944
+ if (!trimmedContent) {
22945
+ return {
22946
+ senderEmail: null,
22947
+ senderEmailRaw: null,
22948
+ instructions: '',
22949
+ };
22950
+ }
22951
+ const lines = trimmedContent
22952
+ .split(/\r?\n/)
22953
+ .map((line) => line.trim())
22954
+ .filter(Boolean);
22955
+ if (lines.length === 0) {
22956
+ return {
22957
+ senderEmail: null,
22958
+ senderEmailRaw: null,
22959
+ instructions: '',
22960
+ };
22961
+ }
22962
+ const firstLine = lines[0] || '';
22963
+ const senderMatch = firstLine.match(EMAIL_TOKEN_PATTERN);
22964
+ const senderEmailRaw = (senderMatch === null || senderMatch === void 0 ? void 0 : senderMatch[0]) || null;
22965
+ const senderEmail = senderEmailRaw && isValidEmail(senderEmailRaw) ? senderEmailRaw : null;
22966
+ let firstLineWithoutSender = firstLine;
22967
+ if (senderEmailRaw) {
22968
+ const matchIndex = firstLine.indexOf(senderEmailRaw);
22969
+ const prefix = firstLine.slice(0, matchIndex).trim();
22970
+ const suffix = firstLine.slice(matchIndex + senderEmailRaw.length).trim();
22971
+ firstLineWithoutSender = [prefix, suffix].filter(Boolean).join(' ').trim();
22972
+ }
22973
+ const instructionLines = [firstLineWithoutSender, ...lines.slice(1)].filter(Boolean);
22974
+ const instructions = instructionLines.join('\n').trim();
22975
+ return {
22976
+ senderEmail,
22977
+ senderEmailRaw,
22978
+ instructions,
22979
+ };
22980
+ }
22981
+
22982
+ /**
22983
+ * Client-side safe wrapper for sending emails.
22984
+ *
22985
+ * This function proxies requests to the Agents Server API endpoint for email queuing,
22986
+ * making it safe to use in browser environments.
22987
+ *
22988
+ * @param args Email payload forwarded to the server-side `send_email` tool
22989
+ * @param agentsServerUrl The base URL of the agents server (defaults to current origin)
22990
+ * @returns Result string from the server-side send_email tool
22991
+ *
22992
+ * @private internal utility for USE EMAIL commitment
22993
+ */
22994
+ async function sendEmailViaBrowser(args, agentsServerUrl) {
22995
+ try {
22996
+ const baseUrl = agentsServerUrl || (typeof window !== 'undefined' ? window.location.origin : '');
22997
+ if (!baseUrl) {
22998
+ throw new Error('Agents server URL is required in non-browser environments');
22999
+ }
23000
+ const apiUrl = new URL('/api/send-email', baseUrl);
23001
+ const response = await fetch(apiUrl.toString(), {
23002
+ method: 'POST',
23003
+ headers: {
23004
+ 'Content-Type': 'application/json',
23005
+ },
23006
+ body: JSON.stringify(args),
23007
+ });
23008
+ if (!response.ok) {
23009
+ const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
23010
+ throw new Error(`Failed to send email: ${errorData.error || response.statusText}`);
23011
+ }
23012
+ const data = await response.json();
23013
+ if (!data.success) {
23014
+ throw new Error(`Email sending failed: ${data.error || 'Unknown error'}`);
23015
+ }
23016
+ return typeof data.result === 'string' ? data.result : JSON.stringify(data.result);
23017
+ }
23018
+ catch (error) {
23019
+ const errorMessage = error instanceof Error ? error.message : String(error);
23020
+ throw new Error(`Error sending email via browser: ${errorMessage}`);
23021
+ }
23022
+ }
23023
+
23024
+ /**
23025
+ * Tool name used by USE EMAIL.
23026
+ *
23027
+ * @private internal USE EMAIL constant
23028
+ */
23029
+ const SEND_EMAIL_TOOL_NAME = 'send_email';
23030
+ /**
23031
+ * Wallet service used for SMTP credentials required by USE EMAIL.
23032
+ *
23033
+ * @private internal USE EMAIL constant
23034
+ */
23035
+ const USE_EMAIL_SMTP_WALLET_SERVICE = 'smtp';
23036
+ /**
23037
+ * Wallet key used for SMTP credentials required by USE EMAIL.
23038
+ *
23039
+ * @private internal USE EMAIL constant
23040
+ */
23041
+ const USE_EMAIL_SMTP_WALLET_KEY = 'use-email-smtp-credentials';
23042
+ /**
23043
+ * USE EMAIL commitment definition.
23044
+ *
23045
+ * The `USE EMAIL` commitment enables outbound email sending through the `send_email` tool.
21817
23046
  *
21818
23047
  * @private [🪔] Maybe export the commitments through some package
21819
23048
  */
21820
- class UsePopupCommitmentDefinition extends BaseCommitmentDefinition {
23049
+ class UseEmailCommitmentDefinition extends BaseCommitmentDefinition {
21821
23050
  constructor() {
21822
- super('USE POPUP', ['POPUP']);
23051
+ super('USE EMAIL', ['EMAIL', 'MAIL']);
21823
23052
  }
21824
- /**
21825
- * The `USE POPUP` commitment is standalone or with instructions.
21826
- */
21827
23053
  get requiresContent() {
21828
23054
  return false;
21829
23055
  }
21830
23056
  /**
21831
- * Short one-line description of USE POPUP.
23057
+ * Short one-line description of USE EMAIL.
21832
23058
  */
21833
23059
  get description() {
21834
- return 'Enable the agent to open a popup window with a specific website.';
23060
+ return 'Enable outbound email sending through a wallet-backed SMTP configuration.';
21835
23061
  }
21836
23062
  /**
21837
23063
  * Icon for this commitment.
21838
23064
  */
21839
23065
  get icon() {
21840
- return '🪟';
23066
+ return '📧';
21841
23067
  }
21842
23068
  /**
21843
- * Markdown documentation for USE POPUP commitment.
23069
+ * Markdown documentation for USE EMAIL commitment.
21844
23070
  */
21845
23071
  get documentation() {
21846
23072
  return spaceTrim$1.spaceTrim(`
21847
- # USE POPUP
23073
+ # USE EMAIL
21848
23074
 
21849
- Enables the agent to open a popup window with a specific website.
23075
+ Enables the agent to send outbound emails through SMTP.
21850
23076
 
21851
23077
  ## Key aspects
21852
23078
 
21853
- - The content following \`USE POPUP\` is an arbitrary text that the agent should know (e.g. constraints or instructions).
21854
- - The actual popup opening is handled by the agent runtime (usually in the browser)
21855
- - Allows the agent to open websites for the user to interact with (e.g. social media posts)
23079
+ - The agent sends email via the \`send_email\` tool.
23080
+ - SMTP credentials are expected from wallet records (\`ACCESS_TOKEN\`, service \`smtp\`, key \`use-email-smtp-credentials\`).
23081
+ - Commitment content can optionally begin with a default sender email address:
23082
+ - \`USE EMAIL agent@example.com\`
23083
+ - Remaining commitment content is treated as optional email-writing instructions.
21856
23084
 
21857
23085
  ## Examples
21858
23086
 
21859
23087
  \`\`\`book
21860
- John the Copywriter
23088
+ Writing Agent
23089
+ USE EMAIL agent@example.com
23090
+ RULE Write emails to customers according to the instructions from user.
23091
+ \`\`\`
21861
23092
 
21862
- PERSONA You are a professional copywriter writing about CNC machines.
21863
- USE POPUP Allow to open Facebook and Linkedin
23093
+ \`\`\`book
23094
+ Formal Email Assistant
23095
+ USE EMAIL agent@example.com Keep emails concise and formal.
21864
23096
  \`\`\`
21865
23097
  `);
21866
23098
  }
21867
23099
  applyToAgentModelRequirements(requirements, content) {
21868
- const extraInstructions = formatOptionalInstructionBlock('Popup instructions', content);
21869
- // Get existing tools array or create new one
21870
- const existingTools = requirements.tools || [];
21871
- // Add 'open_popup' to tools if not already present
21872
- const updatedTools = existingTools.some((tool) => tool.name === 'open_popup')
21873
- ? existingTools
21874
- : [
21875
- ...existingTools,
21876
- {
21877
- name: 'open_popup',
21878
- description: spaceTrim$1.spaceTrim(`
21879
- Opens a popup window with a specific URL.
21880
- Use this when you want to show a specific website to the user in a new window.
21881
- ${!content ? '' : `Constraints / instructions: ${content}`}
21882
- `),
21883
- parameters: {
21884
- type: 'object',
21885
- properties: {
21886
- url: {
21887
- type: 'string',
21888
- description: 'The URL to open in the popup window',
21889
- },
21890
- },
21891
- required: ['url'],
21892
- },
21893
- },
21894
- ];
21895
- // Return requirements with updated tools and metadata
23100
+ const parsedCommitment = parseUseEmailCommitmentContent(content);
23101
+ const extraInstructions = formatOptionalInstructionBlock('Email instructions', parsedCommitment.instructions);
23102
+ const senderInstruction = parsedCommitment.senderEmail
23103
+ ? `- Default sender address from commitment: "${parsedCommitment.senderEmail}".`
23104
+ : '';
23105
+ const updatedTools = addUseEmailTools(requirements.tools || []);
21896
23106
  return this.appendToSystemMessage({
21897
23107
  ...requirements,
21898
23108
  tools: updatedTools,
21899
23109
  _metadata: {
21900
23110
  ...requirements._metadata,
21901
- usePopup: content || true,
23111
+ useEmail: true,
23112
+ ...(parsedCommitment.senderEmail ? { useEmailSender: parsedCommitment.senderEmail } : {}),
21902
23113
  },
21903
23114
  }, spaceTrim$1.spaceTrim((block) => `
21904
- Tool:
21905
- - You can open a popup window with a specific URL using the tool "open_popup".
21906
- - Use this when you want the user to see or interact with a specific website.
23115
+ Email tool:
23116
+ - Use "${SEND_EMAIL_TOOL_NAME}" to send outbound emails.
23117
+ - Prefer \`message\` argument compatible with Promptbook \`Message\` type.
23118
+ - Include subject in \`message.metadata.subject\` (or use legacy \`subject\` argument).
23119
+ - USE EMAIL credentials are read from wallet records (ACCESS_TOKEN, service "${USE_EMAIL_SMTP_WALLET_SERVICE}", key "${USE_EMAIL_SMTP_WALLET_KEY}").
23120
+ - Wallet secret must contain SMTP credentials in JSON format with fields \`host\`, \`port\`, \`secure\`, \`username\`, \`password\`.
23121
+ - If credentials are missing, ask user to add wallet credentials.
23122
+ ${block(senderInstruction)}
21907
23123
  ${block(extraInstructions)}
21908
- `));
23124
+ `));
21909
23125
  }
21910
23126
  /**
21911
23127
  * Gets human-readable titles for tool functions provided by this commitment.
21912
23128
  */
21913
23129
  getToolTitles() {
21914
23130
  return {
21915
- open_popup: 'Open popup',
23131
+ [SEND_EMAIL_TOOL_NAME]: 'Send email',
21916
23132
  };
21917
23133
  }
21918
23134
  /**
21919
- * Gets the `open_popup` tool function implementation.
23135
+ * Gets the browser-safe `send_email` implementation.
23136
+ *
23137
+ * Node.js runtime overrides this via `getAllCommitmentsToolFunctionsForNode`.
21920
23138
  */
21921
23139
  getToolFunctions() {
21922
23140
  return {
21923
- async open_popup(args) {
21924
- console.log('!!!! [Tool] open_popup called', { args });
21925
- const { url } = args;
21926
- if (typeof window !== 'undefined') {
21927
- window.open(url, '_blank');
21928
- return `Popup window with URL "${url}" was opened.`;
21929
- }
21930
- return spaceTrim$1.spaceTrim(`
21931
- Popup window with URL "${url}" was requested.
21932
-
21933
- Note: The agent is currently running on the server, so the popup cannot be opened automatically.
21934
- The user can open it manually from the tool call details in the chat.
21935
- `);
23141
+ async [SEND_EMAIL_TOOL_NAME](args) {
23142
+ return sendEmailViaBrowser(args);
21936
23143
  },
21937
23144
  };
21938
23145
  }
21939
23146
  }
21940
23147
  /**
21941
- * Note: [💞] Ignore a discrepancy between file name and entity name
21942
- */
21943
-
21944
- /**
21945
- * Tool name used by the USE PRIVACY commitment.
23148
+ * Adds USE EMAIL tool definition while keeping already registered tools untouched.
21946
23149
  *
21947
- * @private internal USE PRIVACY constant
23150
+ * @private utility of USE EMAIL commitment
21948
23151
  */
21949
- const TURN_PRIVACY_ON_TOOL_NAME = 'turn_privacy_on';
21950
- /**
21951
- * Status returned when the UI should ask the user to confirm private mode.
21952
- *
21953
- * @private internal USE PRIVACY constant
21954
- */
21955
- const PRIVACY_CONFIRMATION_REQUIRED_STATUS = 'confirmation-required';
21956
- /**
21957
- * Status returned when private mode is already enabled.
21958
- *
21959
- * @private internal USE PRIVACY constant
21960
- */
21961
- const PRIVACY_ALREADY_ENABLED_STATUS = 'already-enabled';
21962
- /**
21963
- * Checks whether private mode is already enabled in runtime context.
21964
- *
21965
- * @private utility of USE PRIVACY commitment
21966
- */
21967
- function isPrivateModeEnabledInRuntimeContext(args) {
21968
- var _a;
21969
- const runtimeContext = readToolRuntimeContextFromToolArgs(args);
21970
- return ((_a = runtimeContext === null || runtimeContext === void 0 ? void 0 : runtimeContext.memory) === null || _a === void 0 ? void 0 : _a.isPrivateMode) === true;
23152
+ function addUseEmailTools(existingTools) {
23153
+ if (existingTools.some((tool) => tool.name === SEND_EMAIL_TOOL_NAME)) {
23154
+ return [...existingTools];
23155
+ }
23156
+ return [
23157
+ ...existingTools,
23158
+ {
23159
+ name: SEND_EMAIL_TOOL_NAME,
23160
+ description: 'Send an outbound email through configured SMTP credentials. Prefer providing Message-like payload in `message`.',
23161
+ parameters: {
23162
+ type: 'object',
23163
+ properties: {
23164
+ message: {
23165
+ type: 'object',
23166
+ description: 'Preferred input payload compatible with Promptbook Message type. Use metadata.subject for subject line.',
23167
+ },
23168
+ to: {
23169
+ type: 'string',
23170
+ description: 'Legacy alias for recipients (use comma-separated emails or JSON array encoded as string).',
23171
+ },
23172
+ cc: {
23173
+ type: 'string',
23174
+ description: 'Optional CC recipients (use comma-separated emails or JSON array encoded as string).',
23175
+ },
23176
+ subject: {
23177
+ type: 'string',
23178
+ description: 'Legacy alias for subject.',
23179
+ },
23180
+ body: {
23181
+ type: 'string',
23182
+ description: 'Legacy alias for markdown body content.',
23183
+ },
23184
+ },
23185
+ required: [],
23186
+ },
23187
+ },
23188
+ ];
21971
23189
  }
21972
23190
  /**
21973
- * Creates a standard "confirmation required" result payload.
21974
- *
21975
- * @private utility of USE PRIVACY commitment
23191
+ * Note: [💞] Ignore a discrepancy between file name and entity name
21976
23192
  */
21977
- function createPrivacyConfirmationRequiredResult() {
21978
- return {
21979
- status: PRIVACY_CONFIRMATION_REQUIRED_STATUS,
21980
- message: 'Private mode requires explicit user confirmation in the UI. Ask the user to confirm the privacy prompt.',
21981
- };
21982
- }
23193
+
21983
23194
  /**
21984
- * Creates a standard "already enabled" result payload.
23195
+ * USE IMAGE GENERATOR commitment definition
21985
23196
  *
21986
- * @private utility of USE PRIVACY commitment
21987
- */
21988
- function createPrivacyAlreadyEnabledResult() {
21989
- return {
21990
- status: PRIVACY_ALREADY_ENABLED_STATUS,
21991
- message: 'Private mode is already enabled for this chat session.',
21992
- };
21993
- }
21994
- /**
21995
- * USE PRIVACY commitment definition.
23197
+ * The `USE IMAGE GENERATOR` commitment indicates that the agent can output
23198
+ * markdown placeholders for UI-driven image generation.
21996
23199
  *
21997
- * The `USE PRIVACY` commitment enables an agent to request enabling private mode.
23200
+ * Example usage in agent source:
23201
+ *
23202
+ * ```book
23203
+ * USE IMAGE GENERATOR
23204
+ * USE IMAGE GENERATOR Create realistic images of nature
23205
+ * ```
21998
23206
  *
21999
23207
  * @private [🪔] Maybe export the commitments through some package
22000
23208
  */
22001
- class UsePrivacyCommitmentDefinition extends BaseCommitmentDefinition {
22002
- constructor() {
22003
- super('USE PRIVACY', ['PRIVACY']);
23209
+ class UseImageGeneratorCommitmentDefinition extends BaseCommitmentDefinition {
23210
+ constructor(type = 'USE IMAGE GENERATOR') {
23211
+ super(type);
22004
23212
  }
22005
23213
  get requiresContent() {
22006
23214
  return false;
22007
23215
  }
22008
23216
  /**
22009
- * Short one-line description of USE PRIVACY.
23217
+ * Short one-line description of USE IMAGE GENERATOR.
22010
23218
  */
22011
23219
  get description() {
22012
- return 'Enable the agent to request turning private mode on for sensitive conversations.';
23220
+ return 'Enable the agent to output markdown image placeholders that the UI turns into generated images.';
22013
23221
  }
22014
23222
  /**
22015
23223
  * Icon for this commitment.
22016
23224
  */
22017
23225
  get icon() {
22018
- return '🔒';
23226
+ return '🖼️';
22019
23227
  }
22020
23228
  /**
22021
- * Markdown documentation for USE PRIVACY commitment.
23229
+ * Markdown documentation for USE IMAGE GENERATOR commitment.
22022
23230
  */
22023
23231
  get documentation() {
22024
23232
  return spaceTrim$1.spaceTrim(`
22025
- # USE PRIVACY
23233
+ # USE IMAGE GENERATOR
22026
23234
 
22027
- Enables the agent to request turning on private mode in chat.
23235
+ Enables the agent to output markdown image placeholders that trigger image generation in the user interface.
22028
23236
 
22029
23237
  ## Key aspects
22030
23238
 
22031
- - The tool \`turn_privacy_on\` asks the UI to show a confirmation dialog to the user.
22032
- - Private mode is enabled only after explicit user confirmation in the UI.
22033
- - In the current implementation, this reuses existing private mode behavior in chat.
22034
- - While private mode is active, chat persistence, memory persistence, and self-learning are disabled.
22035
- - Proper encryption is planned for future updates, but not implemented by this commitment yet.
22036
- - Optional content after \`USE PRIVACY\` can provide additional privacy instructions.
23239
+ - The content following \`USE IMAGE GENERATOR\` is an arbitrary text that the agent should know (e.g. style instructions or safety guidelines).
23240
+ - The agent does **not** call an image-generation tool directly.
23241
+ - The agent inserts markdown notation: \`![alt](?image-prompt=...)\`.
23242
+ - The user interface detects the notation and generates the image asynchronously.
22037
23243
 
22038
23244
  ## Examples
22039
23245
 
22040
23246
  \`\`\`book
22041
- Sensitive Assistant
23247
+ Visual Artist
22042
23248
 
22043
- PERSONA You help with sensitive topics where privacy is important.
22044
- USE PRIVACY
23249
+ PERSONA You are a creative visual artist.
23250
+ USE IMAGE GENERATOR
23251
+ RULE Always describe the generated image to the user.
22045
23252
  \`\`\`
22046
23253
 
22047
23254
  \`\`\`book
22048
- Compliance Assistant
23255
+ Interior Designer
22049
23256
 
22050
- PERSONA You assist with legal and HR conversations.
22051
- USE PRIVACY Offer private mode when user asks to avoid storing data.
23257
+ PERSONA You are an interior designer who helps users visualize their space.
23258
+ USE IMAGE GENERATOR Professional interior design renders.
23259
+ ACTION Add one generated image placeholder whenever a user asks for a visual.
22052
23260
  \`\`\`
22053
23261
  `);
22054
23262
  }
22055
23263
  applyToAgentModelRequirements(requirements, content) {
22056
- const extraInstructions = formatOptionalInstructionBlock('Privacy instructions', content);
22057
- const existingTools = requirements.tools || [];
22058
- const tools = existingTools.some((tool) => tool.name === TURN_PRIVACY_ON_TOOL_NAME)
22059
- ? existingTools
22060
- : [
22061
- ...existingTools,
22062
- {
22063
- name: TURN_PRIVACY_ON_TOOL_NAME,
22064
- description: spaceTrim$1.spaceTrim(`
22065
- Requests turning private mode on in the chat UI.
22066
- The user must explicitly confirm the action in a dialog before private mode is enabled.
22067
- Use this for sensitive topics or when the user asks not to store conversation data.
22068
- `),
22069
- parameters: {
22070
- type: 'object',
22071
- properties: {},
22072
- required: [],
22073
- },
22074
- },
22075
- ];
23264
+ const extraInstructions = formatOptionalInstructionBlock('Image instructions', content);
22076
23265
  return this.appendToSystemMessage({
22077
23266
  ...requirements,
22078
- tools,
22079
23267
  _metadata: {
22080
23268
  ...requirements._metadata,
22081
- usePrivacy: content || true,
23269
+ useImageGenerator: content || true,
22082
23270
  },
22083
23271
  }, spaceTrim$1.spaceTrim((block) => `
22084
- Privacy mode:
22085
- - Use "${TURN_PRIVACY_ON_TOOL_NAME}" when the user asks for a private/sensitive conversation.
22086
- - This tool requests a UI confirmation dialog. Private mode is enabled only after user confirms.
22087
- - Current implementation uses the existing chat private mode (no chat persistence, memory persistence, or self-learning while active).
22088
- - Do not claim that end-to-end encryption is implemented yet.
23272
+ Image generation:
23273
+ - You do not generate images directly and you do not call any image tool.
23274
+ - When the user asks for an image, include markdown notation in your message:
23275
+ \`![<alt text>](?image-prompt=<prompt>)\`
23276
+ - Keep \`<alt text>\` short and descriptive.
23277
+ - Keep \`<prompt>\` detailed so the generated image matches the request.
23278
+ - You can include normal explanatory text before and after the notation.
22089
23279
  ${block(extraInstructions)}
22090
23280
  `));
22091
23281
  }
23282
+ }
23283
+ /**
23284
+ * Note: [💞] Ignore a discrepancy between file name and entity name
23285
+ */
23286
+
23287
+ /**
23288
+ * USE MCP commitment definition
23289
+ *
23290
+ * The `USE MCP` commitment allows to specify an MCP server URL which the agent will connect to
23291
+ * for retrieving additional instructions and actions.
23292
+ *
23293
+ * The content following `USE MCP` is the URL of the MCP server.
23294
+ *
23295
+ * Example usage in agent source:
23296
+ *
23297
+ * ```book
23298
+ * USE MCP http://mcp-server-url.com
23299
+ * ```
23300
+ *
23301
+ * @private [🪔] Maybe export the commitments through some package
23302
+ */
23303
+ class UseMcpCommitmentDefinition extends BaseCommitmentDefinition {
23304
+ constructor() {
23305
+ super('USE MCP', ['MCP']);
23306
+ }
22092
23307
  /**
22093
- * Gets human-readable titles for tool functions provided by this commitment.
23308
+ * Short one-line description of USE MCP.
22094
23309
  */
22095
- getToolTitles() {
22096
- return {
22097
- [TURN_PRIVACY_ON_TOOL_NAME]: 'Turn privacy mode on',
22098
- };
23310
+ get description() {
23311
+ return 'Connects the agent to an external MCP server for additional capabilities.';
22099
23312
  }
22100
23313
  /**
22101
- * Gets the `turn_privacy_on` tool function implementation.
23314
+ * Icon for this commitment.
22102
23315
  */
22103
- getToolFunctions() {
23316
+ get icon() {
23317
+ return '🔌';
23318
+ }
23319
+ /**
23320
+ * Markdown documentation for USE MCP commitment.
23321
+ */
23322
+ get documentation() {
23323
+ return spaceTrim$1.spaceTrim(`
23324
+ # USE MCP
23325
+
23326
+ Connects the agent to an external Model Context Protocol (MCP) server.
23327
+
23328
+ ## Key aspects
23329
+
23330
+ - The content following \`USE MCP\` must be a valid URL
23331
+ - Multiple MCP servers can be connected by using multiple \`USE MCP\` commitments
23332
+ - The agent will have access to tools and resources provided by the MCP server
23333
+
23334
+ ## Example
23335
+
23336
+ \`\`\`book
23337
+ Company Lawyer
23338
+
23339
+ PERSONA You are a company lawyer.
23340
+ USE MCP http://legal-db.example.com
23341
+ \`\`\`
23342
+ `);
23343
+ }
23344
+ applyToAgentModelRequirements(requirements, content) {
23345
+ const mcpServerUrl = content.trim();
23346
+ if (!mcpServerUrl) {
23347
+ return requirements;
23348
+ }
23349
+ const existingMcpServers = requirements.mcpServers || [];
23350
+ // Avoid duplicates
23351
+ if (existingMcpServers.includes(mcpServerUrl)) {
23352
+ return requirements;
23353
+ }
22104
23354
  return {
22105
- async [TURN_PRIVACY_ON_TOOL_NAME](args) {
22106
- const result = isPrivateModeEnabledInRuntimeContext(args)
22107
- ? createPrivacyAlreadyEnabledResult()
22108
- : createPrivacyConfirmationRequiredResult();
22109
- return JSON.stringify(result);
22110
- },
23355
+ ...requirements,
23356
+ mcpServers: [...existingMcpServers, mcpServerUrl],
22111
23357
  };
22112
23358
  }
22113
23359
  }
@@ -22115,135 +23361,447 @@
22115
23361
  * Note: [💞] Ignore a discrepancy between file name and entity name
22116
23362
  */
22117
23363
 
22118
- /* eslint-disable no-magic-numbers */
22119
23364
  /**
22120
- * Default byte limit for one best-effort text decode.
23365
+ * USE POPUP commitment definition
22121
23366
  *
22122
- * @private constant of decodeAttachmentAsText
22123
- */
22124
- const DEFAULT_ATTACHMENT_TEXT_DECODE_BYTES = 512 * 1024;
22125
- /**
22126
- * Marker appended when only a prefix of the payload is decoded.
23367
+ * The `USE POPUP` commitment indicates that the agent can open a popup window with a specific website.
23368
+ * This is useful, for example, when the agent writes a post on Facebook but wants the user to post it on Facebook.
22127
23369
  *
22128
- * @private constant of decodeAttachmentAsText
22129
- */
22130
- const TRUNCATED_MARKER = '…[TRUNCATED]…';
22131
- /**
22132
- * MIME types that are trustworthy indicators of textual content.
23370
+ * Example usage in agent source:
22133
23371
  *
22134
- * @private constant of decodeAttachmentAsText
23372
+ * ```book
23373
+ * USE POPUP Allow to open Facebook and Linkedin
23374
+ * ```
23375
+ *
23376
+ * @private [🪔] Maybe export the commitments through some package
22135
23377
  */
22136
- const TRUSTED_TEXT_MIME_TYPES = new Set([
22137
- 'application/json',
22138
- 'application/ld+json',
22139
- 'application/javascript',
22140
- 'application/x-javascript',
22141
- 'application/xml',
22142
- 'application/xhtml+xml',
22143
- 'application/x-www-form-urlencoded',
22144
- 'application/yaml',
22145
- 'application/x-yaml',
22146
- 'application/toml',
22147
- 'application/sql',
22148
- 'application/rtf',
22149
- 'application/x-subrip',
22150
- ]);
23378
+ class UsePopupCommitmentDefinition extends BaseCommitmentDefinition {
23379
+ constructor() {
23380
+ super('USE POPUP', ['POPUP']);
23381
+ }
23382
+ /**
23383
+ * The `USE POPUP` commitment is standalone or with instructions.
23384
+ */
23385
+ get requiresContent() {
23386
+ return false;
23387
+ }
23388
+ /**
23389
+ * Short one-line description of USE POPUP.
23390
+ */
23391
+ get description() {
23392
+ return 'Enable the agent to open a popup window with a specific website.';
23393
+ }
23394
+ /**
23395
+ * Icon for this commitment.
23396
+ */
23397
+ get icon() {
23398
+ return '🪟';
23399
+ }
23400
+ /**
23401
+ * Markdown documentation for USE POPUP commitment.
23402
+ */
23403
+ get documentation() {
23404
+ return spaceTrim$1.spaceTrim(`
23405
+ # USE POPUP
23406
+
23407
+ Enables the agent to open a popup window with a specific website.
23408
+
23409
+ ## Key aspects
23410
+
23411
+ - The content following \`USE POPUP\` is an arbitrary text that the agent should know (e.g. constraints or instructions).
23412
+ - The actual popup opening is handled by the agent runtime (usually in the browser)
23413
+ - Allows the agent to open websites for the user to interact with (e.g. social media posts)
23414
+
23415
+ ## Examples
23416
+
23417
+ \`\`\`book
23418
+ John the Copywriter
23419
+
23420
+ PERSONA You are a professional copywriter writing about CNC machines.
23421
+ USE POPUP Allow to open Facebook and Linkedin
23422
+ \`\`\`
23423
+ `);
23424
+ }
23425
+ applyToAgentModelRequirements(requirements, content) {
23426
+ const extraInstructions = formatOptionalInstructionBlock('Popup instructions', content);
23427
+ // Get existing tools array or create new one
23428
+ const existingTools = requirements.tools || [];
23429
+ // Add 'open_popup' to tools if not already present
23430
+ const updatedTools = existingTools.some((tool) => tool.name === 'open_popup')
23431
+ ? existingTools
23432
+ : [
23433
+ ...existingTools,
23434
+ {
23435
+ name: 'open_popup',
23436
+ description: spaceTrim$1.spaceTrim(`
23437
+ Opens a popup window with a specific URL.
23438
+ Use this when you want to show a specific website to the user in a new window.
23439
+ ${!content ? '' : `Constraints / instructions: ${content}`}
23440
+ `),
23441
+ parameters: {
23442
+ type: 'object',
23443
+ properties: {
23444
+ url: {
23445
+ type: 'string',
23446
+ description: 'The URL to open in the popup window',
23447
+ },
23448
+ },
23449
+ required: ['url'],
23450
+ },
23451
+ },
23452
+ ];
23453
+ // Return requirements with updated tools and metadata
23454
+ return this.appendToSystemMessage({
23455
+ ...requirements,
23456
+ tools: updatedTools,
23457
+ _metadata: {
23458
+ ...requirements._metadata,
23459
+ usePopup: content || true,
23460
+ },
23461
+ }, spaceTrim$1.spaceTrim((block) => `
23462
+ Tool:
23463
+ - You can open a popup window with a specific URL using the tool "open_popup".
23464
+ - Use this when you want the user to see or interact with a specific website.
23465
+ ${block(extraInstructions)}
23466
+ `));
23467
+ }
23468
+ /**
23469
+ * Gets human-readable titles for tool functions provided by this commitment.
23470
+ */
23471
+ getToolTitles() {
23472
+ return {
23473
+ open_popup: 'Open popup',
23474
+ };
23475
+ }
23476
+ /**
23477
+ * Gets the `open_popup` tool function implementation.
23478
+ */
23479
+ getToolFunctions() {
23480
+ return {
23481
+ async open_popup(args) {
23482
+ console.log('!!!! [Tool] open_popup called', { args });
23483
+ const { url } = args;
23484
+ if (typeof window !== 'undefined') {
23485
+ window.open(url, '_blank');
23486
+ return `Popup window with URL "${url}" was opened.`;
23487
+ }
23488
+ return spaceTrim$1.spaceTrim(`
23489
+ Popup window with URL "${url}" was requested.
23490
+
23491
+ Note: The agent is currently running on the server, so the popup cannot be opened automatically.
23492
+ The user can open it manually from the tool call details in the chat.
23493
+ `);
23494
+ },
23495
+ };
23496
+ }
23497
+ }
22151
23498
  /**
22152
- * MIME types that are trustworthy indicators of binary content.
22153
- *
22154
- * @private constant of decodeAttachmentAsText
23499
+ * Note: [💞] Ignore a discrepancy between file name and entity name
22155
23500
  */
22156
- const TRUSTED_BINARY_MIME_TYPES = new Set([
22157
- 'application/pdf',
22158
- 'application/zip',
22159
- 'application/gzip',
22160
- 'application/x-gzip',
22161
- 'application/x-7z-compressed',
22162
- 'application/vnd.rar',
22163
- 'application/x-rar-compressed',
22164
- ]);
23501
+
22165
23502
  /**
22166
- * MIME prefixes that strongly indicate binary content.
23503
+ * Tool name used by the USE PRIVACY commitment.
22167
23504
  *
22168
- * @private constant of decodeAttachmentAsText
23505
+ * @private internal USE PRIVACY constant
22169
23506
  */
22170
- const TRUSTED_BINARY_MIME_PREFIXES = ['image/', 'audio/', 'video/', 'font/', 'model/'];
23507
+ const TURN_PRIVACY_ON_TOOL_NAME = 'turn_privacy_on';
22171
23508
  /**
22172
- * Suspicious characters that often appear when `windows-1250` text is decoded as `windows-1252`.
23509
+ * Status returned when the UI should ask the user to confirm private mode.
22173
23510
  *
22174
- * @private constant of decodeAttachmentAsText
23511
+ * @private internal USE PRIVACY constant
22175
23512
  */
22176
- const SUSPICIOUS_SINGLE_BYTE_ARTIFACTS = new Set([
22177
- 'ˇ',
22178
- '˘',
22179
- '˙',
22180
- '˛',
22181
- '˝',
22182
- '¸',
22183
- '¤',
22184
- '¦',
22185
- '¨',
22186
- '¯',
22187
- '²',
22188
- '³',
22189
- '¹',
22190
- ]);
23513
+ const PRIVACY_CONFIRMATION_REQUIRED_STATUS = 'confirmation-required';
22191
23514
  /**
22192
- * Supported fallback encodings tried when UTF-8 is not convincing.
23515
+ * Status returned when private mode is already enabled.
22193
23516
  *
22194
- * @private constant of decodeAttachmentAsText
23517
+ * @private internal USE PRIVACY constant
22195
23518
  */
22196
- const FALLBACK_ENCODINGS = ['windows-1250', 'windows-1252', 'iso-8859-1'];
23519
+ const PRIVACY_ALREADY_ENABLED_STATUS = 'already-enabled';
22197
23520
  /**
22198
- * Converts unknown byte containers into `Uint8Array`.
23521
+ * Checks whether private mode is already enabled in runtime context.
22199
23522
  *
22200
- * @private function of decodeAttachmentAsText
23523
+ * @private utility of USE PRIVACY commitment
22201
23524
  */
22202
- function toUint8Array(bytes) {
22203
- if (bytes instanceof Uint8Array) {
22204
- return bytes;
22205
- }
22206
- if (ArrayBuffer.isView(bytes)) {
22207
- return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
22208
- }
22209
- return new Uint8Array(bytes);
23525
+ function isPrivateModeEnabledInRuntimeContext(args) {
23526
+ var _a;
23527
+ const runtimeContext = readToolRuntimeContextFromToolArgs(args);
23528
+ return ((_a = runtimeContext === null || runtimeContext === void 0 ? void 0 : runtimeContext.memory) === null || _a === void 0 ? void 0 : _a.isPrivateMode) === true;
22210
23529
  }
22211
23530
  /**
22212
- * Removes MIME parameters and normalizes casing.
23531
+ * Creates a standard "confirmation required" result payload.
22213
23532
  *
22214
- * @private function of decodeAttachmentAsText
23533
+ * @private utility of USE PRIVACY commitment
22215
23534
  */
22216
- function normalizeMimeType(mimeType) {
22217
- if (!mimeType) {
22218
- return null;
22219
- }
22220
- const normalized = mimeType.split(';')[0].trim().toLowerCase();
22221
- return normalized || null;
23535
+ function createPrivacyConfirmationRequiredResult() {
23536
+ return {
23537
+ status: PRIVACY_CONFIRMATION_REQUIRED_STATUS,
23538
+ message: 'Private mode requires explicit user confirmation in the UI. Ask the user to confirm the privacy prompt.',
23539
+ };
22222
23540
  }
22223
23541
  /**
22224
- * Extracts optional `charset=...` information from a MIME type value.
23542
+ * Creates a standard "already enabled" result payload.
22225
23543
  *
22226
- * @private function of decodeAttachmentAsText
23544
+ * @private utility of USE PRIVACY commitment
22227
23545
  */
22228
- function extractCharset(mimeType) {
22229
- if (!mimeType) {
22230
- return null;
22231
- }
22232
- const match = mimeType.match(/charset\s*=\s*("?)([^";\s]+)\1/i);
22233
- if (!match) {
22234
- return null;
22235
- }
22236
- return match[2].trim().toLowerCase();
23546
+ function createPrivacyAlreadyEnabledResult() {
23547
+ return {
23548
+ status: PRIVACY_ALREADY_ENABLED_STATUS,
23549
+ message: 'Private mode is already enabled for this chat session.',
23550
+ };
22237
23551
  }
22238
23552
  /**
22239
- * Returns true when the MIME type is a strong textual hint.
23553
+ * USE PRIVACY commitment definition.
22240
23554
  *
22241
- * @private function of decodeAttachmentAsText
23555
+ * The `USE PRIVACY` commitment enables an agent to request enabling private mode.
23556
+ *
23557
+ * @private [🪔] Maybe export the commitments through some package
22242
23558
  */
22243
- function isTrustedTextMimeType(mimeType) {
22244
- if (!mimeType) {
22245
- return false;
22246
- }
23559
+ class UsePrivacyCommitmentDefinition extends BaseCommitmentDefinition {
23560
+ constructor() {
23561
+ super('USE PRIVACY', ['PRIVACY']);
23562
+ }
23563
+ get requiresContent() {
23564
+ return false;
23565
+ }
23566
+ /**
23567
+ * Short one-line description of USE PRIVACY.
23568
+ */
23569
+ get description() {
23570
+ return 'Enable the agent to request turning private mode on for sensitive conversations.';
23571
+ }
23572
+ /**
23573
+ * Icon for this commitment.
23574
+ */
23575
+ get icon() {
23576
+ return '🔒';
23577
+ }
23578
+ /**
23579
+ * Markdown documentation for USE PRIVACY commitment.
23580
+ */
23581
+ get documentation() {
23582
+ return spaceTrim$1.spaceTrim(`
23583
+ # USE PRIVACY
23584
+
23585
+ Enables the agent to request turning on private mode in chat.
23586
+
23587
+ ## Key aspects
23588
+
23589
+ - The tool \`turn_privacy_on\` asks the UI to show a confirmation dialog to the user.
23590
+ - Private mode is enabled only after explicit user confirmation in the UI.
23591
+ - In the current implementation, this reuses existing private mode behavior in chat.
23592
+ - While private mode is active, chat persistence, memory persistence, and self-learning are disabled.
23593
+ - Proper encryption is planned for future updates, but not implemented by this commitment yet.
23594
+ - Optional content after \`USE PRIVACY\` can provide additional privacy instructions.
23595
+
23596
+ ## Examples
23597
+
23598
+ \`\`\`book
23599
+ Sensitive Assistant
23600
+
23601
+ PERSONA You help with sensitive topics where privacy is important.
23602
+ USE PRIVACY
23603
+ \`\`\`
23604
+
23605
+ \`\`\`book
23606
+ Compliance Assistant
23607
+
23608
+ PERSONA You assist with legal and HR conversations.
23609
+ USE PRIVACY Offer private mode when user asks to avoid storing data.
23610
+ \`\`\`
23611
+ `);
23612
+ }
23613
+ applyToAgentModelRequirements(requirements, content) {
23614
+ const extraInstructions = formatOptionalInstructionBlock('Privacy instructions', content);
23615
+ const existingTools = requirements.tools || [];
23616
+ const tools = existingTools.some((tool) => tool.name === TURN_PRIVACY_ON_TOOL_NAME)
23617
+ ? existingTools
23618
+ : [
23619
+ ...existingTools,
23620
+ {
23621
+ name: TURN_PRIVACY_ON_TOOL_NAME,
23622
+ description: spaceTrim$1.spaceTrim(`
23623
+ Requests turning private mode on in the chat UI.
23624
+ The user must explicitly confirm the action in a dialog before private mode is enabled.
23625
+ Use this for sensitive topics or when the user asks not to store conversation data.
23626
+ `),
23627
+ parameters: {
23628
+ type: 'object',
23629
+ properties: {},
23630
+ required: [],
23631
+ },
23632
+ },
23633
+ ];
23634
+ return this.appendToSystemMessage({
23635
+ ...requirements,
23636
+ tools,
23637
+ _metadata: {
23638
+ ...requirements._metadata,
23639
+ usePrivacy: content || true,
23640
+ },
23641
+ }, spaceTrim$1.spaceTrim((block) => `
23642
+ Privacy mode:
23643
+ - Use "${TURN_PRIVACY_ON_TOOL_NAME}" when the user asks for a private/sensitive conversation.
23644
+ - This tool requests a UI confirmation dialog. Private mode is enabled only after user confirms.
23645
+ - Current implementation uses the existing chat private mode (no chat persistence, memory persistence, or self-learning while active).
23646
+ - Do not claim that end-to-end encryption is implemented yet.
23647
+ ${block(extraInstructions)}
23648
+ `));
23649
+ }
23650
+ /**
23651
+ * Gets human-readable titles for tool functions provided by this commitment.
23652
+ */
23653
+ getToolTitles() {
23654
+ return {
23655
+ [TURN_PRIVACY_ON_TOOL_NAME]: 'Turn privacy mode on',
23656
+ };
23657
+ }
23658
+ /**
23659
+ * Gets the `turn_privacy_on` tool function implementation.
23660
+ */
23661
+ getToolFunctions() {
23662
+ return {
23663
+ async [TURN_PRIVACY_ON_TOOL_NAME](args) {
23664
+ const result = isPrivateModeEnabledInRuntimeContext(args)
23665
+ ? createPrivacyAlreadyEnabledResult()
23666
+ : createPrivacyConfirmationRequiredResult();
23667
+ return JSON.stringify(result);
23668
+ },
23669
+ };
23670
+ }
23671
+ }
23672
+ /**
23673
+ * Note: [💞] Ignore a discrepancy between file name and entity name
23674
+ */
23675
+
23676
+ /* eslint-disable no-magic-numbers */
23677
+ /**
23678
+ * Default byte limit for one best-effort text decode.
23679
+ *
23680
+ * @private constant of decodeAttachmentAsText
23681
+ */
23682
+ const DEFAULT_ATTACHMENT_TEXT_DECODE_BYTES = 512 * 1024;
23683
+ /**
23684
+ * Marker appended when only a prefix of the payload is decoded.
23685
+ *
23686
+ * @private constant of decodeAttachmentAsText
23687
+ */
23688
+ const TRUNCATED_MARKER = '…[TRUNCATED]…';
23689
+ /**
23690
+ * MIME types that are trustworthy indicators of textual content.
23691
+ *
23692
+ * @private constant of decodeAttachmentAsText
23693
+ */
23694
+ const TRUSTED_TEXT_MIME_TYPES = new Set([
23695
+ 'application/json',
23696
+ 'application/ld+json',
23697
+ 'application/javascript',
23698
+ 'application/x-javascript',
23699
+ 'application/xml',
23700
+ 'application/xhtml+xml',
23701
+ 'application/x-www-form-urlencoded',
23702
+ 'application/yaml',
23703
+ 'application/x-yaml',
23704
+ 'application/toml',
23705
+ 'application/sql',
23706
+ 'application/rtf',
23707
+ 'application/x-subrip',
23708
+ ]);
23709
+ /**
23710
+ * MIME types that are trustworthy indicators of binary content.
23711
+ *
23712
+ * @private constant of decodeAttachmentAsText
23713
+ */
23714
+ const TRUSTED_BINARY_MIME_TYPES = new Set([
23715
+ 'application/pdf',
23716
+ 'application/zip',
23717
+ 'application/gzip',
23718
+ 'application/x-gzip',
23719
+ 'application/x-7z-compressed',
23720
+ 'application/vnd.rar',
23721
+ 'application/x-rar-compressed',
23722
+ ]);
23723
+ /**
23724
+ * MIME prefixes that strongly indicate binary content.
23725
+ *
23726
+ * @private constant of decodeAttachmentAsText
23727
+ */
23728
+ const TRUSTED_BINARY_MIME_PREFIXES = ['image/', 'audio/', 'video/', 'font/', 'model/'];
23729
+ /**
23730
+ * Suspicious characters that often appear when `windows-1250` text is decoded as `windows-1252`.
23731
+ *
23732
+ * @private constant of decodeAttachmentAsText
23733
+ */
23734
+ const SUSPICIOUS_SINGLE_BYTE_ARTIFACTS = new Set([
23735
+ 'ˇ',
23736
+ '˘',
23737
+ '˙',
23738
+ '˛',
23739
+ '˝',
23740
+ '¸',
23741
+ '¤',
23742
+ '¦',
23743
+ '¨',
23744
+ '¯',
23745
+ '²',
23746
+ '³',
23747
+ '¹',
23748
+ ]);
23749
+ /**
23750
+ * Supported fallback encodings tried when UTF-8 is not convincing.
23751
+ *
23752
+ * @private constant of decodeAttachmentAsText
23753
+ */
23754
+ const FALLBACK_ENCODINGS = ['windows-1250', 'windows-1252', 'iso-8859-1'];
23755
+ /**
23756
+ * Converts unknown byte containers into `Uint8Array`.
23757
+ *
23758
+ * @private function of decodeAttachmentAsText
23759
+ */
23760
+ function toUint8Array(bytes) {
23761
+ if (bytes instanceof Uint8Array) {
23762
+ return bytes;
23763
+ }
23764
+ if (ArrayBuffer.isView(bytes)) {
23765
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
23766
+ }
23767
+ return new Uint8Array(bytes);
23768
+ }
23769
+ /**
23770
+ * Removes MIME parameters and normalizes casing.
23771
+ *
23772
+ * @private function of decodeAttachmentAsText
23773
+ */
23774
+ function normalizeMimeType(mimeType) {
23775
+ if (!mimeType) {
23776
+ return null;
23777
+ }
23778
+ const normalized = mimeType.split(';')[0].trim().toLowerCase();
23779
+ return normalized || null;
23780
+ }
23781
+ /**
23782
+ * Extracts optional `charset=...` information from a MIME type value.
23783
+ *
23784
+ * @private function of decodeAttachmentAsText
23785
+ */
23786
+ function extractCharset(mimeType) {
23787
+ if (!mimeType) {
23788
+ return null;
23789
+ }
23790
+ const match = mimeType.match(/charset\s*=\s*("?)([^";\s]+)\1/i);
23791
+ if (!match) {
23792
+ return null;
23793
+ }
23794
+ return match[2].trim().toLowerCase();
23795
+ }
23796
+ /**
23797
+ * Returns true when the MIME type is a strong textual hint.
23798
+ *
23799
+ * @private function of decodeAttachmentAsText
23800
+ */
23801
+ function isTrustedTextMimeType(mimeType) {
23802
+ if (!mimeType) {
23803
+ return false;
23804
+ }
22247
23805
  return (mimeType.startsWith('text/') ||
22248
23806
  mimeType.endsWith('+json') ||
22249
23807
  mimeType.endsWith('+xml') ||
@@ -25207,503 +26765,186 @@
25207
26765
  new NoteCommitmentDefinition('COMMENT'),
25208
26766
  new NoteCommitmentDefinition('NONCE'),
25209
26767
  new NoteCommitmentDefinition('TODO'),
25210
- new GoalCommitmentDefinition('GOAL'),
25211
- new GoalCommitmentDefinition('GOALS'),
25212
- new InitialMessageCommitmentDefinition(),
25213
- new UserMessageCommitmentDefinition(),
25214
- new InternalMessageCommitmentDefinition(),
25215
- new AgentMessageCommitmentDefinition(),
25216
- new MessageSuffixCommitmentDefinition(),
25217
- new MessageCommitmentDefinition('MESSAGE'),
25218
- new MessageCommitmentDefinition('MESSAGES'),
25219
- new ScenarioCommitmentDefinition('SCENARIO'),
25220
- new ScenarioCommitmentDefinition('SCENARIOS'),
25221
- new DeleteCommitmentDefinition('DELETE'),
25222
- new DeleteCommitmentDefinition('CANCEL'),
25223
- new DeleteCommitmentDefinition('DISCARD'),
25224
- new DeleteCommitmentDefinition('REMOVE'),
25225
- new DictionaryCommitmentDefinition(),
25226
- new OpenCommitmentDefinition(),
25227
- new ClosedCommitmentDefinition(),
25228
- new TeamCommitmentDefinition(),
25229
- new UseBrowserCommitmentDefinition(),
25230
- new UseSearchEngineCommitmentDefinition(),
25231
- new UseSpawnCommitmentDefinition(),
25232
- new UseTimeoutCommitmentDefinition(),
25233
- new UseTimeCommitmentDefinition(),
25234
- new UseUserLocationCommitmentDefinition(),
25235
- new UseEmailCommitmentDefinition(),
25236
- new UsePopupCommitmentDefinition(),
25237
- new UseImageGeneratorCommitmentDefinition('USE IMAGE GENERATOR'),
25238
- new UseMcpCommitmentDefinition(),
25239
- new UsePrivacyCommitmentDefinition(),
25240
- new UseProjectCommitmentDefinition(),
25241
- new UseCommitmentDefinition(),
25242
- // Not yet implemented commitments (using placeholder)
25243
- new NotYetImplementedCommitmentDefinition('EXPECT'),
25244
- new NotYetImplementedCommitmentDefinition('BEHAVIOUR'),
25245
- new NotYetImplementedCommitmentDefinition('BEHAVIOURS'),
25246
- new NotYetImplementedCommitmentDefinition('AVOID'),
25247
- new NotYetImplementedCommitmentDefinition('AVOIDANCE'),
25248
- new NotYetImplementedCommitmentDefinition('CONTEXT'),
25249
- // <- TODO: Prompt: Leverage aliases instead of duplicating commitment definitions
25250
- ];
25251
- /**
25252
- * TODO: [🧠] Maybe create through standardized $register
25253
- * Note: [💞] Ignore a discrepancy between file name and entity name
25254
- */
25255
-
25256
- /**
25257
- * Gets all available commitment definitions
25258
- * @returns Array of all commitment definitions
25259
- *
25260
- * @public exported from `@promptbook/core`
25261
- */
25262
- function getAllCommitmentDefinitions() {
25263
- return $deepFreeze([...COMMITMENT_REGISTRY]);
25264
- }
25265
-
25266
- /**
25267
- * Collects tool functions from all commitment definitions.
25268
- *
25269
- * @returns Map of tool function implementations.
25270
- * @private internal helper for commitment tool registry
25271
- */
25272
- function collectCommitmentToolFunctions() {
25273
- const allToolFunctions = {};
25274
- for (const commitmentDefinition of getAllCommitmentDefinitions()) {
25275
- const toolFunctions = commitmentDefinition.getToolFunctions();
25276
- for (const [funcName, funcImpl] of Object.entries(toolFunctions)) {
25277
- if (allToolFunctions[funcName] !== undefined &&
25278
- just(false) /* <- Note: [??] How to deal with commitment aliases */) {
25279
- throw new UnexpectedError(`Duplicate tool function name detected: \`${funcName}\` provided by commitment \`${commitmentDefinition.type}\``);
25280
- }
25281
- allToolFunctions[funcName] = funcImpl;
25282
- }
25283
- }
25284
- return allToolFunctions;
25285
- }
25286
- /**
25287
- * Creates a proxy that resolves tool functions on demand.
25288
- *
25289
- * @param getFunctions - Provider of current tool functions.
25290
- * @returns Proxy exposing tool functions as properties.
25291
- * @private internal helper for commitment tool registry
25292
- */
25293
- function createToolFunctionsProxy(getFunctions) {
25294
- const resolveFunctions = () => getFunctions();
25295
- return new Proxy({}, {
25296
- get(_target, prop) {
25297
- if (typeof prop !== 'string') {
25298
- return undefined;
25299
- }
25300
- return resolveFunctions()[prop];
25301
- },
25302
- has(_target, prop) {
25303
- if (typeof prop !== 'string') {
25304
- return false;
25305
- }
25306
- return prop in resolveFunctions();
25307
- },
25308
- ownKeys() {
25309
- return Object.keys(resolveFunctions());
25310
- },
25311
- getOwnPropertyDescriptor(_target, prop) {
25312
- if (typeof prop !== 'string') {
25313
- return undefined;
25314
- }
25315
- const value = resolveFunctions()[prop];
25316
- if (value === undefined) {
25317
- return undefined;
25318
- }
25319
- return {
25320
- enumerable: true,
25321
- configurable: true,
25322
- writable: false,
25323
- value,
25324
- };
25325
- },
25326
- });
25327
- }
25328
- /**
25329
- * Note: [💞] Ignore a discrepancy between file name and entity name
25330
- */
25331
-
25332
- const nodeToolFunctions = {
25333
- /**
25334
- * @@@
25335
- *
25336
- * Note: [??] This function has implementation both for browser and node, this is the full one for node
25337
- */
25338
- async fetch_url_content(args) {
25339
- console.log('!!!! [Tool] fetch_url_content called', { args });
25340
- const { url } = args;
25341
- return await fetchUrlContent(url);
25342
- },
25343
- /**
25344
- * @@@
25345
- *
25346
- * Note: [??] This function has implementation both for browser and node, this is the server one for node
25347
- */
25348
- run_browser: resolveRunBrowserToolForNode(),
25349
- /**
25350
- * @@@
25351
- *
25352
- * Note: [??] This function has implementation both for browser and node, this is the server one for node
25353
- */
25354
- send_email: resolveSendEmailToolForNode(),
25355
- /**
25356
- * @@@
25357
- *
25358
- * Note: [??] This function has implementation both for browser and node, this is the server one for node
25359
- */
25360
- spawn_agent: resolveSpawnAgentToolForNode(),
25361
- // TODO: !!!! Unhardcode, make proper server function register from definitions
25362
- };
25363
- const nodeToolFunctionsProxy = createToolFunctionsProxy(() => ({
25364
- ...collectCommitmentToolFunctions(),
25365
- ...nodeToolFunctions,
25366
- }));
25367
- /**
25368
- * Gets all function implementations provided by all commitments
25369
- *
25370
- * Note: This function is intended for server use, there is also equivalent `getAllCommitmentsToolFunctionsForBrowser` for browser use
25371
- *
25372
- * @public exported from `@promptbook/node`
25373
- */
25374
- function getAllCommitmentsToolFunctionsForNode() {
25375
- if (!$isRunningInNode()) {
25376
- throw new EnvironmentMismatchError(spaceTrim(`
25377
- Function getAllCommitmentsToolFunctionsForNode should be run in Node.js environment.
25378
-
25379
- - In browser use getAllCommitmentsToolFunctionsForBrowser instead.
25380
- - This function can include server-only tools which cannot run in browser environment.
25381
-
25382
- `));
25383
- }
25384
- return nodeToolFunctionsProxy;
25385
- }
25386
- /**
25387
- * Note: [??] Code in this file should never be never released in packages that could be imported into browser environment
25388
- * TODO: [??] Unite `xxxForServer` and `xxxForNode` naming
25389
- */
25390
-
25391
- /**
25392
- * Normalize options for `execCommand` and `execCommands`
25393
- *
25394
- * Note: `$` is used to indicate that this function behaves differently according to `process.platform`
25395
- *
25396
- * @private internal utility of `execCommand` and `execCommands`
25397
- */
25398
- function $execCommandNormalizeOptions(options) {
25399
- var _a, _b, _c, _d;
25400
- let command;
25401
- let cwd;
25402
- let crashOnError;
25403
- let args = [];
25404
- let timeout;
25405
- let isVerbose;
25406
- let env;
25407
- if (typeof options === 'string') {
25408
- // TODO: [1] DRY default values
25409
- command = options;
25410
- cwd = process.cwd();
25411
- crashOnError = true;
25412
- timeout = Infinity; // <- TODO: [⏳]
25413
- isVerbose = DEFAULT_IS_VERBOSE;
25414
- env = undefined;
25415
- }
25416
- else {
25417
- /*
25418
- TODO:
25419
- if ((options as any).commands !== undefined) {
25420
- commands = (options as any).commands;
25421
- } else {
25422
- commands = [(options as any).command];
25423
- }
25424
- */
25425
- // TODO: [1] DRY default values
25426
- command = options.command;
25427
- cwd = (_a = options.cwd) !== null && _a !== void 0 ? _a : process.cwd();
25428
- crashOnError = (_b = options.crashOnError) !== null && _b !== void 0 ? _b : true;
25429
- timeout = (_c = options.timeout) !== null && _c !== void 0 ? _c : Infinity;
25430
- isVerbose = (_d = options.isVerbose) !== null && _d !== void 0 ? _d : DEFAULT_IS_VERBOSE;
25431
- env = options.env;
25432
- }
25433
- // TODO: /(-[a-zA-Z0-9-]+\s+[^\s]*)|[^\s]*/g
25434
- const _ = Array.from(command.matchAll(/(".*")|([^\s]*)/g))
25435
- .map(([match]) => match)
25436
- .filter((arg) => arg !== '');
25437
- if (_.length > 1) {
25438
- [command, ...args] = _;
25439
- }
25440
- if (options.args) {
25441
- args = [...args, ...options.args];
25442
- }
25443
- let humanReadableCommand = !['npx', 'npm'].includes(command) ? command : args[0];
25444
- if (['ts-node'].includes(humanReadableCommand)) {
25445
- humanReadableCommand += ` ${args[1]}`;
25446
- }
25447
- if (/^win/.test(process.platform) && ['npm', 'npx'].includes(command)) {
25448
- command = `${command}.cmd`;
25449
- }
25450
- return { command, humanReadableCommand, args, cwd, crashOnError, timeout, isVerbose, env };
25451
- }
25452
- // TODO: This should show type error> execCommandNormalizeOptions({ command: '', commands: [''] });
25453
-
25454
- /**
25455
- * Run one command in a shell
25456
- *
25457
- *
25458
- * Note: There are 2 similar functions in the codebase:
25459
- * - `$execCommand` which runs a single command
25460
- * - `$execCommands` which runs multiple commands
25461
- * Note: `$` is used to indicate that this function is not a pure function - it runs a command in a shell
25462
- *
25463
- * @public exported from `@promptbook/node`
25464
- */
25465
- function $execCommand(options) {
25466
- if (!$isRunningInNode()) {
25467
- throw new EnvironmentMismatchError('Function `$execCommand` can run only in Node environment.js');
25468
- }
25469
- return new Promise((resolve, reject) => {
25470
- // eslint-disable-next-line prefer-const
25471
- const { command, humanReadableCommand, args, cwd, crashOnError, timeout, isVerbose = DEFAULT_IS_VERBOSE, env, } = $execCommandNormalizeOptions(options);
25472
- if (timeout !== Infinity) {
25473
- // TODO: In waitasecond forTime(Infinity) should be equivalent to forEver()
25474
- waitasecond.forTime(timeout).then(() => {
25475
- if (crashOnError) {
25476
- reject(new Error(`Command "${humanReadableCommand}" exceeded time limit of ${timeout}ms`));
25477
- }
25478
- else {
25479
- console.warn(`Command "${humanReadableCommand}" exceeded time limit of ${timeout}ms but continues running`);
25480
- // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
25481
- resolve('Command exceeded time limit');
25482
- }
25483
- });
25484
- }
25485
- if (isVerbose) {
25486
- console.info(colors__default["default"].yellow(cwd) + ' ' + colors__default["default"].green(command) + ' ' + colors__default["default"].blue(args.join(' ')));
25487
- }
25488
- try {
25489
- const commandProcess = child_process.spawn(command, args, {
25490
- cwd,
25491
- shell: true,
25492
- env: env ? { ...process.env, ...env } : process.env,
25493
- });
25494
- if (isVerbose) {
25495
- commandProcess.on('message', (message) => {
25496
- console.info({ message });
25497
- });
25498
- }
25499
- const output = [];
25500
- commandProcess.stdout.on('data', (stdout) => {
25501
- output.push(stdout.toString());
25502
- if (isVerbose) {
25503
- console.info(stdout.toString());
25504
- }
25505
- });
25506
- commandProcess.stderr.on('data', (stderr) => {
25507
- output.push(stderr.toString());
25508
- if (isVerbose && stderr.toString().trim()) {
25509
- console.warn(stderr.toString());
25510
- // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
25511
- }
25512
- });
25513
- const finishWithCode = (code) => {
25514
- if (code !== 0) {
25515
- if (crashOnError) {
25516
- reject(new Error(output.join('\n').trim() ||
25517
- `Command "${humanReadableCommand}" exited with code ${code}`));
25518
- }
25519
- else {
25520
- if (isVerbose) {
25521
- console.warn(`Command "${humanReadableCommand}" exited with code ${code}`);
25522
- // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
25523
- }
25524
- resolve(spaceTrim$1.spaceTrim(output.join('\n')));
25525
- }
25526
- }
25527
- else {
25528
- resolve(spaceTrim$1.spaceTrim(output.join('\n')));
25529
- }
25530
- };
25531
- commandProcess.on('close', finishWithCode);
25532
- commandProcess.on('exit', finishWithCode);
25533
- commandProcess.on('disconnect', () => {
25534
- // Note: Unexpected disconnection should always result in rejection
25535
- reject(new Error(`Command "${humanReadableCommand}" disconnected`));
25536
- });
25537
- commandProcess.on('error', (error) => {
25538
- if (crashOnError) {
25539
- reject(new Error(`Command "${humanReadableCommand}" failed: \n${error.message}`));
25540
- }
25541
- else {
25542
- if (isVerbose) {
25543
- console.warn(error);
25544
- // <- TODO: [🏮] Some standard way how to transform errors into warnings and how to handle non-critical fails during the tasks
25545
- }
25546
- resolve(spaceTrim$1.spaceTrim(output.join('\n')));
25547
- }
25548
- });
25549
- }
25550
- catch (error) {
25551
- // Note: Unexpected error in sync code should always result in rejection
25552
- reject(error);
25553
- }
25554
- });
25555
- }
25556
- /**
25557
- * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
25558
- */
25559
-
25560
- /**
25561
- * Attempts to locate the specified application on a Linux system using the 'which' command.
25562
- * Returns the path to the executable if found, or null otherwise.
25563
- *
25564
- * @private within the repository
25565
- */
25566
- async function locateAppOnLinux({ linuxWhich, }) {
25567
- try {
25568
- const result = await $execCommand({ crashOnError: true, command: `which ${linuxWhich}` });
25569
- return result.trim();
25570
- }
25571
- catch (error) {
25572
- assertsError(error);
25573
- return null;
25574
- }
25575
- }
26768
+ new GoalCommitmentDefinition('GOAL'),
26769
+ new GoalCommitmentDefinition('GOALS'),
26770
+ new InitialMessageCommitmentDefinition(),
26771
+ new UserMessageCommitmentDefinition(),
26772
+ new InternalMessageCommitmentDefinition(),
26773
+ new AgentMessageCommitmentDefinition(),
26774
+ new MessageSuffixCommitmentDefinition(),
26775
+ new MessageCommitmentDefinition('MESSAGE'),
26776
+ new MessageCommitmentDefinition('MESSAGES'),
26777
+ new ScenarioCommitmentDefinition('SCENARIO'),
26778
+ new ScenarioCommitmentDefinition('SCENARIOS'),
26779
+ new DeleteCommitmentDefinition('DELETE'),
26780
+ new DeleteCommitmentDefinition('CANCEL'),
26781
+ new DeleteCommitmentDefinition('DISCARD'),
26782
+ new DeleteCommitmentDefinition('REMOVE'),
26783
+ new DictionaryCommitmentDefinition(),
26784
+ new OpenCommitmentDefinition(),
26785
+ new ClosedCommitmentDefinition(),
26786
+ new TeamCommitmentDefinition(),
26787
+ new UseBrowserCommitmentDefinition(),
26788
+ new UseSearchEngineCommitmentDefinition(),
26789
+ new UseSpawnCommitmentDefinition(),
26790
+ new UseTimeoutCommitmentDefinition(),
26791
+ new UseTimeCommitmentDefinition(),
26792
+ new UseUserLocationCommitmentDefinition(),
26793
+ new UseCalendarCommitmentDefinition(),
26794
+ new UseEmailCommitmentDefinition(),
26795
+ new UsePopupCommitmentDefinition(),
26796
+ new UseImageGeneratorCommitmentDefinition('USE IMAGE GENERATOR'),
26797
+ new UseMcpCommitmentDefinition(),
26798
+ new UsePrivacyCommitmentDefinition(),
26799
+ new UseProjectCommitmentDefinition(),
26800
+ new UseCommitmentDefinition(),
26801
+ // Not yet implemented commitments (using placeholder)
26802
+ new NotYetImplementedCommitmentDefinition('EXPECT'),
26803
+ new NotYetImplementedCommitmentDefinition('BEHAVIOUR'),
26804
+ new NotYetImplementedCommitmentDefinition('BEHAVIOURS'),
26805
+ new NotYetImplementedCommitmentDefinition('AVOID'),
26806
+ new NotYetImplementedCommitmentDefinition('AVOIDANCE'),
26807
+ new NotYetImplementedCommitmentDefinition('CONTEXT'),
26808
+ // <- TODO: Prompt: Leverage aliases instead of duplicating commitment definitions
26809
+ ];
25576
26810
  /**
25577
- * TODO: [🧠][♿] Maybe export through `@promptbook/node`
25578
- * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
26811
+ * TODO: [🧠] Maybe create through standardized $register
26812
+ * Note: [💞] Ignore a discrepancy between file name and entity name
25579
26813
  */
25580
26814
 
25581
26815
  /**
25582
- * Checks if the file is executable
26816
+ * Gets all available commitment definitions
26817
+ * @returns Array of all commitment definitions
25583
26818
  *
25584
- * @private within the repository
26819
+ * @public exported from `@promptbook/core`
25585
26820
  */
25586
- async function isExecutable(path, fs) {
25587
- try {
25588
- await fs.access(path, fs.constants.X_OK);
25589
- return true;
25590
- }
25591
- catch (error) {
25592
- return false;
25593
- }
26821
+ function getAllCommitmentDefinitions() {
26822
+ return $deepFreeze([...COMMITMENT_REGISTRY]);
25594
26823
  }
25595
- /**
25596
- * Note: Not [~🟢~] because it is not directly dependent on `fs
25597
- * TODO: [🖇] What about symlinks?
25598
- */
25599
26824
 
25600
- // Note: Module `userhome` has no types available, so it is imported using `require`
25601
- // @see https://stackoverflow.com/questions/37000981/how-to-import-node-module-in-typescript-without-type-definitions
25602
- // eslint-disable-next-line @typescript-eslint/no-var-requires
25603
- const userhome = require('userhome');
25604
26825
  /**
25605
- * Attempts to locate the specified application on a macOS system by checking standard application paths and using mdfind.
25606
- * Returns the path to the executable if found, or null otherwise.
26826
+ * Collects tool functions from all commitment definitions.
25607
26827
  *
25608
- * @private within the repository
26828
+ * @returns Map of tool function implementations.
26829
+ * @private internal helper for commitment tool registry
25609
26830
  */
25610
- async function locateAppOnMacOs({ macOsName, }) {
25611
- try {
25612
- const toExec = `/Contents/MacOS/${macOsName}`;
25613
- const regPath = `/Applications/${macOsName}.app` + toExec;
25614
- const altPath = userhome(regPath.slice(1));
25615
- if (await isExecutable(regPath, $provideFilesystemForNode())) {
25616
- return regPath;
25617
- }
25618
- else if (await isExecutable(altPath, $provideFilesystemForNode())) {
25619
- return altPath;
26831
+ function collectCommitmentToolFunctions() {
26832
+ const allToolFunctions = {};
26833
+ for (const commitmentDefinition of getAllCommitmentDefinitions()) {
26834
+ const toolFunctions = commitmentDefinition.getToolFunctions();
26835
+ for (const [funcName, funcImpl] of Object.entries(toolFunctions)) {
26836
+ if (allToolFunctions[funcName] !== undefined &&
26837
+ just(false) /* <- Note: [??] How to deal with commitment aliases */) {
26838
+ throw new UnexpectedError(`Duplicate tool function name detected: \`${funcName}\` provided by commitment \`${commitmentDefinition.type}\``);
26839
+ }
26840
+ allToolFunctions[funcName] = funcImpl;
25620
26841
  }
25621
- const result = await $execCommand({
25622
- crashOnError: true,
25623
- command: `mdfind 'kMDItemDisplayName == "${macOsName}" && kMDItemKind == Application'`,
25624
- });
25625
- return result.trim() + toExec;
25626
- }
25627
- catch (error) {
25628
- assertsError(error);
25629
- return null;
25630
26842
  }
26843
+ return allToolFunctions;
25631
26844
  }
25632
26845
  /**
25633
- * TODO: [🧠][♿] Maybe export through `@promptbook/node`
25634
- * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
25635
- */
25636
-
25637
- /**
25638
- * Attempts to locate the specified application on a Windows system by searching common installation directories.
25639
- * Returns the path to the executable if found, or null otherwise.
26846
+ * Creates a proxy that resolves tool functions on demand.
25640
26847
  *
25641
- * @private within the repository
26848
+ * @param getFunctions - Provider of current tool functions.
26849
+ * @returns Proxy exposing tool functions as properties.
26850
+ * @private internal helper for commitment tool registry
25642
26851
  */
25643
- async function locateAppOnWindows({ appName, windowsSuffix, }) {
25644
- try {
25645
- const prefixes = [
25646
- process.env.LOCALAPPDATA,
25647
- path.join(process.env.LOCALAPPDATA || '', 'Programs'),
25648
- process.env.PROGRAMFILES,
25649
- process.env['PROGRAMFILES(X86)'],
25650
- ];
25651
- for (const prefix of prefixes) {
25652
- const path = prefix + windowsSuffix;
25653
- if (await isExecutable(path, $provideFilesystemForNode())) {
25654
- return path;
26852
+ function createToolFunctionsProxy(getFunctions) {
26853
+ const resolveFunctions = () => getFunctions();
26854
+ return new Proxy({}, {
26855
+ get(_target, prop) {
26856
+ if (typeof prop !== 'string') {
26857
+ return undefined;
25655
26858
  }
25656
- }
25657
- throw new Error(`Can not locate app ${appName} on Windows.`);
25658
- }
25659
- catch (error) {
25660
- assertsError(error);
25661
- return null;
25662
- }
26859
+ return resolveFunctions()[prop];
26860
+ },
26861
+ has(_target, prop) {
26862
+ if (typeof prop !== 'string') {
26863
+ return false;
26864
+ }
26865
+ return prop in resolveFunctions();
26866
+ },
26867
+ ownKeys() {
26868
+ return Object.keys(resolveFunctions());
26869
+ },
26870
+ getOwnPropertyDescriptor(_target, prop) {
26871
+ if (typeof prop !== 'string') {
26872
+ return undefined;
26873
+ }
26874
+ const value = resolveFunctions()[prop];
26875
+ if (value === undefined) {
26876
+ return undefined;
26877
+ }
26878
+ return {
26879
+ enumerable: true,
26880
+ configurable: true,
26881
+ writable: false,
26882
+ value,
26883
+ };
26884
+ },
26885
+ });
25663
26886
  }
25664
26887
  /**
25665
- * TODO: [🧠][♿] Maybe export through `@promptbook/node`
25666
- * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
26888
+ * Note: [💞] Ignore a discrepancy between file name and entity name
25667
26889
  */
25668
26890
 
26891
+ const nodeToolFunctions = {
26892
+ /**
26893
+ * @@@
26894
+ *
26895
+ * Note: [??] This function has implementation both for browser and node, this is the full one for node
26896
+ */
26897
+ async fetch_url_content(args) {
26898
+ console.log('!!!! [Tool] fetch_url_content called', { args });
26899
+ const { url } = args;
26900
+ return await fetchUrlContent(url);
26901
+ },
26902
+ /**
26903
+ * @@@
26904
+ *
26905
+ * Note: [??] This function has implementation both for browser and node, this is the server one for node
26906
+ */
26907
+ run_browser: resolveRunBrowserToolForNode(),
26908
+ /**
26909
+ * @@@
26910
+ *
26911
+ * Note: [??] This function has implementation both for browser and node, this is the server one for node
26912
+ */
26913
+ send_email: resolveSendEmailToolForNode(),
26914
+ /**
26915
+ * @@@
26916
+ *
26917
+ * Note: [??] This function has implementation both for browser and node, this is the server one for node
26918
+ */
26919
+ spawn_agent: resolveSpawnAgentToolForNode(),
26920
+ // TODO: !!!! Unhardcode, make proper server function register from definitions
26921
+ };
26922
+ const nodeToolFunctionsProxy = createToolFunctionsProxy(() => ({
26923
+ ...collectCommitmentToolFunctions(),
26924
+ ...nodeToolFunctions,
26925
+ }));
25669
26926
  /**
25670
- * Locates an application on the system
26927
+ * Gets all function implementations provided by all commitments
25671
26928
  *
25672
- * @private within the repository
26929
+ * Note: This function is intended for server use, there is also equivalent `getAllCommitmentsToolFunctionsForBrowser` for browser use
26930
+ *
26931
+ * @public exported from `@promptbook/node`
25673
26932
  */
25674
- function locateApp(options) {
26933
+ function getAllCommitmentsToolFunctionsForNode() {
25675
26934
  if (!$isRunningInNode()) {
25676
- throw new EnvironmentMismatchError('Locating apps works only in Node.js environment');
25677
- }
25678
- const { appName, linuxWhich, windowsSuffix, macOsName } = options;
25679
- if (process.platform === 'win32') {
25680
- if (windowsSuffix) {
25681
- return locateAppOnWindows({ appName, windowsSuffix });
25682
- }
25683
- else {
25684
- throw new Error(`${appName} is not available on Windows.`);
25685
- }
25686
- }
25687
- else if (process.platform === 'darwin') {
25688
- if (macOsName) {
25689
- return locateAppOnMacOs({ macOsName });
25690
- }
25691
- else {
25692
- throw new Error(`${appName} is not available on macOS.`);
25693
- }
25694
- }
25695
- else {
25696
- if (linuxWhich) {
25697
- return locateAppOnLinux({ linuxWhich });
25698
- }
25699
- else {
25700
- throw new Error(`${appName} is not available on Linux.`);
25701
- }
26935
+ throw new EnvironmentMismatchError(spaceTrim(`
26936
+ Function getAllCommitmentsToolFunctionsForNode should be run in Node.js environment.
26937
+
26938
+ - In browser use getAllCommitmentsToolFunctionsForBrowser instead.
26939
+ - This function can include server-only tools which cannot run in browser environment.
26940
+
26941
+ `));
25702
26942
  }
26943
+ return nodeToolFunctionsProxy;
25703
26944
  }
25704
26945
  /**
25705
- * TODO: [🧠][♿] Maybe export through `@promptbook/node`
25706
- * Note: [🟢] Code in this file should never be never released in packages that could be imported into browser environment
26946
+ * Note: [??] Code in this file should never be never released in packages that could be imported into browser environment
26947
+ * TODO: [??] Unite `xxxForServer` and `xxxForNode` naming
25707
26948
  */
25708
26949
 
25709
26950
  /**
@@ -27768,6 +29009,14 @@
27768
29009
  });
27769
29010
  continue;
27770
29011
  }
29012
+ if (commitment.type === 'USE CALENDAR') {
29013
+ capabilities.push({
29014
+ type: 'calendar',
29015
+ label: 'Calendar',
29016
+ iconName: 'Calendar',
29017
+ });
29018
+ continue;
29019
+ }
27771
29020
  if (commitment.type === 'FROM') {
27772
29021
  const content = spaceTrim__default["default"](commitment.content).split(/\r?\n/)[0] || '';
27773
29022
  if (content === 'Adam' || content === '' /* <- Note: Adam is implicit */) {