oomi-ai 0.2.16 → 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 +238 -203
  2. package/agent_instructions.md +209 -184
  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 -424
  13. package/openclaw.plugin.json +17 -17
  14. package/package.json +67 -65
  15. package/skills/oomi/SKILL.md +191 -170
  16. package/skills/oomi/agent_instructions.md +80 -78
  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
@@ -0,0 +1,112 @@
1
+ import { createChannelPluginClient } from './channelPluginClient.js';
2
+
3
+ function wait(ms) {
4
+ return new Promise((resolve) => {
5
+ setTimeout(resolve, ms);
6
+ });
7
+ }
8
+
9
+ function failureCodeFor(error) {
10
+ if (error && typeof error === 'object' && typeof error.code === 'string' && error.code.trim()) {
11
+ return error.code.trim();
12
+ }
13
+ return 'persona_job_delivery_failed';
14
+ }
15
+
16
+ export function startPersonaJobPoller({
17
+ backendUrl,
18
+ deviceToken,
19
+ onMessage,
20
+ fetchImpl = globalThis.fetch,
21
+ metadataType = 'persona_job',
22
+ pollIntervalMs = 3000,
23
+ idleIntervalMs = 3000,
24
+ logger = console,
25
+ }) {
26
+ if (typeof onMessage !== 'function') {
27
+ throw new Error('onMessage callback is required.');
28
+ }
29
+
30
+ const client = createChannelPluginClient({
31
+ backendUrl,
32
+ deviceToken,
33
+ fetchImpl,
34
+ });
35
+
36
+ let stopped = false;
37
+ let activeLoop = null;
38
+
39
+ async function loop() {
40
+ while (!stopped) {
41
+ try {
42
+ const payload = await client.pollMessages({
43
+ limit: 10,
44
+ metadataType,
45
+ });
46
+ const messages = Array.isArray(payload?.messages) ? payload.messages : [];
47
+
48
+ if (messages.length === 0) {
49
+ await wait(idleIntervalMs);
50
+ continue;
51
+ }
52
+
53
+ for (const message of messages) {
54
+ if (stopped) break;
55
+
56
+ try {
57
+ await onMessage(message);
58
+ await client.ackMessage({
59
+ messageId: message?.messageId,
60
+ outcome: 'delivered',
61
+ });
62
+ } catch (error) {
63
+ const messageId = typeof message?.messageId === 'string' ? message.messageId : '';
64
+ try {
65
+ if (messageId) {
66
+ await client.ackMessage({
67
+ messageId,
68
+ outcome: 'failed',
69
+ failureCode: failureCodeFor(error),
70
+ });
71
+ }
72
+ } catch (ackError) {
73
+ logger.error?.(
74
+ `[persona-jobs] failed to ack message ${messageId || 'unknown'}: ${
75
+ ackError instanceof Error ? ackError.message : String(ackError)
76
+ }`
77
+ );
78
+ }
79
+
80
+ logger.error?.(
81
+ `[persona-jobs] execution failed for ${messageId || 'unknown'}: ${
82
+ error instanceof Error ? error.message : String(error)
83
+ }`
84
+ );
85
+ }
86
+ }
87
+
88
+ if (stopped) {
89
+ break;
90
+ }
91
+ await wait(pollIntervalMs);
92
+ } catch (error) {
93
+ logger.error?.(
94
+ `[persona-jobs] poll failed: ${error instanceof Error ? error.message : String(error)}`
95
+ );
96
+ if (stopped) {
97
+ break;
98
+ }
99
+ await wait(idleIntervalMs);
100
+ }
101
+ }
102
+ }
103
+
104
+ activeLoop = loop();
105
+
106
+ return {
107
+ stop() {
108
+ stopped = true;
109
+ },
110
+ completed: activeLoop,
111
+ };
112
+ }
@@ -0,0 +1,152 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+
6
+ function resolveNpmCommand() {
7
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm';
8
+ }
9
+
10
+ function ensureDir(dirPath) {
11
+ fs.mkdirSync(dirPath, { recursive: true });
12
+ }
13
+
14
+ function wait(ms) {
15
+ return new Promise((resolve) => {
16
+ setTimeout(resolve, ms);
17
+ });
18
+ }
19
+
20
+ function runProcess({ command, args, cwd }) {
21
+ return new Promise((resolve, reject) => {
22
+ const child = spawn(command, args, {
23
+ cwd,
24
+ stdio: 'inherit',
25
+ shell: false,
26
+ windowsHide: true,
27
+ });
28
+
29
+ child.on('error', reject);
30
+ child.on('exit', (code) => {
31
+ if (code === 0) {
32
+ resolve();
33
+ return;
34
+ }
35
+ reject(new Error(`${command} ${args.join(' ')} failed with exit code ${code ?? 'unknown'}.`));
36
+ });
37
+ });
38
+ }
39
+
40
+ export async function installPersonaWorkspace({
41
+ workspacePath,
42
+ }) {
43
+ if (!workspacePath) {
44
+ throw new Error('Workspace path is required.');
45
+ }
46
+
47
+ await runProcess({
48
+ command: resolveNpmCommand(),
49
+ args: ['install'],
50
+ cwd: workspacePath,
51
+ });
52
+ }
53
+
54
+ export function startPersonaWorkspace({
55
+ workspacePath,
56
+ logFilePath,
57
+ env = {},
58
+ }) {
59
+ if (!workspacePath) {
60
+ throw new Error('Workspace path is required.');
61
+ }
62
+
63
+ const resolvedLogFilePath =
64
+ logFilePath ||
65
+ path.join(workspacePath, '.oomi', 'runtime.log');
66
+ ensureDir(path.dirname(resolvedLogFilePath));
67
+
68
+ const output = fs.openSync(resolvedLogFilePath, 'a');
69
+ const child = spawn(resolveNpmCommand(), ['run', 'dev'], {
70
+ cwd: workspacePath,
71
+ detached: true,
72
+ stdio: ['ignore', output, output],
73
+ shell: false,
74
+ windowsHide: true,
75
+ env: {
76
+ ...process.env,
77
+ ...env,
78
+ },
79
+ });
80
+
81
+ child.unref();
82
+
83
+ return {
84
+ pid: child.pid,
85
+ logFilePath: resolvedLogFilePath,
86
+ };
87
+ }
88
+
89
+ async function fetchHealth(url) {
90
+ const response = await fetch(url, {
91
+ method: 'GET',
92
+ headers: {
93
+ Accept: 'application/json',
94
+ },
95
+ });
96
+
97
+ if (!response.ok) {
98
+ throw new Error(`Healthcheck returned ${response.status}.`);
99
+ }
100
+
101
+ return response;
102
+ }
103
+
104
+ export async function waitForPersonaRuntime({
105
+ healthcheckUrl,
106
+ timeoutMs = 45000,
107
+ intervalMs = 1000,
108
+ }) {
109
+ if (!healthcheckUrl) {
110
+ throw new Error('Healthcheck URL is required.');
111
+ }
112
+
113
+ const startedAt = Date.now();
114
+ let lastError = null;
115
+ while (Date.now() - startedAt <= timeoutMs) {
116
+ try {
117
+ await fetchHealth(healthcheckUrl);
118
+ return;
119
+ } catch (error) {
120
+ lastError = error;
121
+ await wait(intervalMs);
122
+ }
123
+ }
124
+
125
+ const message =
126
+ lastError instanceof Error
127
+ ? lastError.message
128
+ : 'Timed out waiting for persona runtime healthcheck.';
129
+ throw new Error(`Timed out waiting for persona runtime healthcheck: ${message}`);
130
+ }
131
+
132
+ export function buildLocalPersonaRuntime({
133
+ localPort,
134
+ healthPath,
135
+ }) {
136
+ const port = Number(localPort);
137
+ if (!Number.isFinite(port) || port <= 0) {
138
+ throw new Error('Local port is required.');
139
+ }
140
+
141
+ const endpoint = `http://127.0.0.1:${port}`;
142
+ return {
143
+ transport: 'local',
144
+ endpoint,
145
+ localPort: port,
146
+ healthcheckUrl: `${endpoint}${healthPath}`,
147
+ };
148
+ }
149
+
150
+ export function defaultPersonaWorkspaceRoot() {
151
+ return path.join(os.homedir(), '.openclaw', 'personas');
152
+ }
@@ -0,0 +1,108 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import {
5
+ DEFAULT_TEMPLATE_ID,
6
+ DEFAULT_TEMPLATE_VERSION,
7
+ readTemplateDescriptor,
8
+ renderTemplateFile,
9
+ resolveTemplateRoot,
10
+ } from './template.js';
11
+
12
+ function ensureDir(dirPath) {
13
+ fs.mkdirSync(dirPath, { recursive: true });
14
+ }
15
+
16
+ function validatePersonaSlug(slug) {
17
+ if (typeof slug !== 'string' || !slug.trim()) {
18
+ throw new Error('Persona slug is required.');
19
+ }
20
+
21
+ const normalized = slug.trim();
22
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(normalized)) {
23
+ throw new Error(
24
+ 'Persona slug must use lowercase letters, numbers, and single dashes only.',
25
+ );
26
+ }
27
+
28
+ return normalized;
29
+ }
30
+
31
+ function ensureWritableOutputDir(outDir, force) {
32
+ if (!outDir) {
33
+ throw new Error('Output directory is required. Use --out <path>.');
34
+ }
35
+
36
+ if (!fs.existsSync(outDir)) {
37
+ ensureDir(outDir);
38
+ return;
39
+ }
40
+
41
+ const entries = fs.readdirSync(outDir);
42
+ if (entries.length > 0 && !force) {
43
+ throw new Error(`Output directory is not empty: ${outDir}. Use --force to overwrite.`);
44
+ }
45
+ }
46
+
47
+ function copyTemplateTree(sourceRoot, targetRoot, variables) {
48
+ const entries = fs.readdirSync(sourceRoot, { withFileTypes: true });
49
+ for (const entry of entries) {
50
+ if (entry.name === 'template.json') {
51
+ continue;
52
+ }
53
+
54
+ const sourcePath = path.join(sourceRoot, entry.name);
55
+ const targetPath = path.join(targetRoot, entry.name);
56
+
57
+ if (entry.isDirectory()) {
58
+ ensureDir(targetPath);
59
+ copyTemplateTree(sourcePath, targetPath, variables);
60
+ continue;
61
+ }
62
+
63
+ const content = fs.readFileSync(sourcePath, 'utf8');
64
+ fs.writeFileSync(targetPath, renderTemplateFile(content, variables), 'utf8');
65
+ }
66
+ }
67
+
68
+ export function scaffoldPersonaApp({
69
+ slug,
70
+ name,
71
+ description,
72
+ outDir,
73
+ templateVersion = DEFAULT_TEMPLATE_VERSION,
74
+ templateId = DEFAULT_TEMPLATE_ID,
75
+ force = false,
76
+ }) {
77
+ const safeSlug = validatePersonaSlug(slug);
78
+ const safeName = typeof name === 'string' && name.trim() ? name.trim() : safeSlug;
79
+ const safeDescription = typeof description === 'string' ? description.trim() : '';
80
+ if (!safeDescription) {
81
+ throw new Error('Persona description is required. Use --description "<text>".');
82
+ }
83
+
84
+ const resolvedOutDir = path.resolve(outDir);
85
+ const templateRoot = resolveTemplateRoot(templateId, templateVersion);
86
+ const descriptor = readTemplateDescriptor(templateRoot);
87
+ const variables = {
88
+ __OOMI_PERSONA_SLUG__: safeSlug,
89
+ __OOMI_PERSONA_NAME__: safeName,
90
+ __OOMI_PERSONA_DESCRIPTION__: safeDescription,
91
+ __OOMI_TEMPLATE_VERSION__: templateVersion,
92
+ };
93
+
94
+ ensureWritableOutputDir(resolvedOutDir, force);
95
+ copyTemplateTree(templateRoot, resolvedOutDir, variables);
96
+
97
+ return {
98
+ ok: true,
99
+ templateId,
100
+ templateVersion,
101
+ slug: safeSlug,
102
+ outDir: resolvedOutDir,
103
+ startCommand: `cd ${resolvedOutDir} && npm install && npm run dev`,
104
+ healthPath: descriptor.healthPath,
105
+ editableZones: descriptor.editableZones,
106
+ defaultPort: descriptor.defaultPort,
107
+ };
108
+ }
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ export const DEFAULT_TEMPLATE_ID = 'persona-app';
6
+ export const DEFAULT_TEMPLATE_VERSION = 'v1';
7
+
8
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
+ const TEMPLATE_ROOT = path.join(PACKAGE_ROOT, 'templates');
10
+
11
+ export function readTemplateDescriptor(templateRoot) {
12
+ const descriptorPath = path.join(templateRoot, 'template.json');
13
+ if (!fs.existsSync(descriptorPath)) {
14
+ throw new Error(`Template descriptor not found: ${descriptorPath}`);
15
+ }
16
+
17
+ return JSON.parse(fs.readFileSync(descriptorPath, 'utf8'));
18
+ }
19
+
20
+ export function resolveTemplateRoot(
21
+ templateId = DEFAULT_TEMPLATE_ID,
22
+ version = DEFAULT_TEMPLATE_VERSION,
23
+ ) {
24
+ const templateRoot = path.join(TEMPLATE_ROOT, templateId);
25
+ if (!fs.existsSync(templateRoot)) {
26
+ throw new Error(`Unknown template: ${templateId}`);
27
+ }
28
+
29
+ const descriptor = readTemplateDescriptor(templateRoot);
30
+ if (descriptor.version !== version) {
31
+ throw new Error(
32
+ `Unsupported template version "${version}" for ${templateId}. Available version: ${descriptor.version}`,
33
+ );
34
+ }
35
+
36
+ return templateRoot;
37
+ }
38
+
39
+ export function renderTemplateFile(content, variables) {
40
+ let rendered = content;
41
+ for (const [token, value] of Object.entries(variables)) {
42
+ rendered = rendered.split(token).join(String(value));
43
+ }
44
+ return rendered;
45
+ }