@realtimex/sdk 2.0.30 → 2.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,5 +19,29 @@ const workspaces = await client.request("listWorkspaces");
19
19
  `REALTIMEX_BASE_URL`. Desktop and `dev:all` runtimes propagate the resolved
20
20
  dynamic `/cli` endpoint; the SDK does not guess a localhost port.
21
21
 
22
+ ## Public webhook task delivery
23
+
24
+ Use a separate webhook client to trigger an existing public webhook endpoint.
25
+ Webhook endpoint creation and editing remain in RealtimeX settings.
26
+
27
+ ```js
28
+ const { createRealtimeXWebhookClient } = require("@realtimex/sdk");
29
+
30
+ const webhook = createRealtimeXWebhookClient({
31
+ endpointUrl: process.env.REALTIMEX_WEBHOOK_URL,
32
+ secret: process.env.REALTIMEX_WEBHOOK_SECRET,
33
+ });
34
+
35
+ const receipt = await webhook.trigger(
36
+ { prompt: "Review this task", context: { sourceId: "task-123" } },
37
+ { deliveryId: "task-123" }
38
+ );
39
+ ```
40
+
41
+ The client signs the exact UTF-8 request bytes with HMAC-SHA256. Network and
42
+ server-error retries reuse the serialized body, timestamp, and delivery ID so
43
+ the receiving endpoint can deduplicate delivery safely. A successful receipt
44
+ confirms acceptance and routing, not agent-task completion.
45
+
22
46
  The package also publishes generated skill assets under
23
47
  `skills/realtimex-moderator-sdk`.
package/index.d.ts CHANGED
@@ -423,6 +423,25 @@ export interface GetWorkspaceOptions extends RealtimeXRequestOptions {
423
423
  body?: GetWorkspaceBody;
424
424
  }
425
425
 
426
+ export type GoalKickoffComposerParams = Record<string, never>;
427
+ export type GoalKickoffComposerQuery = Record<string, never>;
428
+ export type GoalKickoffComposerHeaders = Record<string, never>;
429
+ export type GoalKickoffComposerBody = {
430
+ cdpUrl?: string;
431
+ mainAppUrl?: string;
432
+ artifactPath?: string;
433
+ screenshotDir?: string;
434
+ timeoutMs?: number;
435
+ confirmDestructive?: boolean;
436
+ };
437
+ export type GoalKickoffComposerResponse = Record<string, unknown>;
438
+ export interface GoalKickoffComposerOptions extends RealtimeXRequestOptions {
439
+ params?: GoalKickoffComposerParams;
440
+ query?: GoalKickoffComposerQuery;
441
+ headers?: GoalKickoffComposerHeaders & Record<string, string>;
442
+ body?: GoalKickoffComposerBody;
443
+ }
444
+
426
445
  export type InstallPluginParams = Record<string, never>;
427
446
  export type InstallPluginQuery = Record<string, never>;
428
447
  export type InstallPluginHeaders = {
@@ -826,7 +845,7 @@ export type RespondTerminalIdlePromptHeaders = {
826
845
  "x-app-id"?: string;
827
846
  };
828
847
  export type RespondTerminalIdlePromptBody = {
829
- responseAction: "confirm-selected-option" | "cancel-prompt";
848
+ responseAction: "confirm-selected-option" | "confirm-persistent-workspace-trust" | "cancel-prompt";
830
849
  expectedEventSeq: number;
831
850
  dispatchKey: string;
832
851
  };
@@ -1457,6 +1476,47 @@ export interface RealtimeXRequestOptions {
1457
1476
  signal?: AbortSignal;
1458
1477
  }
1459
1478
 
1479
+ export interface RealtimeXWebhookClientOptions {
1480
+ endpointUrl?: string;
1481
+ url?: string;
1482
+ secret?: string;
1483
+ signatureHeader?: string;
1484
+ signaturePrefix?: string;
1485
+ timestampHeader?: string;
1486
+ deliveryIdHeader?: string;
1487
+ eventTypeHeader?: string;
1488
+ sourceHeader?: string;
1489
+ eventType?: string;
1490
+ source?: string;
1491
+ headers?: Record<string, string>;
1492
+ maxRetries?: number;
1493
+ retryDelayMs?: number;
1494
+ timeoutMs?: number;
1495
+ fetch?: typeof fetch;
1496
+ }
1497
+
1498
+ export interface RealtimeXWebhookTriggerOptions {
1499
+ deliveryId?: string;
1500
+ timestamp?: string | number;
1501
+ eventType?: string;
1502
+ source?: string;
1503
+ headers?: Record<string, string>;
1504
+ maxRetries?: number;
1505
+ retryDelayMs?: number;
1506
+ timeoutMs?: number;
1507
+ signal?: AbortSignal;
1508
+ }
1509
+
1510
+ export interface RealtimeXWebhookReceipt {
1511
+ success: boolean;
1512
+ accepted: boolean;
1513
+ duplicate: boolean;
1514
+ deliveryId: string;
1515
+ threadId: string | number | null;
1516
+ taskId: string | number | null;
1517
+ [key: string]: unknown;
1518
+ }
1519
+
1460
1520
  export declare class RealtimeXApiError extends Error {
1461
1521
  status?: number;
1462
1522
  statusText?: string;
@@ -1464,6 +1524,21 @@ export declare class RealtimeXApiError extends Error {
1464
1524
  response: Response;
1465
1525
  }
1466
1526
 
1527
+ export declare class RealtimeXWebhookError extends Error {
1528
+ status?: number;
1529
+ statusText?: string;
1530
+ body: unknown;
1531
+ response: Response | null;
1532
+ }
1533
+
1534
+ export declare class RealtimeXWebhookClient {
1535
+ constructor(options?: RealtimeXWebhookClientOptions);
1536
+ trigger(
1537
+ payload: unknown,
1538
+ options?: RealtimeXWebhookTriggerOptions
1539
+ ): Promise<RealtimeXWebhookReceipt>;
1540
+ }
1541
+
1467
1542
  export declare class RealtimeXClient {
1468
1543
  constructor(options?: RealtimeXClientOptions);
1469
1544
  request(operationName: string, options?: RealtimeXRequestOptions): Promise<unknown>;
@@ -1533,6 +1608,9 @@ export declare class RealtimeXClient {
1533
1608
  /** Get one workspace by exact workspace slug. */
1534
1609
  getWorkspace(options?: GetWorkspaceOptions): Promise<GetWorkspaceResponse>;
1535
1610
  getWorkspace(workspaceSlug: string, options?: Omit<GetWorkspaceOptions, 'params'>): Promise<GetWorkspaceResponse>;
1611
+ /** Verify the shared Project/global Goal kickoff composer, routing, attachment authorization, retry, preview, and responsive layouts. */
1612
+ goalKickoffComposer(options?: GoalKickoffComposerOptions): Promise<GoalKickoffComposerResponse>;
1613
+ goalKickoffComposer(body: GoalKickoffComposerBody, options?: Omit<GoalKickoffComposerOptions, 'body'>): Promise<GoalKickoffComposerResponse>;
1536
1614
  /** Install a plugin from a local source directory on the same machine as the server. Pass an absolute path containing realtimex.plugin.json. Pass force=true to replace an already-installed version; its saved configuration (including secrets) is preserved. */
1537
1615
  installPlugin(options?: InstallPluginOptions): Promise<InstallPluginResponse>;
1538
1616
  installPlugin(body: InstallPluginBody, options?: Omit<InstallPluginOptions, 'body'>): Promise<InstallPluginResponse>;
@@ -1689,6 +1767,9 @@ export declare class RealtimeXClient {
1689
1767
  export declare function createRealtimeXClient(
1690
1768
  options?: RealtimeXClientOptions
1691
1769
  ): RealtimeXClient;
1770
+ export declare function createRealtimeXWebhookClient(
1771
+ options?: RealtimeXWebhookClientOptions
1772
+ ): RealtimeXWebhookClient;
1692
1773
  export declare const createClient: typeof createRealtimeXClient;
1693
1774
  export declare const operations: Record<
1694
1775
  string,
package/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('node:crypto');
4
+
3
5
  const operations = {
4
6
  "approveChannelPairingCode": {
5
7
  "method": "POST",
@@ -268,6 +270,14 @@ const operations = {
268
270
  ],
269
271
  "hasBody": false
270
272
  },
273
+ "goalKickoffComposer": {
274
+ "method": "POST",
275
+ "path": "/goal-kickoff-composer",
276
+ "pathParams": [],
277
+ "queryParams": [],
278
+ "headerParams": [],
279
+ "hasBody": true
280
+ },
271
281
  "installPlugin": {
272
282
  "method": "POST",
273
283
  "path": "/install-plugin",
@@ -876,6 +886,17 @@ class RealtimeXApiError extends Error {
876
886
  }
877
887
  }
878
888
 
889
+ class RealtimeXWebhookError extends Error {
890
+ constructor(message, response = null, body = null, cause = null) {
891
+ super(message, cause ? { cause } : undefined);
892
+ this.name = 'RealtimeXWebhookError';
893
+ this.status = response && response.status;
894
+ this.statusText = response && response.statusText;
895
+ this.body = body;
896
+ this.response = response;
897
+ }
898
+ }
899
+
879
900
  function joinUrl(baseUrl, pathname) {
880
901
  const base = String(baseUrl || '').replace(/\/+$/g, '');
881
902
  const path = String(pathname || '').replace(/^\/+/, '');
@@ -929,6 +950,220 @@ async function parseResponseBody(response) {
929
950
  }
930
951
  }
931
952
 
953
+ function normalizeWebhookEndpointUrl(value) {
954
+ const normalized = String(value || '').trim();
955
+ if (!normalized) {
956
+ throw new Error(
957
+ 'RealtimeX webhook URL is unavailable; pass { endpointUrl } or set REALTIMEX_WEBHOOK_URL.'
958
+ );
959
+ }
960
+ const parsed = new URL(normalized);
961
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
962
+ throw new Error('RealtimeX webhook URL must use http or https.');
963
+ }
964
+ if (parsed.username || parsed.password) {
965
+ throw new Error('RealtimeX webhook URL must not contain embedded credentials.');
966
+ }
967
+ return parsed.toString();
968
+ }
969
+
970
+ function normalizeRetryCount(value, fallback = 2) {
971
+ const parsed = Number(value);
972
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 10) return fallback;
973
+ return parsed;
974
+ }
975
+
976
+ function normalizeTimeout(value, fallback = 15000) {
977
+ const parsed = Number(value);
978
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
979
+ return Math.min(Math.floor(parsed), 120000);
980
+ }
981
+
982
+ function waitForRetry(delayMs, signal) {
983
+ const delay = Math.max(0, Number(delayMs) || 0);
984
+ if (delay === 0) return Promise.resolve();
985
+ return new Promise((resolve, reject) => {
986
+ const finish = () => {
987
+ if (signal) signal.removeEventListener('abort', abort);
988
+ resolve();
989
+ };
990
+ const timeout = setTimeout(finish, delay);
991
+ const abort = () => {
992
+ clearTimeout(timeout);
993
+ signal.removeEventListener('abort', abort);
994
+ reject(signal.reason || new Error('Webhook request aborted.'));
995
+ };
996
+ if (signal) {
997
+ if (signal.aborted) return abort();
998
+ signal.addEventListener('abort', abort, { once: true });
999
+ }
1000
+ });
1001
+ }
1002
+
1003
+ function createAttemptSignal(externalSignal, timeoutMs) {
1004
+ const controller = new AbortController();
1005
+ const abortFromExternal = () => {
1006
+ controller.abort(externalSignal.reason || new Error('Webhook request aborted.'));
1007
+ };
1008
+ if (externalSignal) {
1009
+ if (externalSignal.aborted) abortFromExternal();
1010
+ else externalSignal.addEventListener('abort', abortFromExternal, { once: true });
1011
+ }
1012
+ const timeout = setTimeout(
1013
+ () => controller.abort(new Error('Webhook request timed out.')),
1014
+ timeoutMs
1015
+ );
1016
+ return {
1017
+ signal: controller.signal,
1018
+ cleanup() {
1019
+ clearTimeout(timeout);
1020
+ if (externalSignal) {
1021
+ externalSignal.removeEventListener('abort', abortFromExternal);
1022
+ }
1023
+ },
1024
+ };
1025
+ }
1026
+
1027
+ class RealtimeXWebhookClient {
1028
+ constructor(options = {}) {
1029
+ const env = typeof process !== 'undefined' ? process.env || {} : {};
1030
+ this.endpointUrl = normalizeWebhookEndpointUrl(
1031
+ options.endpointUrl || options.url || env.REALTIMEX_WEBHOOK_URL
1032
+ );
1033
+ this.secret = String(options.secret || env.REALTIMEX_WEBHOOK_SECRET || '');
1034
+ if (!this.secret) {
1035
+ throw new Error(
1036
+ 'RealtimeX webhook secret is unavailable; pass { secret } or set REALTIMEX_WEBHOOK_SECRET.'
1037
+ );
1038
+ }
1039
+ this.fetch = options.fetch || globalThis.fetch;
1040
+ if (typeof this.fetch !== 'function') {
1041
+ throw new Error('A fetch implementation is required. Use Node 18+ or pass { fetch }.');
1042
+ }
1043
+ this.signatureHeader =
1044
+ options.signatureHeader ||
1045
+ env.REALTIMEX_WEBHOOK_SIGNATURE_HEADER ||
1046
+ 'X-Webhook-Signature-256';
1047
+ this.signaturePrefix =
1048
+ options.signaturePrefix ?? env.REALTIMEX_WEBHOOK_SIGNATURE_PREFIX ?? 'sha256=';
1049
+ this.timestampHeader =
1050
+ options.timestampHeader ||
1051
+ env.REALTIMEX_WEBHOOK_TIMESTAMP_HEADER ||
1052
+ 'X-Webhook-Timestamp';
1053
+ this.deliveryIdHeader =
1054
+ options.deliveryIdHeader ||
1055
+ env.REALTIMEX_WEBHOOK_DELIVERY_ID_HEADER ||
1056
+ 'X-Webhook-Id';
1057
+ this.eventTypeHeader =
1058
+ options.eventTypeHeader ||
1059
+ env.REALTIMEX_WEBHOOK_EVENT_TYPE_HEADER ||
1060
+ 'X-Webhook-Event';
1061
+ this.sourceHeader =
1062
+ options.sourceHeader ||
1063
+ env.REALTIMEX_WEBHOOK_SOURCE_HEADER ||
1064
+ 'X-Webhook-Source';
1065
+ this.defaultEventType =
1066
+ options.eventType || env.REALTIMEX_WEBHOOK_EVENT_TYPE || 'realtimex.task';
1067
+ this.defaultSource =
1068
+ options.source || env.REALTIMEX_WEBHOOK_SOURCE || 'local-app';
1069
+ this.defaultHeaders = { ...(options.headers || {}) };
1070
+ this.maxRetries = normalizeRetryCount(options.maxRetries, 2);
1071
+ const configuredRetryDelay = Number(options.retryDelayMs);
1072
+ this.retryDelayMs = Number.isFinite(configuredRetryDelay) && configuredRetryDelay >= 0
1073
+ ? configuredRetryDelay
1074
+ : 250;
1075
+ this.timeoutMs = normalizeTimeout(options.timeoutMs, 15000);
1076
+ }
1077
+
1078
+ async trigger(payload, options = {}) {
1079
+ if (payload === undefined) {
1080
+ throw new Error('RealtimeX webhook payload is required.');
1081
+ }
1082
+ const serialized = typeof payload === 'string' ? payload : JSON.stringify(payload);
1083
+ if (serialized === undefined) {
1084
+ throw new Error('RealtimeX webhook payload is not JSON serializable.');
1085
+ }
1086
+ const body = Buffer.from(serialized, 'utf8');
1087
+ const deliveryId = String(options.deliveryId || crypto.randomUUID());
1088
+ const timestamp = String(
1089
+ options.timestamp || Math.floor(Date.now() / 1000)
1090
+ );
1091
+ const signature = crypto
1092
+ .createHmac('sha256', this.secret)
1093
+ .update(body)
1094
+ .digest('hex');
1095
+ const headers = {
1096
+ ...this.defaultHeaders,
1097
+ ...(options.headers || {}),
1098
+ 'content-type': 'application/json',
1099
+ [this.signatureHeader]: String(this.signaturePrefix || '') + signature,
1100
+ [this.timestampHeader]: timestamp,
1101
+ [this.deliveryIdHeader]: deliveryId,
1102
+ [this.eventTypeHeader]: String(options.eventType || this.defaultEventType),
1103
+ [this.sourceHeader]: String(options.source || this.defaultSource),
1104
+ };
1105
+ const maxRetries = normalizeRetryCount(options.maxRetries, this.maxRetries);
1106
+ const retryDelayMs =
1107
+ options.retryDelayMs === undefined
1108
+ ? this.retryDelayMs
1109
+ : Math.max(0, Number(options.retryDelayMs) || 0);
1110
+ const timeoutMs = normalizeTimeout(options.timeoutMs, this.timeoutMs);
1111
+
1112
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
1113
+ const attemptSignal = createAttemptSignal(options.signal, timeoutMs);
1114
+ try {
1115
+ const response = await this.fetch(this.endpointUrl, {
1116
+ method: 'POST',
1117
+ headers,
1118
+ body,
1119
+ signal: attemptSignal.signal,
1120
+ });
1121
+ const responseBody = await parseResponseBody(response);
1122
+ if (response.status >= 500 && attempt < maxRetries) {
1123
+ await waitForRetry(retryDelayMs * (attempt + 1), options.signal);
1124
+ continue;
1125
+ }
1126
+ if (!response.ok) {
1127
+ throw new RealtimeXWebhookError(
1128
+ 'RealtimeX webhook request failed: ' +
1129
+ response.status +
1130
+ ' ' +
1131
+ response.statusText,
1132
+ response,
1133
+ responseBody
1134
+ );
1135
+ }
1136
+ return responseBody;
1137
+ } catch (error) {
1138
+ if (error instanceof RealtimeXWebhookError) throw error;
1139
+ if (options.signal && options.signal.aborted) {
1140
+ throw new RealtimeXWebhookError(
1141
+ 'RealtimeX webhook request aborted.',
1142
+ null,
1143
+ null,
1144
+ error
1145
+ );
1146
+ }
1147
+ if (attempt >= maxRetries) {
1148
+ const timedOut = attemptSignal.signal.aborted;
1149
+ throw new RealtimeXWebhookError(
1150
+ timedOut
1151
+ ? 'RealtimeX webhook request timed out.'
1152
+ : 'RealtimeX webhook request failed: ' + (error.message || String(error)),
1153
+ null,
1154
+ null,
1155
+ error
1156
+ );
1157
+ }
1158
+ await waitForRetry(retryDelayMs * (attempt + 1), options.signal);
1159
+ } finally {
1160
+ attemptSignal.cleanup();
1161
+ }
1162
+ }
1163
+ throw new RealtimeXWebhookError('RealtimeX webhook request failed.');
1164
+ }
1165
+ }
1166
+
932
1167
  function isPlainObject(value) {
933
1168
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
934
1169
  }
@@ -1217,6 +1452,13 @@ class RealtimeXClient {
1217
1452
  );
1218
1453
  }
1219
1454
 
1455
+ goalKickoffComposer(...args) {
1456
+ return this.request(
1457
+ "goalKickoffComposer",
1458
+ normalizeOperationOptions(operations["goalKickoffComposer"], args)
1459
+ );
1460
+ }
1461
+
1220
1462
  installPlugin(...args) {
1221
1463
  return this.request(
1222
1464
  "installPlugin",
@@ -1593,11 +1835,18 @@ function createRealtimeXClient(options = {}) {
1593
1835
  return new RealtimeXClient(options);
1594
1836
  }
1595
1837
 
1838
+ function createRealtimeXWebhookClient(options = {}) {
1839
+ return new RealtimeXWebhookClient(options);
1840
+ }
1841
+
1596
1842
  module.exports = {
1597
1843
  RealtimeXApiError,
1598
1844
  RealtimeXClient,
1845
+ RealtimeXWebhookClient,
1846
+ RealtimeXWebhookError,
1599
1847
  createClient: createRealtimeXClient,
1600
1848
  createRealtimeXClient,
1849
+ createRealtimeXWebhookClient,
1601
1850
  operations,
1602
- version: "2.0.30",
1851
+ version: "2.0.31",
1603
1852
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realtimex/sdk",
3
- "version": "2.0.30",
3
+ "version": "2.0.31",
4
4
  "description": "RealtimeX runtime SDK package and moderator agent skill assets.",
5
5
  "private": false,
6
6
  "main": "./index.js",
@@ -20,15 +20,15 @@ This skill drives the `realtimex-pp-cli` binary. Verify the CLI is installed and
20
20
 
21
21
  1. Install or reinstall the pinned version via npm:
22
22
  ```bash
23
- npm install -g @realtimex/pp-cli@2.0.30
23
+ npm install -g @realtimex/pp-cli@2.0.31
24
24
  ```
25
25
  2. Verify the exact version:
26
26
  ```bash
27
27
  realtimex-pp-cli --version
28
28
  ```
29
- The output must be `realtimex-pp-cli 2.0.30`.
29
+ The output must be `realtimex-pp-cli 2.0.31`.
30
30
 
31
- If `--version` reports "command not found" after install, the npm global bin directory is not on `$PATH`. If it reports any version other than `2.0.30`, reinstall with the pinned npm command above. Do not proceed with skill commands until exact-version verification succeeds.
31
+ If `--version` reports "command not found" after install, the npm global bin directory is not on `$PATH`. If it reports any version other than `2.0.31`, reinstall with the pinned npm command above. Do not proceed with skill commands until exact-version verification succeeds.
32
32
 
33
33
  * Always run first:
34
34
 
@@ -126,6 +126,10 @@ If `--version` reports "command not found" after install, the npm global bin dir
126
126
 
127
127
  - `realtimex-pp-cli get-workspace <workspaceSlug>` — Get one workspace by exact workspace slug.
128
128
 
129
+ **goal-kickoff-composer** — Manage goal kickoff composer
130
+
131
+ - `realtimex-pp-cli goal-kickoff-composer` — Verify the shared Project/global Goal kickoff composer, routing, attachment authorization, retry, preview
132
+
129
133
  **install-plugin** — Manage install plugin
130
134
 
131
135
  - `realtimex-pp-cli install-plugin` — Install a plugin from a local source directory on the same machine as the server.
@@ -460,9 +464,9 @@ Verify: `claude mcp list`
460
464
  ```bash
461
465
  realtimex-pp-cli --version
462
466
  ```
463
- If the command is missing or the output is not exactly `realtimex-pp-cli 2.0.30`, reinstall the pinned version:
467
+ If the command is missing or the output is not exactly `realtimex-pp-cli 2.0.31`, reinstall the pinned version:
464
468
  ```bash
465
- npm install -g @realtimex/pp-cli@2.0.30
469
+ npm install -g @realtimex/pp-cli@2.0.31
466
470
  ```
467
471
  Then run `realtimex-pp-cli --version` again and proceed only after exact-version verification succeeds.
468
472
  2. Match the user query to the best command from the Unique Capabilities and Command Reference above.