oomi-ai 0.2.17 → 0.2.18

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 (47) hide show
  1. package/README.md +237 -202
  2. package/agent_instructions.md +209 -186
  3. package/bin/oomi-ai.js +3989 -3460
  4. package/bin/sessionBridgeState.js +78 -78
  5. package/lib/channelPluginClient.js +119 -0
  6. package/lib/personaApiClient.js +221 -0
  7. package/lib/personaJobExecutor.js +115 -0
  8. package/lib/personaJobPoller.js +112 -0
  9. package/lib/personaRuntimeProcess.js +152 -0
  10. package/lib/scaffold.js +108 -0
  11. package/lib/template.js +45 -0
  12. package/openclaw.extension.js +602 -602
  13. package/openclaw.plugin.json +17 -17
  14. package/package.json +67 -65
  15. package/skills/oomi/SKILL.md +191 -191
  16. package/skills/oomi/agent_instructions.md +80 -80
  17. package/skills/oomi/config.json +2 -2
  18. package/skills/oomi/scripts/get_avatar_capabilities.py +40 -40
  19. package/skills/oomi/scripts/get_data.py +49 -49
  20. package/skills/oomi/scripts/install_agent_instructions.py +78 -78
  21. package/skills/oomi/scripts/send_goal.py +53 -53
  22. package/skills/oomi/scripts/sync.py +46 -46
  23. package/skills/oomi/setup.py +41 -41
  24. package/templates/persona-app/.env.example +8 -0
  25. package/templates/persona-app/README.md +35 -0
  26. package/templates/persona-app/eslint.config.js +28 -0
  27. package/templates/persona-app/index.html +18 -0
  28. package/templates/persona-app/oomi.runtime.json +13 -0
  29. package/templates/persona-app/package.json +42 -0
  30. package/templates/persona-app/persona/brief.md +14 -0
  31. package/templates/persona-app/persona.json +14 -0
  32. package/templates/persona-app/public/manifest.webmanifest +8 -0
  33. package/templates/persona-app/public/oomi.health.json +6 -0
  34. package/templates/persona-app/src/App.css +180 -0
  35. package/templates/persona-app/src/App.tsx +14 -0
  36. package/templates/persona-app/src/index.css +32 -0
  37. package/templates/persona-app/src/main.tsx +10 -0
  38. package/templates/persona-app/src/pages/HomePage.tsx +73 -0
  39. package/templates/persona-app/src/pages/ScenePage.tsx +18 -0
  40. package/templates/persona-app/src/persona/config.ts +6 -0
  41. package/templates/persona-app/src/persona/notes.ts +5 -0
  42. package/templates/persona-app/src/vite-env.d.ts +3 -0
  43. package/templates/persona-app/template.json +13 -0
  44. package/templates/persona-app/tsconfig.app.json +23 -0
  45. package/templates/persona-app/tsconfig.json +7 -0
  46. package/templates/persona-app/tsconfig.node.json +21 -0
  47. package/templates/persona-app/vite.config.ts +18 -0
@@ -1,78 +1,78 @@
1
- const WS_CONNECTING = 0;
2
- const WS_OPEN = 1;
3
-
4
- /**
5
- * Ensure session state exists so client frames can be buffered before client.open arrives.
6
- */
7
- export function ensureSessionBridge({ sessions, sessionId, createSocket }) {
8
- const id = String(sessionId || '').trim();
9
- if (!id) return null;
10
-
11
- const existing = sessions.get(id);
12
- if (existing) return existing;
13
-
14
- const socket = createSocket(id);
15
- const next = {
16
- socket,
17
- queue: [],
18
- connectAccepted: false,
19
- waitingForConnect: [],
20
- };
21
- sessions.set(id, next);
22
- return next;
23
- }
24
-
25
- /**
26
- * Forward a frame to the gateway socket or queue it while connecting.
27
- */
28
- export function forwardFrameToSession(sessionBridge, frameText, options = {}) {
29
- if (!sessionBridge || !sessionBridge.socket || typeof frameText !== 'string' || !frameText) {
30
- return 'dropped';
31
- }
32
-
33
- if (options.requiresConnectAccepted === true && sessionBridge.connectAccepted !== true) {
34
- if (!Array.isArray(sessionBridge.waitingForConnect)) {
35
- sessionBridge.waitingForConnect = [];
36
- }
37
- sessionBridge.waitingForConnect.push(frameText);
38
- return 'waiting_for_connect';
39
- }
40
-
41
- const { socket } = sessionBridge;
42
- if (socket.readyState === WS_OPEN) {
43
- socket.send(frameText);
44
- return 'sent';
45
- }
46
-
47
- if (socket.readyState === WS_CONNECTING) {
48
- sessionBridge.queue.push(frameText);
49
- return 'queued';
50
- }
51
-
52
- return 'dropped';
53
- }
54
-
55
- export function flushWaitingForConnect(sessionBridge) {
56
- if (!sessionBridge) return [];
57
-
58
- sessionBridge.connectAccepted = true;
59
- const pending = Array.isArray(sessionBridge.waitingForConnect)
60
- ? sessionBridge.waitingForConnect.splice(0, sessionBridge.waitingForConnect.length)
61
- : [];
62
-
63
- return pending.map((frameText) => ({
64
- frameText,
65
- result: forwardFrameToSession(sessionBridge, frameText),
66
- }));
67
- }
68
-
69
- export function flushSessionQueue(sessionBridge) {
70
- if (!sessionBridge || !sessionBridge.socket) return;
71
- const socket = sessionBridge.socket;
72
- while (sessionBridge.queue.length > 0 && socket.readyState === WS_OPEN) {
73
- const nextFrame = sessionBridge.queue.shift();
74
- if (typeof nextFrame === 'string' && nextFrame) {
75
- socket.send(nextFrame);
76
- }
77
- }
78
- }
1
+ const WS_CONNECTING = 0;
2
+ const WS_OPEN = 1;
3
+
4
+ /**
5
+ * Ensure session state exists so client frames can be buffered before client.open arrives.
6
+ */
7
+ export function ensureSessionBridge({ sessions, sessionId, createSocket }) {
8
+ const id = String(sessionId || '').trim();
9
+ if (!id) return null;
10
+
11
+ const existing = sessions.get(id);
12
+ if (existing) return existing;
13
+
14
+ const socket = createSocket(id);
15
+ const next = {
16
+ socket,
17
+ queue: [],
18
+ connectAccepted: false,
19
+ waitingForConnect: [],
20
+ };
21
+ sessions.set(id, next);
22
+ return next;
23
+ }
24
+
25
+ /**
26
+ * Forward a frame to the gateway socket or queue it while connecting.
27
+ */
28
+ export function forwardFrameToSession(sessionBridge, frameText, options = {}) {
29
+ if (!sessionBridge || !sessionBridge.socket || typeof frameText !== 'string' || !frameText) {
30
+ return 'dropped';
31
+ }
32
+
33
+ if (options.requiresConnectAccepted === true && sessionBridge.connectAccepted !== true) {
34
+ if (!Array.isArray(sessionBridge.waitingForConnect)) {
35
+ sessionBridge.waitingForConnect = [];
36
+ }
37
+ sessionBridge.waitingForConnect.push(frameText);
38
+ return 'waiting_for_connect';
39
+ }
40
+
41
+ const { socket } = sessionBridge;
42
+ if (socket.readyState === WS_OPEN) {
43
+ socket.send(frameText);
44
+ return 'sent';
45
+ }
46
+
47
+ if (socket.readyState === WS_CONNECTING) {
48
+ sessionBridge.queue.push(frameText);
49
+ return 'queued';
50
+ }
51
+
52
+ return 'dropped';
53
+ }
54
+
55
+ export function flushWaitingForConnect(sessionBridge) {
56
+ if (!sessionBridge) return [];
57
+
58
+ sessionBridge.connectAccepted = true;
59
+ const pending = Array.isArray(sessionBridge.waitingForConnect)
60
+ ? sessionBridge.waitingForConnect.splice(0, sessionBridge.waitingForConnect.length)
61
+ : [];
62
+
63
+ return pending.map((frameText) => ({
64
+ frameText,
65
+ result: forwardFrameToSession(sessionBridge, frameText),
66
+ }));
67
+ }
68
+
69
+ export function flushSessionQueue(sessionBridge) {
70
+ if (!sessionBridge || !sessionBridge.socket) return;
71
+ const socket = sessionBridge.socket;
72
+ while (sessionBridge.queue.length > 0 && socket.readyState === WS_OPEN) {
73
+ const nextFrame = sessionBridge.queue.shift();
74
+ if (typeof nextFrame === 'string' && nextFrame) {
75
+ socket.send(nextFrame);
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,119 @@
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 requestJson({
14
+ fetchImpl,
15
+ backendUrl,
16
+ path,
17
+ deviceToken,
18
+ method = 'POST',
19
+ body,
20
+ }) {
21
+ const baseUrl = stripTrailingSlash(backendUrl);
22
+ if (!baseUrl) {
23
+ throw new Error('Backend URL is required.');
24
+ }
25
+ const token = trimString(deviceToken);
26
+ if (!token) {
27
+ throw new Error('Device token is required.');
28
+ }
29
+
30
+ const response = await fetchImpl(`${baseUrl}${path}`, {
31
+ method,
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ Authorization: `Bearer ${token}`,
35
+ },
36
+ body: body === undefined ? undefined : JSON.stringify(body),
37
+ });
38
+
39
+ const payload = await readJsonResponse(response);
40
+ if (!response.ok) {
41
+ const errorMessage =
42
+ trimString(payload?.error) ||
43
+ trimString(payload?.message) ||
44
+ `Channel plugin request failed (${response.status})`;
45
+ throw new Error(errorMessage);
46
+ }
47
+
48
+ return payload;
49
+ }
50
+
51
+ export function createChannelPluginClient({
52
+ backendUrl,
53
+ deviceToken,
54
+ fetchImpl = globalThis.fetch,
55
+ }) {
56
+ if (typeof fetchImpl !== 'function') {
57
+ throw new Error('fetch implementation is required.');
58
+ }
59
+
60
+ const resolvedBackendUrl = stripTrailingSlash(backendUrl);
61
+ const resolvedDeviceToken = trimString(deviceToken);
62
+
63
+ return {
64
+ pollMessages({
65
+ limit = 20,
66
+ metadataType,
67
+ } = {}) {
68
+ const safeLimit = Number.isFinite(Number(limit)) ? Math.max(1, Math.min(100, Math.floor(Number(limit)))) : 20;
69
+ const payload = { limit: safeLimit };
70
+ const safeMetadataType = trimString(metadataType);
71
+ if (safeMetadataType) {
72
+ payload.metadataType = safeMetadataType;
73
+ }
74
+
75
+ return requestJson({
76
+ fetchImpl,
77
+ backendUrl: resolvedBackendUrl,
78
+ deviceToken: resolvedDeviceToken,
79
+ path: '/v1/channel/plugin/poll',
80
+ method: 'POST',
81
+ body: payload,
82
+ });
83
+ },
84
+
85
+ ackMessage({
86
+ messageId,
87
+ outcome = 'delivered',
88
+ failureCode,
89
+ }) {
90
+ const safeMessageId = trimString(messageId);
91
+ if (!safeMessageId) {
92
+ throw new Error('Channel message id is required.');
93
+ }
94
+
95
+ const safeOutcome = trimString(outcome) || 'delivered';
96
+ if (safeOutcome !== 'delivered' && safeOutcome !== 'failed') {
97
+ throw new Error('Ack outcome must be delivered or failed.');
98
+ }
99
+
100
+ const body = {
101
+ messageId: safeMessageId,
102
+ outcome: safeOutcome,
103
+ };
104
+ const safeFailureCode = trimString(failureCode);
105
+ if (safeFailureCode) {
106
+ body.failureCode = safeFailureCode;
107
+ }
108
+
109
+ return requestJson({
110
+ fetchImpl,
111
+ backendUrl: resolvedBackendUrl,
112
+ deviceToken: resolvedDeviceToken,
113
+ path: '/v1/channel/plugin/acks',
114
+ method: 'POST',
115
+ body,
116
+ });
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,221 @@
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
+ registerRuntime({
70
+ slug,
71
+ endpoint,
72
+ healthcheckUrl,
73
+ transport = 'local',
74
+ localPort,
75
+ startedAt,
76
+ }) {
77
+ const safeSlug = trimString(slug);
78
+ if (!safeSlug) {
79
+ throw new Error('Persona slug is required.');
80
+ }
81
+
82
+ return postJson({
83
+ fetchImpl,
84
+ backendUrl: resolvedBackendUrl,
85
+ deviceToken: resolvedDeviceToken,
86
+ path: `/v1/personas/${encodeURIComponent(safeSlug)}/runtime_register`,
87
+ body: withDevice({
88
+ endpoint,
89
+ healthcheckUrl,
90
+ transport,
91
+ localPort,
92
+ startedAt,
93
+ }),
94
+ });
95
+ },
96
+
97
+ heartbeatRuntime({
98
+ slug,
99
+ endpoint,
100
+ healthcheckUrl,
101
+ transport = 'local',
102
+ localPort,
103
+ observedAt,
104
+ }) {
105
+ const safeSlug = trimString(slug);
106
+ if (!safeSlug) {
107
+ throw new Error('Persona slug is required.');
108
+ }
109
+
110
+ return postJson({
111
+ fetchImpl,
112
+ backendUrl: resolvedBackendUrl,
113
+ deviceToken: resolvedDeviceToken,
114
+ path: `/v1/personas/${encodeURIComponent(safeSlug)}/heartbeat`,
115
+ body: withDevice({
116
+ endpoint,
117
+ healthcheckUrl,
118
+ transport,
119
+ localPort,
120
+ observedAt,
121
+ }),
122
+ });
123
+ },
124
+
125
+ failRuntime({
126
+ slug,
127
+ code,
128
+ message,
129
+ }) {
130
+ const safeSlug = trimString(slug);
131
+ if (!safeSlug) {
132
+ throw new Error('Persona slug is required.');
133
+ }
134
+
135
+ return postJson({
136
+ fetchImpl,
137
+ backendUrl: resolvedBackendUrl,
138
+ deviceToken: resolvedDeviceToken,
139
+ path: `/v1/personas/${encodeURIComponent(safeSlug)}/fail`,
140
+ body: withDevice({
141
+ code,
142
+ message,
143
+ }),
144
+ });
145
+ },
146
+
147
+ startJob({
148
+ jobId,
149
+ startedAt,
150
+ }) {
151
+ const safeJobId = trimString(jobId);
152
+ if (!safeJobId) {
153
+ throw new Error('Persona job id is required.');
154
+ }
155
+
156
+ return postJson({
157
+ fetchImpl,
158
+ backendUrl: resolvedBackendUrl,
159
+ deviceToken: resolvedDeviceToken,
160
+ path: `/v1/persona_jobs/${encodeURIComponent(safeJobId)}/start`,
161
+ body: withDevice({
162
+ startedAt,
163
+ }),
164
+ });
165
+ },
166
+
167
+ succeedJob({
168
+ jobId,
169
+ workspacePath,
170
+ localPort,
171
+ transport = 'local',
172
+ endpoint,
173
+ healthcheckUrl,
174
+ completedAt,
175
+ }) {
176
+ const safeJobId = trimString(jobId);
177
+ if (!safeJobId) {
178
+ throw new Error('Persona job id is required.');
179
+ }
180
+
181
+ return postJson({
182
+ fetchImpl,
183
+ backendUrl: resolvedBackendUrl,
184
+ deviceToken: resolvedDeviceToken,
185
+ path: `/v1/persona_jobs/${encodeURIComponent(safeJobId)}/succeed`,
186
+ body: withDevice({
187
+ workspacePath,
188
+ localPort,
189
+ transport,
190
+ endpoint,
191
+ healthcheckUrl,
192
+ completedAt,
193
+ }),
194
+ });
195
+ },
196
+
197
+ failJob({
198
+ jobId,
199
+ code,
200
+ message,
201
+ completedAt,
202
+ }) {
203
+ const safeJobId = trimString(jobId);
204
+ if (!safeJobId) {
205
+ throw new Error('Persona job id is required.');
206
+ }
207
+
208
+ return postJson({
209
+ fetchImpl,
210
+ backendUrl: resolvedBackendUrl,
211
+ deviceToken: resolvedDeviceToken,
212
+ path: `/v1/persona_jobs/${encodeURIComponent(safeJobId)}/fail`,
213
+ body: withDevice({
214
+ code,
215
+ message,
216
+ completedAt,
217
+ }),
218
+ });
219
+ },
220
+ };
221
+ }
@@ -0,0 +1,115 @@
1
+ import { scaffoldPersonaApp } from './scaffold.js';
2
+
3
+ export function extractPersonaJobPayload(message = {}) {
4
+ const metadata = message.metadata && typeof message.metadata === 'object' ? message.metadata : {};
5
+ const payload = metadata.payload && typeof metadata.payload === 'object' ? metadata.payload : null;
6
+
7
+ if (metadata.type !== 'persona_job' || !payload) {
8
+ throw new Error('Message is not a persona job payload.');
9
+ }
10
+
11
+ return payload;
12
+ }
13
+
14
+ export async function executePersonaJob({
15
+ message,
16
+ installWorkspace = async () => {},
17
+ startWorkspace = async () => ({ pid: null, logFilePath: '' }),
18
+ waitForRuntime = async () => {},
19
+ registerRuntime = async () => {},
20
+ onJobStart = async () => {},
21
+ onJobSuccess = async () => {},
22
+ onJobFailure = async () => {},
23
+ }) {
24
+ const payload = extractPersonaJobPayload(message);
25
+ const jobId = String(payload.jobId || message?.metadata?.jobId || '').trim();
26
+ if (!jobId) {
27
+ throw new Error('Persona job payload is missing jobId.');
28
+ }
29
+
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
+
41
+ const scaffoldResult = scaffoldPersonaApp({
42
+ slug: String(persona.slug || '').trim(),
43
+ name: String(persona.name || '').trim(),
44
+ description: String(persona.description || '').trim(),
45
+ outDir: String(scaffold.outDir || '').trim(),
46
+ templateVersion,
47
+ force: true,
48
+ });
49
+
50
+ await installWorkspace({
51
+ payload,
52
+ workspacePath: scaffoldResult.outDir,
53
+ scaffoldResult,
54
+ });
55
+
56
+ const runtime = {
57
+ transport: 'local',
58
+ endpoint: `http://127.0.0.1:${scaffoldResult.defaultPort}`,
59
+ localPort: scaffoldResult.defaultPort,
60
+ healthcheckUrl: `http://127.0.0.1:${scaffoldResult.defaultPort}${scaffoldResult.healthPath}`,
61
+ };
62
+ const processInfo = await startWorkspace({
63
+ payload,
64
+ workspacePath: scaffoldResult.outDir,
65
+ scaffoldResult,
66
+ runtime,
67
+ });
68
+ await waitForRuntime({
69
+ payload,
70
+ workspacePath: scaffoldResult.outDir,
71
+ scaffoldResult,
72
+ runtime,
73
+ processInfo,
74
+ });
75
+
76
+ const result = {
77
+ workspacePath: scaffoldResult.outDir,
78
+ localPort: runtime.localPort,
79
+ transport: runtime.transport,
80
+ endpoint: runtime.endpoint,
81
+ healthcheckUrl: runtime.healthcheckUrl,
82
+ pid: processInfo?.pid || null,
83
+ logFilePath: processInfo?.logFilePath || '',
84
+ templateVersion,
85
+ };
86
+
87
+ await registerRuntime({ jobId, payload, result });
88
+ await onJobSuccess({ jobId, payload, result });
89
+
90
+ return {
91
+ ok: true,
92
+ jobId,
93
+ result,
94
+ };
95
+ } catch (error) {
96
+ const messageText = error instanceof Error ? error.message : 'Persona job execution failed.';
97
+ await onJobFailure({
98
+ jobId,
99
+ payload,
100
+ error: {
101
+ code: 'PERSONA_JOB_EXECUTION_FAILED',
102
+ message: messageText,
103
+ },
104
+ });
105
+
106
+ return {
107
+ ok: false,
108
+ jobId,
109
+ error: {
110
+ code: 'PERSONA_JOB_EXECUTION_FAILED',
111
+ message: messageText,
112
+ },
113
+ };
114
+ }
115
+ }