@base44-preview/sdk 0.7.0-dev.e78162e → 0.7.0-pr.26.0b8f663

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/dist/client.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { AgentsModuleConfig } from "./modules/agents.js";
1
2
  /**
2
3
  * Create a Base44 client instance
3
4
  * @param {Object} config - Client configuration
@@ -6,6 +7,7 @@
6
7
  * @param {string} [config.token] - Authentication token
7
8
  * @param {string} [config.serviceToken] - Service role authentication token
8
9
  * @param {boolean} [config.requiresAuth=false] - Whether the app requires authentication
10
+ * @param {AgentsModuleConfig} [config.agents] - Configuration for agents module
9
11
  * @returns {Object} Base44 client instance
10
12
  */
11
13
  export declare function createClient(config: {
@@ -14,6 +16,7 @@ export declare function createClient(config: {
14
16
  token?: string;
15
17
  serviceToken?: string;
16
18
  requiresAuth?: boolean;
19
+ agents?: AgentsModuleConfig;
17
20
  }): {
18
21
  /**
19
22
  * Set authentication token for all requests
@@ -38,6 +41,21 @@ export declare function createClient(config: {
38
41
  functions: {
39
42
  invoke(functionName: string, data: Record<string, any>): Promise<import("axios").AxiosResponse<any, any>>;
40
43
  };
44
+ agents: {
45
+ listConversations(filterParams?: import("./modules/agents.js").FilterParams): Promise<import("./modules/agents.js").AgentConversation[]>;
46
+ getConversation(conversationId: string): Promise<import("./modules/agents.js").AgentConversation>;
47
+ createConversation(payload: import("./modules/agents.js").CreateConversationPayload): Promise<import("./modules/agents.js").AgentConversation>;
48
+ updateConversation(conversationId: string, payload: import("./modules/agents.js").UpdateConversationPayload): Promise<import("./modules/agents.js").AgentConversation>;
49
+ sendMessage(conversationId: string, message: Omit<import("./modules/agents.js").Message, "id">): Promise<import("./modules/agents.js").Message>;
50
+ deleteMessage(conversationId: string, messageId: string): Promise<void>;
51
+ subscribeToConversation(conversationId: string, onUpdate: (conversation: import("./modules/agents.js").AgentConversation) => void): () => void;
52
+ getWebSocketStatus(): {
53
+ enabled: boolean;
54
+ connected: boolean;
55
+ };
56
+ connectWebSocket(): Promise<void>;
57
+ disconnectWebSocket(): void;
58
+ };
41
59
  };
42
60
  entities: {};
43
61
  integrations: {};
@@ -56,6 +74,21 @@ export declare function createClient(config: {
56
74
  functions: {
57
75
  invoke(functionName: string, data: Record<string, any>): Promise<import("axios").AxiosResponse<any, any>>;
58
76
  };
77
+ agents: {
78
+ listConversations(filterParams?: import("./modules/agents.js").FilterParams): Promise<import("./modules/agents.js").AgentConversation[]>;
79
+ getConversation(conversationId: string): Promise<import("./modules/agents.js").AgentConversation>;
80
+ createConversation(payload: import("./modules/agents.js").CreateConversationPayload): Promise<import("./modules/agents.js").AgentConversation>;
81
+ updateConversation(conversationId: string, payload: import("./modules/agents.js").UpdateConversationPayload): Promise<import("./modules/agents.js").AgentConversation>;
82
+ sendMessage(conversationId: string, message: Omit<import("./modules/agents.js").Message, "id">): Promise<import("./modules/agents.js").Message>;
83
+ deleteMessage(conversationId: string, messageId: string): Promise<void>;
84
+ subscribeToConversation(conversationId: string, onUpdate: (conversation: import("./modules/agents.js").AgentConversation) => void): () => void;
85
+ getWebSocketStatus(): {
86
+ enabled: boolean;
87
+ connected: boolean;
88
+ };
89
+ connectWebSocket(): Promise<void>;
90
+ disconnectWebSocket(): void;
91
+ };
59
92
  };
60
93
  export declare function createClientFromRequest(request: Request): {
61
94
  /**
@@ -81,6 +114,21 @@ export declare function createClientFromRequest(request: Request): {
81
114
  functions: {
82
115
  invoke(functionName: string, data: Record<string, any>): Promise<import("axios").AxiosResponse<any, any>>;
83
116
  };
117
+ agents: {
118
+ listConversations(filterParams?: import("./modules/agents.js").FilterParams): Promise<import("./modules/agents.js").AgentConversation[]>;
119
+ getConversation(conversationId: string): Promise<import("./modules/agents.js").AgentConversation>;
120
+ createConversation(payload: import("./modules/agents.js").CreateConversationPayload): Promise<import("./modules/agents.js").AgentConversation>;
121
+ updateConversation(conversationId: string, payload: import("./modules/agents.js").UpdateConversationPayload): Promise<import("./modules/agents.js").AgentConversation>;
122
+ sendMessage(conversationId: string, message: Omit<import("./modules/agents.js").Message, "id">): Promise<import("./modules/agents.js").Message>;
123
+ deleteMessage(conversationId: string, messageId: string): Promise<void>;
124
+ subscribeToConversation(conversationId: string, onUpdate: (conversation: import("./modules/agents.js").AgentConversation) => void): () => void;
125
+ getWebSocketStatus(): {
126
+ enabled: boolean;
127
+ connected: boolean;
128
+ };
129
+ connectWebSocket(): Promise<void>;
130
+ disconnectWebSocket(): void;
131
+ };
84
132
  };
85
133
  entities: {};
86
134
  integrations: {};
@@ -99,4 +147,19 @@ export declare function createClientFromRequest(request: Request): {
99
147
  functions: {
100
148
  invoke(functionName: string, data: Record<string, any>): Promise<import("axios").AxiosResponse<any, any>>;
101
149
  };
150
+ agents: {
151
+ listConversations(filterParams?: import("./modules/agents.js").FilterParams): Promise<import("./modules/agents.js").AgentConversation[]>;
152
+ getConversation(conversationId: string): Promise<import("./modules/agents.js").AgentConversation>;
153
+ createConversation(payload: import("./modules/agents.js").CreateConversationPayload): Promise<import("./modules/agents.js").AgentConversation>;
154
+ updateConversation(conversationId: string, payload: import("./modules/agents.js").UpdateConversationPayload): Promise<import("./modules/agents.js").AgentConversation>;
155
+ sendMessage(conversationId: string, message: Omit<import("./modules/agents.js").Message, "id">): Promise<import("./modules/agents.js").Message>;
156
+ deleteMessage(conversationId: string, messageId: string): Promise<void>;
157
+ subscribeToConversation(conversationId: string, onUpdate: (conversation: import("./modules/agents.js").AgentConversation) => void): () => void;
158
+ getWebSocketStatus(): {
159
+ enabled: boolean;
160
+ connected: boolean;
161
+ };
162
+ connectWebSocket(): Promise<void>;
163
+ disconnectWebSocket(): void;
164
+ };
102
165
  };
package/dist/client.js CHANGED
@@ -5,6 +5,7 @@ import { createAuthModule } from "./modules/auth.js";
5
5
  import { createSsoModule } from "./modules/sso.js";
6
6
  import { getAccessToken } from "./utils/auth-utils.js";
7
7
  import { createFunctionsModule } from "./modules/functions.js";
8
+ import { createAgentsModule } from "./modules/agents.js";
8
9
  /**
9
10
  * Create a Base44 client instance
10
11
  * @param {Object} config - Client configuration
@@ -13,10 +14,11 @@ import { createFunctionsModule } from "./modules/functions.js";
13
14
  * @param {string} [config.token] - Authentication token
14
15
  * @param {string} [config.serviceToken] - Service role authentication token
15
16
  * @param {boolean} [config.requiresAuth=false] - Whether the app requires authentication
17
+ * @param {AgentsModuleConfig} [config.agents] - Configuration for agents module
16
18
  * @returns {Object} Base44 client instance
17
19
  */
18
20
  export function createClient(config) {
19
- const { serverUrl = "https://base44.app", appId, token, serviceToken, requiresAuth = false, } = config;
21
+ const { serverUrl = "https://base44.app", appId, token, serviceToken, requiresAuth = false, agents = {}, } = config;
20
22
  const axiosClient = createAxiosClient({
21
23
  baseURL: `${serverUrl}/api`,
22
24
  headers: {
@@ -62,12 +64,14 @@ export function createClient(config) {
62
64
  integrations: createIntegrationsModule(axiosClient, appId),
63
65
  auth: createAuthModule(axiosClient, functionsAxiosClient, appId),
64
66
  functions: createFunctionsModule(functionsAxiosClient, appId),
67
+ agents: createAgentsModule(axiosClient, appId, agents),
65
68
  };
66
69
  const serviceRoleModules = {
67
70
  entities: createEntitiesModule(serviceRoleAxiosClient, appId),
68
71
  integrations: createIntegrationsModule(serviceRoleAxiosClient, appId),
69
72
  sso: createSsoModule(serviceRoleAxiosClient, appId, token),
70
73
  functions: createFunctionsModule(serviceRoleFunctionsAxiosClient, appId),
74
+ agents: createAgentsModule(serviceRoleAxiosClient, appId, agents),
71
75
  };
72
76
  // Always try to get token from localStorage or URL parameters
73
77
  if (typeof window !== "undefined") {
@@ -120,10 +124,10 @@ export function createClient(config) {
120
124
  */
121
125
  get asServiceRole() {
122
126
  if (!serviceToken) {
123
- throw new Error('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.');
127
+ throw new Error("Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.");
124
128
  }
125
129
  return serviceRoleModules;
126
- }
130
+ },
127
131
  };
128
132
  return client;
129
133
  }
@@ -139,16 +143,20 @@ export function createClientFromRequest(request) {
139
143
  let serviceRoleToken;
140
144
  let userToken;
141
145
  if (serviceRoleAuthHeader !== null) {
142
- if (serviceRoleAuthHeader === '' || !serviceRoleAuthHeader.startsWith('Bearer ') || serviceRoleAuthHeader.split(' ').length !== 2) {
146
+ if (serviceRoleAuthHeader === "" ||
147
+ !serviceRoleAuthHeader.startsWith("Bearer ") ||
148
+ serviceRoleAuthHeader.split(" ").length !== 2) {
143
149
  throw new Error('Invalid authorization header format. Expected "Bearer <token>"');
144
150
  }
145
- serviceRoleToken = serviceRoleAuthHeader.split(' ')[1];
151
+ serviceRoleToken = serviceRoleAuthHeader.split(" ")[1];
146
152
  }
147
153
  if (authHeader !== null) {
148
- if (authHeader === '' || !authHeader.startsWith('Bearer ') || authHeader.split(' ').length !== 2) {
154
+ if (authHeader === "" ||
155
+ !authHeader.startsWith("Bearer ") ||
156
+ authHeader.split(" ").length !== 2) {
149
157
  throw new Error('Invalid authorization header format. Expected "Bearer <token>"');
150
158
  }
151
- userToken = authHeader.split(' ')[1];
159
+ userToken = authHeader.split(" ")[1];
152
160
  }
153
161
  return createClient({
154
162
  serverUrl: serverUrlHeader || "https://base44.app",
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createClient, createClientFromRequest } from "./client.js";
2
2
  import { Base44Error } from "./utils/axios-client.js";
3
3
  import { getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl } from "./utils/auth-utils.js";
4
+ export type { Message, AgentConversation, CreateConversationPayload, UpdateConversationPayload, FilterParams, AgentsModuleConfig, AgentsModule, } from "./modules/agents.js";
4
5
  export { createClient, createClientFromRequest, Base44Error, getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
@@ -0,0 +1,85 @@
1
+ import { AxiosInstance } from "axios";
2
+ export interface Message {
3
+ id: string;
4
+ role: "user" | "assistant";
5
+ content: string;
6
+ timestamp?: string;
7
+ metadata?: Record<string, any>;
8
+ }
9
+ export interface AgentConversation {
10
+ id: string;
11
+ app_id: string;
12
+ created_by_id: string;
13
+ agent_name: string;
14
+ messages: Message[];
15
+ metadata: Record<string, any>;
16
+ created_at?: string;
17
+ updated_at?: string;
18
+ }
19
+ export interface CreateConversationPayload {
20
+ agent_name: string;
21
+ metadata?: Record<string, any>;
22
+ }
23
+ export interface UpdateConversationPayload {
24
+ metadata?: Record<string, any>;
25
+ }
26
+ export interface FilterParams {
27
+ query?: Record<string, any>;
28
+ sort?: Record<string, 1 | -1>;
29
+ limit?: number;
30
+ skip?: number;
31
+ }
32
+ export interface AgentsModuleConfig {
33
+ enableWebSocket?: boolean;
34
+ socketUrl?: string;
35
+ }
36
+ /**
37
+ * Create agents module for managing AI agent conversations
38
+ */
39
+ export declare function createAgentsModule(axiosClient: AxiosInstance, appId: string, config?: AgentsModuleConfig): {
40
+ /**
41
+ * List all conversations for the current user
42
+ */
43
+ listConversations(filterParams?: FilterParams): Promise<AgentConversation[]>;
44
+ /**
45
+ * Get a specific conversation by ID
46
+ */
47
+ getConversation(conversationId: string): Promise<AgentConversation>;
48
+ /**
49
+ * Create a new agent conversation
50
+ */
51
+ createConversation(payload: CreateConversationPayload): Promise<AgentConversation>;
52
+ /**
53
+ * Update conversation metadata
54
+ */
55
+ updateConversation(conversationId: string, payload: UpdateConversationPayload): Promise<AgentConversation>;
56
+ /**
57
+ * Send a message to an agent and get response
58
+ */
59
+ sendMessage(conversationId: string, message: Omit<Message, "id">): Promise<Message>;
60
+ /**
61
+ * Delete a message from a conversation
62
+ */
63
+ deleteMessage(conversationId: string, messageId: string): Promise<void>;
64
+ /**
65
+ * Subscribe to real-time updates for a conversation
66
+ * Requires WebSocket to be enabled in config
67
+ */
68
+ subscribeToConversation(conversationId: string, onUpdate: (conversation: AgentConversation) => void): () => void;
69
+ /**
70
+ * Get WebSocket connection status
71
+ */
72
+ getWebSocketStatus(): {
73
+ enabled: boolean;
74
+ connected: boolean;
75
+ };
76
+ /**
77
+ * Manually connect WebSocket
78
+ */
79
+ connectWebSocket(): Promise<void>;
80
+ /**
81
+ * Disconnect WebSocket
82
+ */
83
+ disconnectWebSocket(): void;
84
+ };
85
+ export type AgentsModule = ReturnType<typeof createAgentsModule>;
@@ -0,0 +1,251 @@
1
+ /**
2
+ * WebSocket manager for real-time agent conversations
3
+ */
4
+ class AgentWebSocketManager {
5
+ constructor(socketUrl, appId, token) {
6
+ this.socket = null;
7
+ this.listeners = new Map();
8
+ this.reconnectAttempts = 0;
9
+ this.maxReconnectAttempts = 5;
10
+ this.reconnectDelay = 1000;
11
+ this.socketUrl = socketUrl;
12
+ this.appId = appId;
13
+ this.token = token;
14
+ }
15
+ connect() {
16
+ return new Promise((resolve, reject) => {
17
+ var _a;
18
+ // Check if WebSocket is available (browser environment)
19
+ if (typeof WebSocket === "undefined") {
20
+ reject(new Error("WebSocket is not available in this environment"));
21
+ return;
22
+ }
23
+ if (((_a = this.socket) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) {
24
+ resolve();
25
+ return;
26
+ }
27
+ try {
28
+ const wsUrl = new URL(this.socketUrl);
29
+ wsUrl.searchParams.set("appId", this.appId);
30
+ if (this.token) {
31
+ wsUrl.searchParams.set("token", this.token);
32
+ }
33
+ this.socket = new WebSocket(wsUrl.toString());
34
+ this.socket.onopen = () => {
35
+ this.reconnectAttempts = 0;
36
+ resolve();
37
+ };
38
+ this.socket.onmessage = (event) => {
39
+ try {
40
+ const data = JSON.parse(event.data);
41
+ this.handleMessage(data);
42
+ }
43
+ catch (error) {
44
+ console.error("Failed to parse WebSocket message:", error);
45
+ }
46
+ };
47
+ this.socket.onclose = () => {
48
+ this.attemptReconnect();
49
+ };
50
+ this.socket.onerror = (error) => {
51
+ console.error("WebSocket error:", error);
52
+ reject(error);
53
+ };
54
+ }
55
+ catch (error) {
56
+ reject(error);
57
+ }
58
+ });
59
+ }
60
+ handleMessage(data) {
61
+ var _a;
62
+ if (data.event === "update_model" && ((_a = data.data) === null || _a === void 0 ? void 0 : _a.room)) {
63
+ const listeners = this.listeners.get(data.data.room);
64
+ if (listeners) {
65
+ const parsedData = typeof data.data.data === "string"
66
+ ? JSON.parse(data.data.data)
67
+ : data.data.data;
68
+ listeners.forEach((callback) => callback(parsedData));
69
+ }
70
+ }
71
+ }
72
+ attemptReconnect() {
73
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
74
+ console.error("Max reconnection attempts reached");
75
+ return;
76
+ }
77
+ this.reconnectAttempts++;
78
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
79
+ setTimeout(() => {
80
+ console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
81
+ this.connect().catch((error) => {
82
+ console.error("Reconnection failed:", error);
83
+ });
84
+ }, delay);
85
+ }
86
+ subscribe(room, callback) {
87
+ if (!this.listeners.has(room)) {
88
+ this.listeners.set(room, new Set());
89
+ }
90
+ this.listeners.get(room).add(callback);
91
+ // Send subscription message if connected
92
+ if (typeof WebSocket !== "undefined" &&
93
+ this.socket &&
94
+ this.socket.readyState === WebSocket.OPEN &&
95
+ this.socket.send) {
96
+ this.socket.send(JSON.stringify({
97
+ type: "subscribe",
98
+ room: room,
99
+ }));
100
+ }
101
+ // Return unsubscribe function
102
+ return () => {
103
+ const roomListeners = this.listeners.get(room);
104
+ if (roomListeners) {
105
+ roomListeners.delete(callback);
106
+ if (roomListeners.size === 0) {
107
+ this.listeners.delete(room);
108
+ // Send unsubscribe message if connected
109
+ if (typeof WebSocket !== "undefined" &&
110
+ this.socket &&
111
+ this.socket.readyState === WebSocket.OPEN &&
112
+ this.socket.send) {
113
+ this.socket.send(JSON.stringify({
114
+ type: "unsubscribe",
115
+ room: room,
116
+ }));
117
+ }
118
+ }
119
+ }
120
+ };
121
+ }
122
+ disconnect() {
123
+ if (this.socket) {
124
+ this.socket.close();
125
+ this.socket = null;
126
+ }
127
+ this.listeners.clear();
128
+ }
129
+ isConnected() {
130
+ var _a;
131
+ if (typeof WebSocket === "undefined") {
132
+ return false;
133
+ }
134
+ return ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN;
135
+ }
136
+ }
137
+ /**
138
+ * Create agents module for managing AI agent conversations
139
+ */
140
+ export function createAgentsModule(axiosClient, appId, config = {}) {
141
+ var _a, _b;
142
+ let webSocketManager = null;
143
+ let currentConversation = null;
144
+ // Initialize WebSocket if enabled
145
+ if (config.enableWebSocket) {
146
+ const socketUrl = config.socketUrl || "wss://base44.app/ws";
147
+ // Extract token from axios client if available
148
+ const token = (_b = (_a = axiosClient.defaults.headers.common) === null || _a === void 0 ? void 0 : _a.Authorization) === null || _b === void 0 ? void 0 : _b.toString().replace("Bearer ", "");
149
+ webSocketManager = new AgentWebSocketManager(socketUrl, appId, token);
150
+ }
151
+ return {
152
+ /**
153
+ * List all conversations for the current user
154
+ */
155
+ async listConversations(filterParams) {
156
+ const response = await axiosClient.get(`/apps/${appId}/agents/conversations`, {
157
+ params: filterParams,
158
+ });
159
+ return response;
160
+ },
161
+ /**
162
+ * Get a specific conversation by ID
163
+ */
164
+ async getConversation(conversationId) {
165
+ const response = await axiosClient.get(`/apps/${appId}/agents/conversations/${conversationId}`);
166
+ return response;
167
+ },
168
+ /**
169
+ * Create a new agent conversation
170
+ */
171
+ async createConversation(payload) {
172
+ const response = await axiosClient.post(`/apps/${appId}/agents/conversations`, payload);
173
+ return response;
174
+ },
175
+ /**
176
+ * Update conversation metadata
177
+ */
178
+ async updateConversation(conversationId, payload) {
179
+ const response = await axiosClient.put(`/apps/${appId}/agents/conversations/${conversationId}`, payload);
180
+ return response;
181
+ },
182
+ /**
183
+ * Send a message to an agent and get response
184
+ */
185
+ async sendMessage(conversationId, message) {
186
+ // Update current conversation for WebSocket tracking
187
+ if ((currentConversation === null || currentConversation === void 0 ? void 0 : currentConversation.id) === conversationId) {
188
+ currentConversation.messages = [
189
+ ...currentConversation.messages,
190
+ { ...message, id: "temp-" + Date.now() },
191
+ ];
192
+ }
193
+ const response = await axiosClient.post(`/apps/${appId}/agents/conversations/${conversationId}/messages`, message);
194
+ return response;
195
+ },
196
+ /**
197
+ * Delete a message from a conversation
198
+ */
199
+ async deleteMessage(conversationId, messageId) {
200
+ await axiosClient.delete(`/apps/${appId}/agents/conversations/${conversationId}/messages/${messageId}`);
201
+ },
202
+ /**
203
+ * Subscribe to real-time updates for a conversation
204
+ * Requires WebSocket to be enabled in config
205
+ */
206
+ subscribeToConversation(conversationId, onUpdate) {
207
+ if (!webSocketManager) {
208
+ throw new Error("WebSocket is not enabled. Set enableWebSocket: true in agents config");
209
+ }
210
+ // Connect WebSocket if not already connected
211
+ if (!webSocketManager.isConnected()) {
212
+ webSocketManager.connect().catch((error) => {
213
+ console.error("Failed to connect WebSocket:", error);
214
+ });
215
+ }
216
+ return webSocketManager.subscribe(`/agent-conversations/${conversationId}`, (data) => {
217
+ // Update current conversation reference
218
+ if (data.id === conversationId) {
219
+ currentConversation = data;
220
+ }
221
+ onUpdate(data);
222
+ });
223
+ },
224
+ /**
225
+ * Get WebSocket connection status
226
+ */
227
+ getWebSocketStatus() {
228
+ return {
229
+ enabled: !!webSocketManager,
230
+ connected: (webSocketManager === null || webSocketManager === void 0 ? void 0 : webSocketManager.isConnected()) || false,
231
+ };
232
+ },
233
+ /**
234
+ * Manually connect WebSocket
235
+ */
236
+ async connectWebSocket() {
237
+ if (!webSocketManager) {
238
+ throw new Error("WebSocket is not enabled. Set enableWebSocket: true in agents config");
239
+ }
240
+ await webSocketManager.connect();
241
+ },
242
+ /**
243
+ * Disconnect WebSocket
244
+ */
245
+ disconnectWebSocket() {
246
+ if (webSocketManager) {
247
+ webSocketManager.disconnect();
248
+ }
249
+ },
250
+ };
251
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/sdk",
3
- "version": "0.7.0-dev.e78162e",
3
+ "version": "0.7.0-pr.26.0b8f663",
4
4
  "description": "JavaScript SDK for Base44 API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",