oomi-ai 0.2.28 → 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.
@@ -0,0 +1,193 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { createPersonaApiClient } from './personaApiClient.js';
5
+ import { launchManagedPersonaRuntime } from './personaRuntimeManager.js';
6
+ import { readPersonaRuntimeState } from './personaRuntimeRegistry.js';
7
+ import { isPersonaWorkspaceProcessRunning } from './personaRuntimeProcess.js';
8
+
9
+ function trimString(value) {
10
+ return typeof value === 'string' ? value.trim() : '';
11
+ }
12
+
13
+ function wait(ms) {
14
+ return new Promise((resolve) => {
15
+ setTimeout(resolve, ms);
16
+ });
17
+ }
18
+
19
+ function listWorkspacePaths(workspaceRoot) {
20
+ const safeRoot = trimString(workspaceRoot);
21
+ if (!safeRoot || !fs.existsSync(safeRoot)) {
22
+ return [];
23
+ }
24
+
25
+ return fs
26
+ .readdirSync(safeRoot, { withFileTypes: true })
27
+ .filter((entry) => entry.isDirectory())
28
+ .map((entry) => path.join(safeRoot, entry.name));
29
+ }
30
+
31
+ async function healthcheckOk(url) {
32
+ const safeUrl = trimString(url);
33
+ if (!safeUrl) {
34
+ return false;
35
+ }
36
+
37
+ try {
38
+ const response = await fetch(safeUrl, {
39
+ method: 'GET',
40
+ headers: {
41
+ Accept: 'application/json',
42
+ },
43
+ });
44
+ return response.ok;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ async function reconcileWorkspace({
51
+ workspacePath,
52
+ workspaceRoot,
53
+ client,
54
+ logger,
55
+ autoRestart,
56
+ }) {
57
+ const state = readPersonaRuntimeState(workspacePath);
58
+ const slug = trimString(state.slug);
59
+ if (!slug || trimString(state.status) !== 'running') {
60
+ return;
61
+ }
62
+
63
+ const runtime = {
64
+ slug,
65
+ endpoint: trimString(state.endpoint || state.entryUrl || state.localEndpoint),
66
+ healthcheckUrl: trimString(state.healthcheckUrl),
67
+ transport: trimString(state.transport) || 'local',
68
+ localPort: Number.isFinite(Number(state.localPort)) ? Number(state.localPort) : null,
69
+ };
70
+
71
+ const processRunning = isPersonaWorkspaceProcessRunning(state.pid, {
72
+ workspacePath,
73
+ expectedCommand: state.devCommand,
74
+ localPort: runtime.localPort,
75
+ });
76
+
77
+ let effectiveRuntime = runtime;
78
+ if (!processRunning) {
79
+ if (!autoRestart) {
80
+ return;
81
+ }
82
+
83
+ try {
84
+ const launchResult = await launchManagedPersonaRuntime({
85
+ slug,
86
+ name: trimString(state.name) || slug,
87
+ description: trimString(state.description) || trimString(state.name) || slug,
88
+ workspaceRoot,
89
+ templateVersion: trimString(state.templateVersion) || 'v1',
90
+ forceInstall: false,
91
+ restart: false,
92
+ logFilePath: trimString(state.logFilePath),
93
+ entryUrl: '',
94
+ transport: 'local',
95
+ });
96
+
97
+ effectiveRuntime = {
98
+ slug,
99
+ endpoint: launchResult.runtime.endpoint,
100
+ healthcheckUrl: launchResult.runtime.healthcheckUrl,
101
+ transport: launchResult.runtime.transport,
102
+ localPort: launchResult.runtime.localPort,
103
+ };
104
+
105
+ await client.registerRuntime({
106
+ slug,
107
+ endpoint: effectiveRuntime.endpoint,
108
+ healthcheckUrl: effectiveRuntime.healthcheckUrl,
109
+ localPort: effectiveRuntime.localPort,
110
+ transport: effectiveRuntime.transport,
111
+ startedAt: new Date().toISOString(),
112
+ });
113
+ } catch (error) {
114
+ logger.warn?.(
115
+ `[persona-runtime] restart failed for ${slug}: ${
116
+ error instanceof Error ? error.message : String(error)
117
+ }`
118
+ );
119
+ return;
120
+ }
121
+ }
122
+
123
+ const healthy = await healthcheckOk(effectiveRuntime.healthcheckUrl);
124
+ if (!healthy) {
125
+ return;
126
+ }
127
+
128
+ try {
129
+ await client.heartbeatRuntime({
130
+ slug,
131
+ endpoint: effectiveRuntime.endpoint,
132
+ healthcheckUrl: effectiveRuntime.healthcheckUrl,
133
+ localPort: effectiveRuntime.localPort,
134
+ transport: effectiveRuntime.transport,
135
+ observedAt: new Date().toISOString(),
136
+ });
137
+ } catch (error) {
138
+ logger.warn?.(
139
+ `[persona-runtime] heartbeat failed for ${slug}: ${
140
+ error instanceof Error ? error.message : String(error)
141
+ }`
142
+ );
143
+ }
144
+ }
145
+
146
+ export function startPersonaRuntimeSupervisor({
147
+ backendUrl,
148
+ deviceToken,
149
+ workspaceRoot,
150
+ fetchImpl = globalThis.fetch,
151
+ intervalMs = 30000,
152
+ logger = console,
153
+ autoRestart = true,
154
+ }) {
155
+ const client = createPersonaApiClient({
156
+ backendUrl,
157
+ deviceToken,
158
+ fetchImpl,
159
+ });
160
+
161
+ let stopped = false;
162
+ let loopPromise = null;
163
+
164
+ async function runLoop() {
165
+ while (!stopped) {
166
+ const workspaces = listWorkspacePaths(workspaceRoot);
167
+ for (const workspacePath of workspaces) {
168
+ if (stopped) break;
169
+ await reconcileWorkspace({
170
+ workspacePath,
171
+ workspaceRoot,
172
+ client,
173
+ logger,
174
+ autoRestart,
175
+ });
176
+ }
177
+
178
+ if (stopped) {
179
+ break;
180
+ }
181
+ await wait(intervalMs);
182
+ }
183
+ }
184
+
185
+ loopPromise = runLoop();
186
+
187
+ return {
188
+ stop() {
189
+ stopped = true;
190
+ },
191
+ completed: loopPromise,
192
+ };
193
+ }
@@ -2,7 +2,7 @@
2
2
  "id": "oomi-ai",
3
3
  "name": "Oomi Channel Plugin",
4
4
  "description": "Managed Oomi channel integration for OpenClaw.",
5
- "version": "0.2.28",
5
+ "version": "0.2.38",
6
6
  "author": "Oomi",
7
7
  "license": "MIT",
8
8
  "openclawVersion": ">=0.5.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oomi-ai",
3
- "version": "0.2.28",
3
+ "version": "0.2.38",
4
4
  "description": "Oomi OpenClaw channel plugin and bridge tooling",
5
5
  "bin": {
6
6
  "oomi": "bin/oomi-ai.js"