oomi-ai 0.2.29 → 0.2.38

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.
@@ -1,253 +1,304 @@
1
- function trimString(value) {
2
- return typeof value === 'string' ? value.trim() : '';
3
- }
4
-
5
- function stripTrailingSlash(value) {
6
- return trimString(value).replace(/\/+$/, '');
7
- }
8
-
9
- async function readJsonResponse(response) {
10
- return response.json().catch(() => ({}));
11
- }
12
-
13
- async function postJson({ fetchImpl, backendUrl, path, deviceToken, body }) {
14
- const baseUrl = stripTrailingSlash(backendUrl);
15
- if (!baseUrl) {
16
- throw new Error('Backend URL is required.');
17
- }
18
- const token = trimString(deviceToken);
19
- if (!token) {
20
- throw new Error('Device token is required.');
21
- }
22
-
23
- const response = await fetchImpl(`${baseUrl}${path}`, {
24
- method: 'POST',
25
- headers: {
26
- 'Content-Type': 'application/json',
27
- Authorization: `Bearer ${token}`,
28
- },
29
- body: JSON.stringify(body),
30
- });
31
-
32
- const payload = await readJsonResponse(response);
33
- if (!response.ok) {
34
- const errorMessage =
35
- trimString(payload?.error) ||
36
- trimString(payload?.message) ||
37
- `Persona API request failed (${response.status})`;
38
- throw new Error(errorMessage);
39
- }
40
-
41
- return payload;
42
- }
43
-
44
- export function createPersonaApiClient({
45
- backendUrl,
46
- deviceToken,
47
- deviceId,
48
- fetchImpl = globalThis.fetch,
49
- }) {
50
- if (typeof fetchImpl !== 'function') {
51
- throw new Error('fetch implementation is required.');
52
- }
53
-
54
- const resolvedBackendUrl = stripTrailingSlash(backendUrl);
55
- const resolvedDeviceToken = trimString(deviceToken);
56
- const resolvedDeviceId = trimString(deviceId);
57
-
58
- function withDevice(body = {}) {
59
- if (!resolvedDeviceId) {
60
- return body;
61
- }
62
- return {
63
- ...body,
64
- deviceId: resolvedDeviceId,
65
- };
66
- }
67
-
68
- return {
69
- createManagedPersona({
70
- slug,
71
- name,
72
- description,
73
- templateType = 'persona-app',
74
- promptTemplateVersion = 'v1',
75
- }) {
76
- const safeName = trimString(name);
77
- if (!safeName) {
78
- throw new Error('Persona name is required.');
79
- }
80
-
81
- const body = withDevice({
82
- name: safeName,
83
- description: trimString(description) || safeName,
84
- templateType: trimString(templateType) || 'persona-app',
85
- promptTemplateVersion: trimString(promptTemplateVersion) || 'v1',
86
- });
87
- const safeSlug = trimString(slug);
88
- if (safeSlug) {
89
- body.slug = safeSlug;
90
- }
91
-
92
- return postJson({
93
- fetchImpl,
94
- backendUrl: resolvedBackendUrl,
95
- deviceToken: resolvedDeviceToken,
96
- path: '/v1/personas/managed_create',
97
- body,
98
- });
99
- },
100
-
101
- registerRuntime({
102
- slug,
103
- endpoint,
104
- healthcheckUrl,
105
- transport = 'local',
106
- localPort,
107
- startedAt,
108
- }) {
109
- const safeSlug = trimString(slug);
110
- if (!safeSlug) {
111
- throw new Error('Persona slug is required.');
112
- }
113
-
114
- return postJson({
115
- fetchImpl,
116
- backendUrl: resolvedBackendUrl,
117
- deviceToken: resolvedDeviceToken,
118
- path: `/v1/personas/${encodeURIComponent(safeSlug)}/runtime_register`,
119
- body: withDevice({
120
- endpoint,
121
- healthcheckUrl,
122
- transport,
123
- localPort,
124
- startedAt,
125
- }),
126
- });
127
- },
128
-
129
- heartbeatRuntime({
130
- slug,
131
- endpoint,
132
- healthcheckUrl,
133
- transport = 'local',
134
- localPort,
135
- observedAt,
136
- }) {
137
- const safeSlug = trimString(slug);
138
- if (!safeSlug) {
139
- throw new Error('Persona slug is required.');
140
- }
141
-
142
- return postJson({
143
- fetchImpl,
144
- backendUrl: resolvedBackendUrl,
145
- deviceToken: resolvedDeviceToken,
146
- path: `/v1/personas/${encodeURIComponent(safeSlug)}/heartbeat`,
147
- body: withDevice({
148
- endpoint,
149
- healthcheckUrl,
150
- transport,
151
- localPort,
152
- observedAt,
153
- }),
154
- });
155
- },
156
-
157
- failRuntime({
158
- slug,
159
- code,
160
- message,
161
- }) {
162
- const safeSlug = trimString(slug);
163
- if (!safeSlug) {
164
- throw new Error('Persona slug is required.');
165
- }
166
-
167
- return postJson({
168
- fetchImpl,
169
- backendUrl: resolvedBackendUrl,
170
- deviceToken: resolvedDeviceToken,
171
- path: `/v1/personas/${encodeURIComponent(safeSlug)}/fail`,
172
- body: withDevice({
173
- code,
174
- message,
175
- }),
176
- });
177
- },
178
-
179
- startJob({
180
- jobId,
181
- startedAt,
182
- }) {
183
- const safeJobId = trimString(jobId);
184
- if (!safeJobId) {
185
- throw new Error('Persona job id is required.');
186
- }
187
-
188
- return postJson({
189
- fetchImpl,
190
- backendUrl: resolvedBackendUrl,
191
- deviceToken: resolvedDeviceToken,
192
- path: `/v1/persona_jobs/${encodeURIComponent(safeJobId)}/start`,
193
- body: withDevice({
194
- startedAt,
195
- }),
196
- });
197
- },
198
-
199
- succeedJob({
200
- jobId,
201
- workspacePath,
202
- localPort,
203
- transport = 'local',
204
- endpoint,
205
- healthcheckUrl,
206
- completedAt,
207
- }) {
208
- const safeJobId = trimString(jobId);
209
- if (!safeJobId) {
210
- throw new Error('Persona job id is required.');
211
- }
212
-
213
- return postJson({
214
- fetchImpl,
215
- backendUrl: resolvedBackendUrl,
216
- deviceToken: resolvedDeviceToken,
217
- path: `/v1/persona_jobs/${encodeURIComponent(safeJobId)}/succeed`,
218
- body: withDevice({
219
- workspacePath,
220
- localPort,
221
- transport,
222
- endpoint,
223
- healthcheckUrl,
224
- completedAt,
225
- }),
226
- });
227
- },
228
-
229
- failJob({
230
- jobId,
231
- code,
232
- message,
233
- completedAt,
234
- }) {
235
- const safeJobId = trimString(jobId);
236
- if (!safeJobId) {
237
- throw new Error('Persona job id is required.');
238
- }
239
-
240
- return postJson({
241
- fetchImpl,
242
- backendUrl: resolvedBackendUrl,
243
- deviceToken: resolvedDeviceToken,
244
- path: `/v1/persona_jobs/${encodeURIComponent(safeJobId)}/fail`,
245
- body: withDevice({
246
- code,
247
- message,
248
- completedAt,
249
- }),
250
- });
251
- },
252
- };
253
- }
1
+ function trimString(value) {
2
+ return typeof value === 'string' ? value.trim() : '';
3
+ }
4
+
5
+ function stripTrailingSlash(value) {
6
+ return trimString(value).replace(/\/+$/, '');
7
+ }
8
+
9
+ async function readJsonResponse(response) {
10
+ return response.json().catch(() => ({}));
11
+ }
12
+
13
+ async function getJson({ fetchImpl, backendUrl, path, deviceToken }) {
14
+ const baseUrl = stripTrailingSlash(backendUrl);
15
+ if (!baseUrl) {
16
+ throw new Error('Backend URL is required.');
17
+ }
18
+
19
+ const headers = {};
20
+ const token = trimString(deviceToken);
21
+ if (token) {
22
+ headers.Authorization = `Bearer ${token}`;
23
+ }
24
+
25
+ const response = await fetchImpl(`${baseUrl}${path}`, {
26
+ method: 'GET',
27
+ headers,
28
+ });
29
+
30
+ const payload = await readJsonResponse(response);
31
+ if (!response.ok) {
32
+ const errorMessage =
33
+ trimString(payload?.error) ||
34
+ trimString(payload?.message) ||
35
+ `Persona API request failed (${response.status})`;
36
+ const error = new Error(errorMessage);
37
+ error.status = response.status;
38
+ error.payload = payload;
39
+ throw error;
40
+ }
41
+
42
+ return payload;
43
+ }
44
+
45
+ async function postJson({ fetchImpl, backendUrl, path, deviceToken, body }) {
46
+ const baseUrl = stripTrailingSlash(backendUrl);
47
+ if (!baseUrl) {
48
+ throw new Error('Backend URL is required.');
49
+ }
50
+ const token = trimString(deviceToken);
51
+ if (!token) {
52
+ throw new Error('Device token is required.');
53
+ }
54
+
55
+ const response = await fetchImpl(`${baseUrl}${path}`, {
56
+ method: 'POST',
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ Authorization: `Bearer ${token}`,
60
+ },
61
+ body: JSON.stringify(body),
62
+ });
63
+
64
+ const payload = await readJsonResponse(response);
65
+ if (!response.ok) {
66
+ const errorMessage =
67
+ trimString(payload?.error) ||
68
+ trimString(payload?.message) ||
69
+ `Persona API request failed (${response.status})`;
70
+ const error = new Error(errorMessage);
71
+ error.status = response.status;
72
+ error.payload = payload;
73
+ throw error;
74
+ }
75
+
76
+ return payload;
77
+ }
78
+
79
+ export function createPersonaApiClient({
80
+ backendUrl,
81
+ deviceToken,
82
+ deviceId,
83
+ fetchImpl = globalThis.fetch,
84
+ }) {
85
+ if (typeof fetchImpl !== 'function') {
86
+ throw new Error('fetch implementation is required.');
87
+ }
88
+
89
+ const resolvedBackendUrl = stripTrailingSlash(backendUrl);
90
+ const resolvedDeviceToken = trimString(deviceToken);
91
+ const resolvedDeviceId = trimString(deviceId);
92
+
93
+ function withDevice(body = {}) {
94
+ if (!resolvedDeviceId) {
95
+ return body;
96
+ }
97
+ return {
98
+ ...body,
99
+ deviceId: resolvedDeviceId,
100
+ };
101
+ }
102
+
103
+ return {
104
+ getPersona({
105
+ slug,
106
+ }) {
107
+ const safeSlug = trimString(slug);
108
+ if (!safeSlug) {
109
+ throw new Error('Persona slug is required.');
110
+ }
111
+
112
+ return getJson({
113
+ fetchImpl,
114
+ backendUrl: resolvedBackendUrl,
115
+ deviceToken: resolvedDeviceToken,
116
+ path: `/v1/personas/${encodeURIComponent(safeSlug)}`,
117
+ });
118
+ },
119
+
120
+ createManagedPersona({
121
+ slug,
122
+ name,
123
+ description,
124
+ templateType = 'persona-app',
125
+ promptTemplateVersion = 'v1',
126
+ }) {
127
+ const safeName = trimString(name);
128
+ if (!safeName) {
129
+ throw new Error('Persona name is required.');
130
+ }
131
+
132
+ const body = withDevice({
133
+ name: safeName,
134
+ description: trimString(description) || safeName,
135
+ templateType: trimString(templateType) || 'persona-app',
136
+ promptTemplateVersion: trimString(promptTemplateVersion) || 'v1',
137
+ });
138
+ const safeSlug = trimString(slug);
139
+ if (safeSlug) {
140
+ body.slug = safeSlug;
141
+ }
142
+
143
+ return postJson({
144
+ fetchImpl,
145
+ backendUrl: resolvedBackendUrl,
146
+ deviceToken: resolvedDeviceToken,
147
+ path: '/v1/personas/managed_create',
148
+ body,
149
+ });
150
+ },
151
+
152
+ registerRuntime({
153
+ slug,
154
+ endpoint,
155
+ healthcheckUrl,
156
+ transport = 'local',
157
+ localPort,
158
+ startedAt,
159
+ }) {
160
+ const safeSlug = trimString(slug);
161
+ if (!safeSlug) {
162
+ throw new Error('Persona slug is required.');
163
+ }
164
+
165
+ return postJson({
166
+ fetchImpl,
167
+ backendUrl: resolvedBackendUrl,
168
+ deviceToken: resolvedDeviceToken,
169
+ path: `/v1/personas/${encodeURIComponent(safeSlug)}/runtime_register`,
170
+ body: withDevice({
171
+ endpoint,
172
+ healthcheckUrl,
173
+ transport,
174
+ localPort,
175
+ startedAt,
176
+ }),
177
+ });
178
+ },
179
+
180
+ heartbeatRuntime({
181
+ slug,
182
+ endpoint,
183
+ healthcheckUrl,
184
+ transport = 'local',
185
+ localPort,
186
+ observedAt,
187
+ }) {
188
+ const safeSlug = trimString(slug);
189
+ if (!safeSlug) {
190
+ throw new Error('Persona slug is required.');
191
+ }
192
+
193
+ return postJson({
194
+ fetchImpl,
195
+ backendUrl: resolvedBackendUrl,
196
+ deviceToken: resolvedDeviceToken,
197
+ path: `/v1/personas/${encodeURIComponent(safeSlug)}/heartbeat`,
198
+ body: withDevice({
199
+ endpoint,
200
+ healthcheckUrl,
201
+ transport,
202
+ localPort,
203
+ observedAt,
204
+ }),
205
+ });
206
+ },
207
+
208
+ failRuntime({
209
+ slug,
210
+ code,
211
+ message,
212
+ }) {
213
+ const safeSlug = trimString(slug);
214
+ if (!safeSlug) {
215
+ throw new Error('Persona slug is required.');
216
+ }
217
+
218
+ return postJson({
219
+ fetchImpl,
220
+ backendUrl: resolvedBackendUrl,
221
+ deviceToken: resolvedDeviceToken,
222
+ path: `/v1/personas/${encodeURIComponent(safeSlug)}/fail`,
223
+ body: withDevice({
224
+ code,
225
+ message,
226
+ }),
227
+ });
228
+ },
229
+
230
+ startJob({
231
+ jobId,
232
+ startedAt,
233
+ }) {
234
+ const safeJobId = trimString(jobId);
235
+ if (!safeJobId) {
236
+ throw new Error('Persona job id is required.');
237
+ }
238
+
239
+ return postJson({
240
+ fetchImpl,
241
+ backendUrl: resolvedBackendUrl,
242
+ deviceToken: resolvedDeviceToken,
243
+ path: `/v1/persona_jobs/${encodeURIComponent(safeJobId)}/start`,
244
+ body: withDevice({
245
+ startedAt,
246
+ }),
247
+ });
248
+ },
249
+
250
+ succeedJob({
251
+ jobId,
252
+ workspacePath,
253
+ localPort,
254
+ transport = 'local',
255
+ endpoint,
256
+ healthcheckUrl,
257
+ completedAt,
258
+ }) {
259
+ const safeJobId = trimString(jobId);
260
+ if (!safeJobId) {
261
+ throw new Error('Persona job id is required.');
262
+ }
263
+
264
+ return postJson({
265
+ fetchImpl,
266
+ backendUrl: resolvedBackendUrl,
267
+ deviceToken: resolvedDeviceToken,
268
+ path: `/v1/persona_jobs/${encodeURIComponent(safeJobId)}/succeed`,
269
+ body: withDevice({
270
+ workspacePath,
271
+ localPort,
272
+ transport,
273
+ endpoint,
274
+ healthcheckUrl,
275
+ completedAt,
276
+ }),
277
+ });
278
+ },
279
+
280
+ failJob({
281
+ jobId,
282
+ code,
283
+ message,
284
+ completedAt,
285
+ }) {
286
+ const safeJobId = trimString(jobId);
287
+ if (!safeJobId) {
288
+ throw new Error('Persona job id is required.');
289
+ }
290
+
291
+ return postJson({
292
+ fetchImpl,
293
+ backendUrl: resolvedBackendUrl,
294
+ deviceToken: resolvedDeviceToken,
295
+ path: `/v1/persona_jobs/${encodeURIComponent(safeJobId)}/fail`,
296
+ body: withDevice({
297
+ code,
298
+ message,
299
+ completedAt,
300
+ }),
301
+ });
302
+ },
303
+ };
304
+ }
@@ -17,6 +17,7 @@ export async function executePersonaJob({
17
17
  startWorkspace = async () => ({ pid: null, logFilePath: '' }),
18
18
  waitForRuntime = async () => {},
19
19
  registerRuntime = async () => {},
20
+ destroyWorkspace = async () => ({ deleted: false }),
20
21
  onJobStart = async () => {},
21
22
  onJobSuccess = async () => {},
22
23
  onJobFailure = async () => {},
@@ -27,17 +28,40 @@ export async function executePersonaJob({
27
28
  throw new Error('Persona job payload is missing jobId.');
28
29
  }
29
30
 
30
- try {
31
- if (payload.jobType !== 'create_persona_runtime') {
32
- throw new Error(`Unsupported persona job type: ${payload.jobType || 'unknown'}`);
33
- }
34
-
35
- await onJobStart({ jobId, payload });
36
-
37
- const persona = payload.persona && typeof payload.persona === 'object' ? payload.persona : {};
38
- const scaffold = payload.scaffold && typeof payload.scaffold === 'object' ? payload.scaffold : {};
39
- const templateVersion = String(persona.templateVersion || 'v1').trim() || 'v1';
40
-
31
+ try {
32
+ if (!['create_persona_runtime', 'destroy_persona_runtime'].includes(payload.jobType)) {
33
+ throw new Error(`Unsupported persona job type: ${payload.jobType || 'unknown'}`);
34
+ }
35
+
36
+ await onJobStart({ jobId, payload });
37
+
38
+ const persona = payload.persona && typeof payload.persona === 'object' ? payload.persona : {};
39
+ const scaffold = payload.scaffold && typeof payload.scaffold === 'object' ? payload.scaffold : {};
40
+ const templateVersion = String(persona.templateVersion || 'v1').trim() || 'v1';
41
+
42
+ if (payload.jobType === 'destroy_persona_runtime') {
43
+ const workspacePath = String(scaffold.outDir || '').trim();
44
+ if (!workspacePath) {
45
+ throw new Error('Destroy persona job payload is missing scaffold.outDir.');
46
+ }
47
+
48
+ const result = {
49
+ workspacePath,
50
+ ...(await destroyWorkspace({
51
+ payload,
52
+ workspacePath,
53
+ })),
54
+ };
55
+
56
+ await onJobSuccess({ jobId, payload, result });
57
+
58
+ return {
59
+ ok: true,
60
+ jobId,
61
+ result,
62
+ };
63
+ }
64
+
41
65
  const scaffoldResult = scaffoldPersonaApp({
42
66
  slug: String(persona.slug || '').trim(),
43
67
  name: String(persona.name || '').trim(),
@@ -0,0 +1,36 @@
1
+ import net from 'node:net';
2
+
3
+ function listenOnce({ port, host }) {
4
+ return new Promise((resolve) => {
5
+ const server = net.createServer();
6
+ server.unref();
7
+
8
+ server.once('error', () => {
9
+ resolve(false);
10
+ });
11
+
12
+ server.listen({ port, host }, () => {
13
+ server.close(() => resolve(true));
14
+ });
15
+ });
16
+ }
17
+
18
+ export async function findAvailablePort({
19
+ preferredPort,
20
+ host = '127.0.0.1',
21
+ basePort = 4789,
22
+ maxAttempts = 50,
23
+ } = {}) {
24
+ const preferred = Number(preferredPort);
25
+ const startPort = Number.isFinite(preferred) && preferred > 0 ? Math.floor(preferred) : Math.floor(basePort);
26
+
27
+ for (let offset = 0; offset < maxAttempts; offset += 1) {
28
+ const port = startPort + offset;
29
+ const available = await listenOnce({ port, host });
30
+ if (available) {
31
+ return port;
32
+ }
33
+ }
34
+
35
+ throw new Error(`Unable to find an available port starting at ${startPort}.`);
36
+ }