@agentuity/server 0.0.105 → 0.0.106

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 (105) hide show
  1. package/dist/api/api.d.ts +11 -6
  2. package/dist/api/api.d.ts.map +1 -1
  3. package/dist/api/api.js +21 -13
  4. package/dist/api/api.js.map +1 -1
  5. package/dist/api/index.d.ts +1 -0
  6. package/dist/api/index.d.ts.map +1 -1
  7. package/dist/api/index.js +1 -0
  8. package/dist/api/index.js.map +1 -1
  9. package/dist/api/project/deploy.d.ts +0 -6
  10. package/dist/api/project/deploy.d.ts.map +1 -1
  11. package/dist/api/project/deploy.js +0 -2
  12. package/dist/api/project/deploy.js.map +1 -1
  13. package/dist/api/project/get.d.ts +2 -1
  14. package/dist/api/project/get.d.ts.map +1 -1
  15. package/dist/api/project/get.js +10 -2
  16. package/dist/api/project/get.js.map +1 -1
  17. package/dist/api/region/create.d.ts +2 -0
  18. package/dist/api/region/create.d.ts.map +1 -1
  19. package/dist/api/region/create.js +1 -0
  20. package/dist/api/region/create.js.map +1 -1
  21. package/dist/api/region/delete.d.ts +12 -2
  22. package/dist/api/region/delete.d.ts.map +1 -1
  23. package/dist/api/region/delete.js +6 -1
  24. package/dist/api/region/delete.js.map +1 -1
  25. package/dist/api/region/resources.d.ts +4 -0
  26. package/dist/api/region/resources.d.ts.map +1 -1
  27. package/dist/api/region/resources.js +2 -0
  28. package/dist/api/region/resources.js.map +1 -1
  29. package/dist/api/sandbox/client.d.ts +125 -0
  30. package/dist/api/sandbox/client.d.ts.map +1 -0
  31. package/dist/api/sandbox/client.js +202 -0
  32. package/dist/api/sandbox/client.js.map +1 -0
  33. package/dist/api/sandbox/create.d.ts +24 -0
  34. package/dist/api/sandbox/create.d.ts.map +1 -0
  35. package/dist/api/sandbox/create.js +133 -0
  36. package/dist/api/sandbox/create.js.map +1 -0
  37. package/dist/api/sandbox/destroy.d.ts +14 -0
  38. package/dist/api/sandbox/destroy.d.ts.map +1 -0
  39. package/dist/api/sandbox/destroy.js +25 -0
  40. package/dist/api/sandbox/destroy.js.map +1 -0
  41. package/dist/api/sandbox/execute.d.ts +18 -0
  42. package/dist/api/sandbox/execute.d.ts.map +1 -0
  43. package/dist/api/sandbox/execute.js +77 -0
  44. package/dist/api/sandbox/execute.js.map +1 -0
  45. package/dist/api/sandbox/execution.d.ts +46 -0
  46. package/dist/api/sandbox/execution.d.ts.map +1 -0
  47. package/dist/api/sandbox/execution.js +101 -0
  48. package/dist/api/sandbox/execution.js.map +1 -0
  49. package/dist/api/sandbox/files.d.ts +41 -0
  50. package/dist/api/sandbox/files.d.ts.map +1 -0
  51. package/dist/api/sandbox/files.js +91 -0
  52. package/dist/api/sandbox/files.js.map +1 -0
  53. package/dist/api/sandbox/get.d.ts +16 -0
  54. package/dist/api/sandbox/get.d.ts.map +1 -0
  55. package/dist/api/sandbox/get.js +57 -0
  56. package/dist/api/sandbox/get.js.map +1 -0
  57. package/dist/api/sandbox/index.d.ts +22 -0
  58. package/dist/api/sandbox/index.d.ts.map +1 -0
  59. package/dist/api/sandbox/index.js +12 -0
  60. package/dist/api/sandbox/index.js.map +1 -0
  61. package/dist/api/sandbox/list.d.ts +15 -0
  62. package/dist/api/sandbox/list.d.ts.map +1 -0
  63. package/dist/api/sandbox/list.js +75 -0
  64. package/dist/api/sandbox/list.js.map +1 -0
  65. package/dist/api/sandbox/run.d.ts +28 -0
  66. package/dist/api/sandbox/run.d.ts.map +1 -0
  67. package/dist/api/sandbox/run.js +269 -0
  68. package/dist/api/sandbox/run.js.map +1 -0
  69. package/dist/api/sandbox/snapshot.d.ts +89 -0
  70. package/dist/api/sandbox/snapshot.d.ts.map +1 -0
  71. package/dist/api/sandbox/snapshot.js +140 -0
  72. package/dist/api/sandbox/snapshot.js.map +1 -0
  73. package/dist/api/sandbox/util.d.ts +37 -0
  74. package/dist/api/sandbox/util.d.ts.map +1 -0
  75. package/dist/api/sandbox/util.js +45 -0
  76. package/dist/api/sandbox/util.js.map +1 -0
  77. package/dist/config.d.ts +1 -0
  78. package/dist/config.d.ts.map +1 -1
  79. package/dist/config.js +1 -0
  80. package/dist/config.js.map +1 -1
  81. package/dist/runtime-bootstrap.d.ts.map +1 -1
  82. package/dist/runtime-bootstrap.js +3 -0
  83. package/dist/runtime-bootstrap.js.map +1 -1
  84. package/package.json +4 -4
  85. package/src/api/api.ts +33 -13
  86. package/src/api/index.ts +1 -0
  87. package/src/api/project/deploy.ts +0 -2
  88. package/src/api/project/get.ts +10 -2
  89. package/src/api/region/create.ts +1 -0
  90. package/src/api/region/delete.ts +9 -2
  91. package/src/api/region/resources.ts +2 -0
  92. package/src/api/sandbox/client.ts +349 -0
  93. package/src/api/sandbox/create.ts +166 -0
  94. package/src/api/sandbox/destroy.ts +41 -0
  95. package/src/api/sandbox/execute.ts +102 -0
  96. package/src/api/sandbox/execution.ts +154 -0
  97. package/src/api/sandbox/files.ts +138 -0
  98. package/src/api/sandbox/get.ts +74 -0
  99. package/src/api/sandbox/index.ts +35 -0
  100. package/src/api/sandbox/list.ts +94 -0
  101. package/src/api/sandbox/run.ts +360 -0
  102. package/src/api/sandbox/snapshot.ts +247 -0
  103. package/src/api/sandbox/util.ts +55 -0
  104. package/src/config.ts +2 -0
  105. package/src/runtime-bootstrap.ts +3 -0
@@ -0,0 +1,35 @@
1
+ export { sandboxCreate } from './create';
2
+ export type { SandboxCreateResponse, SandboxCreateParams } from './create';
3
+ export { sandboxExecute } from './execute';
4
+ export type { SandboxExecuteParams } from './execute';
5
+ export { sandboxGet } from './get';
6
+ export type { SandboxGetParams } from './get';
7
+ export { sandboxList } from './list';
8
+ export type { SandboxListParams } from './list';
9
+ export { sandboxDestroy } from './destroy';
10
+ export type { SandboxDestroyParams } from './destroy';
11
+ export { sandboxRun } from './run';
12
+ export type { SandboxRunParams } from './run';
13
+ export { executionGet, executionList } from './execution';
14
+ export type {
15
+ ExecutionInfo,
16
+ ExecutionGetParams,
17
+ ExecutionListParams,
18
+ ExecutionListResponse,
19
+ } from './execution';
20
+ export { SandboxResponseError, writeAndDrain } from './util';
21
+ export { SandboxClient } from './client';
22
+ export type { SandboxClientOptions, SandboxInstance, ExecuteOptions } from './client';
23
+ export { sandboxWriteFiles, sandboxReadFile } from './files';
24
+ export type { WriteFilesParams, WriteFilesResult, ReadFileParams } from './files';
25
+ export { snapshotCreate, snapshotGet, snapshotList, snapshotDelete, snapshotTag } from './snapshot';
26
+ export type {
27
+ SnapshotInfo,
28
+ SnapshotFileInfo,
29
+ SnapshotCreateParams,
30
+ SnapshotGetParams,
31
+ SnapshotListParams,
32
+ SnapshotListResponse,
33
+ SnapshotDeleteParams,
34
+ SnapshotTagParams,
35
+ } from './snapshot';
@@ -0,0 +1,94 @@
1
+ import { z } from 'zod';
2
+ import { APIClient, APIResponseSchema } from '../api';
3
+ import { SandboxResponseError, API_VERSION } from './util';
4
+ import type { ListSandboxesParams, ListSandboxesResponse, SandboxStatus } from '@agentuity/core';
5
+
6
+ const SandboxInfoSchema = z
7
+ .object({
8
+ sandboxId: z.string().describe('Unique identifier for the sandbox'),
9
+ status: z
10
+ .enum(['creating', 'idle', 'running', 'terminated', 'failed'])
11
+ .describe('Current status of the sandbox'),
12
+ createdAt: z.string().describe('ISO timestamp when the sandbox was created'),
13
+ region: z.string().optional().describe('Region where the sandbox is running'),
14
+ snapshotId: z.string().optional().describe('Snapshot ID this sandbox was created from'),
15
+ snapshotTag: z.string().optional().describe('Snapshot tag this sandbox was created from'),
16
+ executions: z.number().describe('Total number of executions in this sandbox'),
17
+ stdoutStreamUrl: z.string().optional().describe('URL for streaming stdout output'),
18
+ stderrStreamUrl: z.string().optional().describe('URL for streaming stderr output'),
19
+ })
20
+ .describe('Summary information about a sandbox');
21
+
22
+ const ListSandboxesDataSchema = z
23
+ .object({
24
+ sandboxes: z.array(SandboxInfoSchema).describe('List of sandbox entries'),
25
+ total: z.number().describe('Total number of sandboxes matching the query'),
26
+ })
27
+ .describe('Paginated list of sandboxes');
28
+
29
+ const ListSandboxesResponseSchema = APIResponseSchema(ListSandboxesDataSchema);
30
+
31
+ export interface SandboxListParams extends ListSandboxesParams {
32
+ orgId?: string;
33
+ }
34
+
35
+ /**
36
+ * Lists sandboxes with optional filtering and pagination.
37
+ *
38
+ * @param client - The API client to use for the request
39
+ * @param params - Optional parameters for filtering by project, status, and pagination
40
+ * @returns Paginated list of sandboxes with total count
41
+ * @throws {SandboxResponseError} If the request fails
42
+ */
43
+ export async function sandboxList(
44
+ client: APIClient,
45
+ params?: SandboxListParams
46
+ ): Promise<ListSandboxesResponse> {
47
+ const queryParams = new URLSearchParams();
48
+
49
+ if (params?.orgId) {
50
+ queryParams.set('orgId', params.orgId);
51
+ }
52
+ if (params?.projectId) {
53
+ queryParams.set('projectId', params.projectId);
54
+ }
55
+ if (params?.snapshotId) {
56
+ queryParams.set('snapshotId', params.snapshotId);
57
+ }
58
+ if (params?.status) {
59
+ queryParams.set('status', params.status);
60
+ }
61
+ if (params?.limit !== undefined) {
62
+ queryParams.set('limit', params.limit.toString());
63
+ }
64
+ if (params?.offset !== undefined) {
65
+ queryParams.set('offset', params.offset.toString());
66
+ }
67
+
68
+ const queryString = queryParams.toString();
69
+ const url = `/sandbox/${API_VERSION}${queryString ? `?${queryString}` : ''}`;
70
+
71
+ const resp = await client.get<z.infer<typeof ListSandboxesResponseSchema>>(
72
+ url,
73
+ ListSandboxesResponseSchema
74
+ );
75
+
76
+ if (resp.success) {
77
+ return {
78
+ sandboxes: resp.data.sandboxes.map((s) => ({
79
+ sandboxId: s.sandboxId,
80
+ status: s.status as SandboxStatus,
81
+ createdAt: s.createdAt,
82
+ region: s.region,
83
+ snapshotId: s.snapshotId,
84
+ snapshotTag: s.snapshotTag,
85
+ executions: s.executions,
86
+ stdoutStreamUrl: s.stdoutStreamUrl,
87
+ stderrStreamUrl: s.stderrStreamUrl,
88
+ })),
89
+ total: resp.data.total,
90
+ };
91
+ }
92
+
93
+ throw new SandboxResponseError({ message: resp.message });
94
+ }
@@ -0,0 +1,360 @@
1
+ import type { Logger } from '@agentuity/core';
2
+ import type { Readable, Writable } from 'node:stream';
3
+ import { APIClient } from '../api';
4
+ import { sandboxCreate } from './create';
5
+ import { sandboxDestroy } from './destroy';
6
+ import { sandboxGet } from './get';
7
+ import { SandboxResponseError, writeAndDrain } from './util';
8
+ import type { SandboxRunOptions, SandboxRunResult } from '@agentuity/core';
9
+ import { getServiceUrls } from '../../config';
10
+
11
+ const POLL_INTERVAL_MS = 500;
12
+ const MAX_POLL_ATTEMPTS = 7200;
13
+
14
+ export interface SandboxRunParams {
15
+ options: SandboxRunOptions;
16
+ orgId?: string;
17
+ region?: string;
18
+ apiKey?: string;
19
+ signal?: AbortSignal;
20
+ stdin?: Readable;
21
+ stdout?: Writable;
22
+ stderr?: Writable;
23
+ logger?: Logger;
24
+ }
25
+
26
+ /**
27
+ * Creates a sandbox, executes a command, and waits for completion.
28
+ *
29
+ * This is a high-level convenience function that handles the full lifecycle:
30
+ * creating a sandbox, streaming I/O, polling for completion, and cleanup.
31
+ *
32
+ * @param client - The API client to use for the request
33
+ * @param params - Parameters including command options, I/O streams, and timeout settings
34
+ * @returns The run result including exit code and duration
35
+ * @throws {SandboxResponseError} If sandbox creation fails, execution times out, or is cancelled
36
+ */
37
+ export async function sandboxRun(
38
+ client: APIClient,
39
+ params: SandboxRunParams
40
+ ): Promise<SandboxRunResult> {
41
+ const { options, orgId, region, apiKey, signal, stdin, stdout, stderr, logger } = params;
42
+ const started = Date.now();
43
+
44
+ let stdinStreamId: string | undefined;
45
+ let stdinStreamUrl: string | undefined;
46
+
47
+ // If stdin is provided and has data, create a stream for it
48
+ if (stdin && region && apiKey) {
49
+ const streamResult = await createStdinStream(region, apiKey, orgId, logger);
50
+ stdinStreamId = streamResult.id;
51
+ stdinStreamUrl = streamResult.url;
52
+ logger?.debug('created stdin stream: %s', stdinStreamId);
53
+ }
54
+
55
+ const createResponse = await sandboxCreate(client, {
56
+ options: {
57
+ ...options,
58
+ command: {
59
+ exec: options.command.exec,
60
+ files: options.command.files,
61
+ mode: 'oneshot',
62
+ },
63
+ stream: {
64
+ ...options.stream,
65
+ stdin: stdinStreamId,
66
+ },
67
+ },
68
+ orgId,
69
+ });
70
+
71
+ const sandboxId = createResponse.sandboxId;
72
+ const stdoutStreamUrl = createResponse.stdoutStreamUrl;
73
+ const stderrStreamUrl = createResponse.stderrStreamUrl;
74
+
75
+ logger?.debug(
76
+ 'sandbox created: %s, stdoutUrl: %s, stderrUrl: %s',
77
+ sandboxId,
78
+ stdoutStreamUrl ?? 'none',
79
+ stderrStreamUrl ?? 'none'
80
+ );
81
+
82
+ const abortController = new AbortController();
83
+ const streamPromises: Promise<void>[] = [];
84
+
85
+ try {
86
+ // Start stdin streaming if we have stdin and a stream URL
87
+ if (stdin && stdinStreamUrl && apiKey) {
88
+ const stdinPromise = streamStdinToUrl(
89
+ stdin,
90
+ stdinStreamUrl,
91
+ apiKey,
92
+ abortController.signal,
93
+ logger
94
+ );
95
+ streamPromises.push(stdinPromise);
96
+ }
97
+
98
+ // Check if stdout and stderr are the same stream (combined output)
99
+ const isCombinedOutput =
100
+ stdoutStreamUrl && stderrStreamUrl && stdoutStreamUrl === stderrStreamUrl;
101
+
102
+ if (isCombinedOutput) {
103
+ // Stream combined output to stdout only to avoid duplicates
104
+ if (stdout) {
105
+ logger?.debug('using combined output stream (stdout === stderr)');
106
+ const combinedPromise = streamUrlToWritable(
107
+ stdoutStreamUrl,
108
+ stdout,
109
+ abortController.signal,
110
+ logger
111
+ );
112
+ streamPromises.push(combinedPromise);
113
+ }
114
+ } else {
115
+ // Start stdout streaming
116
+ if (stdoutStreamUrl && stdout) {
117
+ const stdoutPromise = streamUrlToWritable(
118
+ stdoutStreamUrl,
119
+ stdout,
120
+ abortController.signal,
121
+ logger
122
+ );
123
+ streamPromises.push(stdoutPromise);
124
+ }
125
+
126
+ // Start stderr streaming
127
+ if (stderrStreamUrl && stderr) {
128
+ const stderrPromise = streamUrlToWritable(
129
+ stderrStreamUrl,
130
+ stderr,
131
+ abortController.signal,
132
+ logger
133
+ );
134
+ streamPromises.push(stderrPromise);
135
+ }
136
+ }
137
+
138
+ // Poll for sandbox completion in parallel with streaming
139
+ let attempts = 0;
140
+ let finalStatus: 'terminated' | 'failed' | null = null;
141
+
142
+ while (attempts < MAX_POLL_ATTEMPTS) {
143
+ if (signal?.aborted) {
144
+ abortController.abort();
145
+ throw new SandboxResponseError({
146
+ message: 'Sandbox execution cancelled',
147
+ sandboxId,
148
+ });
149
+ }
150
+
151
+ await sleep(POLL_INTERVAL_MS);
152
+ attempts++;
153
+
154
+ try {
155
+ const sandboxInfo = await sandboxGet(client, { sandboxId, orgId });
156
+
157
+ if (sandboxInfo.status === 'terminated') {
158
+ finalStatus = 'terminated';
159
+ break;
160
+ }
161
+
162
+ if (sandboxInfo.status === 'failed') {
163
+ finalStatus = 'failed';
164
+ break;
165
+ }
166
+ } catch {
167
+ // Ignore polling errors, continue
168
+ continue;
169
+ }
170
+ }
171
+
172
+ // Sandbox completed - wait for streams to complete naturally (EOF)
173
+ // Pulse closes streams when the sandbox terminates, so streams should EOF
174
+ // We must wait for streams to fully drain before returning
175
+ logger?.debug('waiting for streams to complete...');
176
+ await Promise.allSettled(streamPromises);
177
+ logger?.debug('streams completed');
178
+
179
+ if (finalStatus === 'terminated') {
180
+ return {
181
+ sandboxId,
182
+ exitCode: 0,
183
+ durationMs: Date.now() - started,
184
+ };
185
+ }
186
+
187
+ if (finalStatus === 'failed') {
188
+ return {
189
+ sandboxId,
190
+ exitCode: 1,
191
+ durationMs: Date.now() - started,
192
+ };
193
+ }
194
+
195
+ throw new SandboxResponseError({
196
+ message: 'Sandbox execution polling timed out',
197
+ sandboxId,
198
+ });
199
+ } catch (error) {
200
+ abortController.abort();
201
+ try {
202
+ await sandboxDestroy(client, { sandboxId, orgId });
203
+ } catch {
204
+ // Ignore cleanup errors
205
+ }
206
+ throw error;
207
+ }
208
+ }
209
+
210
+ async function createStdinStream(
211
+ region: string,
212
+ apiKey: string,
213
+ orgId?: string,
214
+ logger?: Logger
215
+ ): Promise<{ id: string; url: string }> {
216
+ const urls = getServiceUrls(region);
217
+ const streamBaseUrl = urls.stream;
218
+
219
+ // Build URL with orgId query param for CLI token validation
220
+ const queryParams = new URLSearchParams();
221
+ if (orgId) {
222
+ queryParams.set('orgId', orgId);
223
+ }
224
+ const queryString = queryParams.toString();
225
+ const url = `${streamBaseUrl}${queryString ? `?${queryString}` : ''}`;
226
+ logger?.trace('creating stdin stream: %s', url);
227
+
228
+ const response = await fetch(url, {
229
+ method: 'POST',
230
+ headers: {
231
+ 'Content-Type': 'application/json',
232
+ Authorization: `Bearer ${apiKey}`,
233
+ },
234
+ body: JSON.stringify({
235
+ name: `sandbox-stdin-${Date.now()}`,
236
+ }),
237
+ });
238
+
239
+ if (!response.ok) {
240
+ throw new Error(`Failed to create stdin stream: ${response.status} ${response.statusText}`);
241
+ }
242
+
243
+ const data = (await response.json()) as { id: string };
244
+ logger?.debug('created stdin stream: %s', data.id);
245
+
246
+ // Include orgId in the URL for subsequent PUT requests (needed for CLI token auth)
247
+ const putQueryString = orgId ? `?orgId=${encodeURIComponent(orgId)}` : '';
248
+ return {
249
+ id: data.id,
250
+ url: `${streamBaseUrl}/${data.id}${putQueryString}`,
251
+ };
252
+ }
253
+
254
+ async function streamStdinToUrl(
255
+ stdin: Readable,
256
+ url: string,
257
+ apiKey: string,
258
+ signal: AbortSignal,
259
+ logger?: Logger
260
+ ): Promise<void> {
261
+ try {
262
+ logger?.debug('streaming stdin to: %s', url);
263
+
264
+ // Convert Node.js Readable to a web ReadableStream for fetch body
265
+ let controllerClosed = false;
266
+ const webStream = new ReadableStream({
267
+ start(controller) {
268
+ stdin.on('data', (chunk: Buffer) => {
269
+ if (!signal.aborted && !controllerClosed) {
270
+ controller.enqueue(chunk);
271
+ }
272
+ });
273
+ stdin.on('end', () => {
274
+ if (!controllerClosed) {
275
+ controllerClosed = true;
276
+ controller.close();
277
+ }
278
+ });
279
+ stdin.on('error', (err) => {
280
+ if (!controllerClosed) {
281
+ controllerClosed = true;
282
+ controller.error(err);
283
+ }
284
+ });
285
+ signal.addEventListener('abort', () => {
286
+ if (!controllerClosed) {
287
+ controllerClosed = true;
288
+ controller.close();
289
+ }
290
+ });
291
+ },
292
+ });
293
+
294
+ const response = await fetch(url, {
295
+ method: 'PUT',
296
+ headers: {
297
+ Authorization: `Bearer ${apiKey}`,
298
+ },
299
+ body: webStream,
300
+ signal,
301
+ duplex: 'half',
302
+ } as RequestInit);
303
+
304
+ if (!response.ok) {
305
+ logger?.debug('stdin stream PUT failed: %d', response.status);
306
+ } else {
307
+ logger?.debug('stdin stream completed');
308
+ }
309
+ } catch (err) {
310
+ if (err instanceof Error && err.name === 'AbortError') {
311
+ logger?.debug('stdin stream aborted (expected on completion)');
312
+ } else {
313
+ logger?.debug('stdin stream error: %s', err);
314
+ }
315
+ }
316
+ }
317
+
318
+ async function streamUrlToWritable(
319
+ url: string,
320
+ writable: Writable,
321
+ signal: AbortSignal,
322
+ logger?: Logger
323
+ ): Promise<void> {
324
+ try {
325
+ logger?.debug('fetching stream: %s', url);
326
+ const response = await fetch(url, { signal });
327
+ logger?.debug('stream response status: %d', response.status);
328
+
329
+ if (!response.ok || !response.body) {
330
+ logger?.debug('stream response not ok or no body');
331
+ return;
332
+ }
333
+
334
+ const reader = response.body.getReader();
335
+
336
+ // Read until EOF - Pulse will block until data is available
337
+ while (true) {
338
+ const { done, value } = await reader.read();
339
+ if (done) {
340
+ logger?.debug('stream EOF');
341
+ break;
342
+ }
343
+
344
+ if (value) {
345
+ logger?.debug('stream chunk: %d bytes', value.length);
346
+ await writeAndDrain(writable, value);
347
+ }
348
+ }
349
+ } catch (err) {
350
+ if (err instanceof Error && err.name === 'AbortError') {
351
+ logger?.debug('stream aborted');
352
+ return;
353
+ }
354
+ logger?.debug('stream error: %s', err);
355
+ }
356
+ }
357
+
358
+ function sleep(ms: number): Promise<void> {
359
+ return new Promise((resolve) => setTimeout(resolve, ms));
360
+ }