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.
- package/README.md +258 -158
- package/bin/oomi-ai.js +2130 -1365
- package/lib/openclawDevGateway.js +384 -0
- package/lib/openclawPaths.js +78 -0
- package/lib/openclawProfile.js +265 -0
- package/lib/personaApiClient.js +304 -253
- package/lib/personaJobExecutor.js +35 -11
- package/lib/personaPortAllocator.js +36 -0
- package/lib/personaRuntimeManager.js +364 -0
- package/lib/personaRuntimeProcess.js +378 -121
- package/lib/personaRuntimeRegistry.js +67 -0
- package/lib/personaRuntimeSupervisor.js +193 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -1,152 +1,409 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
import { resolveOpenclawPersonasDir } from './openclawPaths.js';
|
|
6
|
+
|
|
7
|
+
function resolveNpmCommand() {
|
|
8
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function ensureDir(dirPath) {
|
|
12
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function quoteWindowsCommandPart(value) {
|
|
16
|
+
const text = String(value);
|
|
17
|
+
if (!/[\s"]/u.test(text)) {
|
|
18
|
+
return text;
|
|
19
|
+
}
|
|
20
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildWindowsCommandLine(command, args) {
|
|
24
|
+
const quotedCommand = quoteWindowsCommandPart(command);
|
|
25
|
+
const quotedArgs = args.map((value) => quoteWindowsCommandPart(value)).join(' ');
|
|
26
|
+
return `${quotedCommand}${quotedArgs ? ` ${quotedArgs}` : ''}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizePositiveInteger(value) {
|
|
30
|
+
const parsed = Number(value);
|
|
31
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return Math.floor(parsed);
|
|
8
35
|
}
|
|
9
36
|
|
|
10
|
-
function
|
|
11
|
-
|
|
37
|
+
function resolvePersonaBindHost() {
|
|
38
|
+
const value = String(process.env.OOMI_PERSONA_BIND_HOST || '').trim();
|
|
39
|
+
return value || '127.0.0.1';
|
|
12
40
|
}
|
|
13
41
|
|
|
14
|
-
function
|
|
15
|
-
return
|
|
16
|
-
setTimeout(resolve, ms);
|
|
17
|
-
});
|
|
42
|
+
function trimString(value) {
|
|
43
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
18
44
|
}
|
|
19
45
|
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return;
|
|
46
|
+
function readProcessCommandLine(pid) {
|
|
47
|
+
const safePid = normalizePositiveInteger(pid);
|
|
48
|
+
if (!safePid) {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (process.platform === 'linux') {
|
|
53
|
+
const procPath = `/proc/${safePid}/cmdline`;
|
|
54
|
+
if (fs.existsSync(procPath)) {
|
|
55
|
+
try {
|
|
56
|
+
return fs.readFileSync(procPath).toString().replace(/\u0000/g, ' ').trim();
|
|
57
|
+
} catch {
|
|
58
|
+
return '';
|
|
34
59
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
38
|
-
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
39
62
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
if (process.platform === 'win32') {
|
|
64
|
+
const result = spawnSync(
|
|
65
|
+
'powershell',
|
|
66
|
+
[
|
|
67
|
+
'-NoProfile',
|
|
68
|
+
'-Command',
|
|
69
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${safePid}").CommandLine`,
|
|
70
|
+
],
|
|
71
|
+
{
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
return trimString(result.stdout || '');
|
|
45
77
|
}
|
|
46
78
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
cwd: workspacePath,
|
|
79
|
+
const result = spawnSync('ps', ['-o', 'command=', '-p', String(safePid)], {
|
|
80
|
+
encoding: 'utf8',
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
51
82
|
});
|
|
83
|
+
return trimString(result.stdout || '');
|
|
52
84
|
}
|
|
53
85
|
|
|
54
|
-
export function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}) {
|
|
59
|
-
if (!workspacePath) {
|
|
60
|
-
throw new Error('Workspace path is required.');
|
|
86
|
+
export function matchesPersonaRuntimeCommand(commandLine, options = {}) {
|
|
87
|
+
const text = trimString(commandLine);
|
|
88
|
+
if (!text) {
|
|
89
|
+
return false;
|
|
61
90
|
}
|
|
62
91
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
});
|
|
92
|
+
const workspacePath = trimString(options.workspacePath);
|
|
93
|
+
const command = trimString(options.expectedCommand?.command);
|
|
94
|
+
const args = Array.isArray(options.expectedCommand?.args) ? options.expectedCommand.args : [];
|
|
95
|
+
const firstArg = trimString(args[0]);
|
|
96
|
+
const localPort = normalizePositiveInteger(options.localPort);
|
|
96
97
|
|
|
97
|
-
if (!
|
|
98
|
-
|
|
98
|
+
if (firstArg && !text.includes(firstArg)) {
|
|
99
|
+
return false;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
if (workspacePath && !text.includes(workspacePath)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
if (!healthcheckUrl) {
|
|
110
|
-
throw new Error('Healthcheck URL is required.');
|
|
106
|
+
if (localPort) {
|
|
107
|
+
const strictPortArg = `--port ${localPort}`;
|
|
108
|
+
if (!text.includes(strictPortArg) && !text.includes(`:${localPort}`)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
await fetchHealth(healthcheckUrl);
|
|
118
|
-
return;
|
|
119
|
-
} catch (error) {
|
|
120
|
-
lastError = error;
|
|
121
|
-
await wait(intervalMs);
|
|
113
|
+
if (command) {
|
|
114
|
+
const commandBase = path.basename(command);
|
|
115
|
+
if (!text.includes(command) && !text.includes(commandBase)) {
|
|
116
|
+
return false;
|
|
122
117
|
}
|
|
123
118
|
}
|
|
124
119
|
|
|
125
|
-
|
|
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}`);
|
|
120
|
+
return true;
|
|
130
121
|
}
|
|
131
122
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
123
|
+
function resolveDirectViteCommand({ workspacePath, localPort }) {
|
|
124
|
+
if (!workspacePath) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const viteScriptPath = path.join(workspacePath, 'node_modules', 'vite', 'bin', 'vite.js');
|
|
129
|
+
if (!fs.existsSync(viteScriptPath)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const port = normalizePositiveInteger(localPort);
|
|
134
|
+
const args = [viteScriptPath, '--host', resolvePersonaBindHost()];
|
|
135
|
+
if (port) {
|
|
136
|
+
args.push('--port', String(port), '--strictPort');
|
|
139
137
|
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
command: process.execPath,
|
|
141
|
+
args,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function wait(ms) {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
setTimeout(resolve, ms);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function runProcess({ command, args, cwd }) {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const child =
|
|
154
|
+
process.platform === 'win32'
|
|
155
|
+
? spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', buildWindowsCommandLine(command, args)], {
|
|
156
|
+
cwd,
|
|
157
|
+
stdio: 'inherit',
|
|
158
|
+
shell: false,
|
|
159
|
+
windowsHide: true,
|
|
160
|
+
})
|
|
161
|
+
: spawn(command, args, {
|
|
162
|
+
cwd,
|
|
163
|
+
stdio: 'inherit',
|
|
164
|
+
shell: false,
|
|
165
|
+
windowsHide: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
child.on('error', reject);
|
|
169
|
+
child.on('exit', (code) => {
|
|
170
|
+
if (code === 0) {
|
|
171
|
+
resolve();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
reject(new Error(`${command} ${args.join(' ')} failed with exit code ${code ?? 'unknown'}.`));
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function installPersonaWorkspace({
|
|
180
|
+
workspacePath,
|
|
181
|
+
}) {
|
|
182
|
+
if (!workspacePath) {
|
|
183
|
+
throw new Error('Workspace path is required.');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await runProcess({
|
|
187
|
+
command: resolveNpmCommand(),
|
|
188
|
+
args: ['install', '--silent', '--no-fund', '--no-audit'],
|
|
189
|
+
cwd: workspacePath,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function resolvePersonaDevCommand({
|
|
194
|
+
workspacePath,
|
|
195
|
+
localPort,
|
|
196
|
+
}) {
|
|
197
|
+
const directCommand = resolveDirectViteCommand({ workspacePath, localPort });
|
|
198
|
+
if (directCommand) {
|
|
199
|
+
return directCommand;
|
|
200
|
+
}
|
|
140
201
|
|
|
141
|
-
const
|
|
202
|
+
const port = normalizePositiveInteger(localPort);
|
|
203
|
+
const args = ['run', 'dev'];
|
|
204
|
+
if (port) {
|
|
205
|
+
args.push('--', '--host', resolvePersonaBindHost(), '--port', String(port), '--strictPort');
|
|
206
|
+
}
|
|
142
207
|
return {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
208
|
+
command: resolveNpmCommand(),
|
|
209
|
+
args,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function startPersonaWorkspace({
|
|
214
|
+
workspacePath,
|
|
215
|
+
logFilePath,
|
|
216
|
+
env = {},
|
|
217
|
+
localPort,
|
|
218
|
+
}) {
|
|
219
|
+
if (!workspacePath) {
|
|
220
|
+
throw new Error('Workspace path is required.');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const resolvedLogFilePath =
|
|
224
|
+
logFilePath ||
|
|
225
|
+
path.join(workspacePath, '.oomi', 'runtime.log');
|
|
226
|
+
ensureDir(path.dirname(resolvedLogFilePath));
|
|
227
|
+
|
|
228
|
+
const output = fs.openSync(resolvedLogFilePath, 'a');
|
|
229
|
+
const devCommand = resolvePersonaDevCommand({ workspacePath, localPort });
|
|
230
|
+
const needsWindowsShellWrapper =
|
|
231
|
+
process.platform === 'win32' && /\.cmd$/iu.test(path.basename(devCommand.command));
|
|
232
|
+
|
|
233
|
+
if (needsWindowsShellWrapper) {
|
|
234
|
+
fs.closeSync(output);
|
|
235
|
+
const shellCommand = `${buildWindowsCommandLine(
|
|
236
|
+
devCommand.command,
|
|
237
|
+
devCommand.args
|
|
238
|
+
)} >> "${resolvedLogFilePath.replace(/"/g, '""')}" 2>&1`;
|
|
239
|
+
const child = spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', shellCommand], {
|
|
240
|
+
cwd: workspacePath,
|
|
241
|
+
detached: true,
|
|
242
|
+
stdio: 'ignore',
|
|
243
|
+
shell: false,
|
|
244
|
+
windowsHide: true,
|
|
245
|
+
env: {
|
|
246
|
+
...process.env,
|
|
247
|
+
...env,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
child.unref();
|
|
252
|
+
|
|
253
|
+
const pid = normalizePositiveInteger(child.pid);
|
|
254
|
+
if (!pid) {
|
|
255
|
+
throw new Error('Failed to determine persona workspace process id on Windows.');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
pid,
|
|
260
|
+
logFilePath: resolvedLogFilePath,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let child;
|
|
265
|
+
try {
|
|
266
|
+
child = spawn(devCommand.command, devCommand.args, {
|
|
267
|
+
cwd: workspacePath,
|
|
268
|
+
detached: true,
|
|
269
|
+
stdio: ['ignore', output, output],
|
|
270
|
+
shell: false,
|
|
271
|
+
windowsHide: true,
|
|
272
|
+
env: {
|
|
273
|
+
...process.env,
|
|
274
|
+
...env,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
} finally {
|
|
278
|
+
fs.closeSync(output);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
child.unref();
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
pid: child.pid,
|
|
285
|
+
logFilePath: resolvedLogFilePath,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function isPersonaWorkspaceProcessRunning(pid, options = {}) {
|
|
290
|
+
const safePid = normalizePositiveInteger(pid);
|
|
291
|
+
if (!safePid) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
149
294
|
|
|
150
|
-
|
|
151
|
-
|
|
295
|
+
try {
|
|
296
|
+
process.kill(safePid, 0);
|
|
297
|
+
const hasExpectations =
|
|
298
|
+
trimString(options.workspacePath) ||
|
|
299
|
+
trimString(options.expectedCommand?.command) ||
|
|
300
|
+
normalizePositiveInteger(options.localPort);
|
|
301
|
+
if (!hasExpectations) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const commandLine = readProcessCommandLine(safePid);
|
|
306
|
+
return matchesPersonaRuntimeCommand(commandLine, options);
|
|
307
|
+
|
|
308
|
+
return true;
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
152
312
|
}
|
|
313
|
+
|
|
314
|
+
export async function stopPersonaWorkspace({
|
|
315
|
+
pid,
|
|
316
|
+
waitMs = 4000,
|
|
317
|
+
}) {
|
|
318
|
+
const safePid = normalizePositiveInteger(pid);
|
|
319
|
+
if (!safePid || !isPersonaWorkspaceProcessRunning(safePid)) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
process.kill(safePid, 'SIGTERM');
|
|
325
|
+
} catch {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const startedAt = Date.now();
|
|
330
|
+
while (Date.now() - startedAt <= waitMs) {
|
|
331
|
+
if (!isPersonaWorkspaceProcessRunning(safePid)) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
await wait(200);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
process.kill(safePid, 'SIGKILL');
|
|
339
|
+
} catch {
|
|
340
|
+
return !isPersonaWorkspaceProcessRunning(safePid);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return !isPersonaWorkspaceProcessRunning(safePid);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function fetchHealth(url) {
|
|
347
|
+
const response = await fetch(url, {
|
|
348
|
+
method: 'GET',
|
|
349
|
+
headers: {
|
|
350
|
+
Accept: 'application/json',
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
throw new Error(`Healthcheck returned ${response.status}.`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return response;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export async function waitForPersonaRuntime({
|
|
362
|
+
healthcheckUrl,
|
|
363
|
+
timeoutMs = 45000,
|
|
364
|
+
intervalMs = 1000,
|
|
365
|
+
}) {
|
|
366
|
+
if (!healthcheckUrl) {
|
|
367
|
+
throw new Error('Healthcheck URL is required.');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const startedAt = Date.now();
|
|
371
|
+
let lastError = null;
|
|
372
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
373
|
+
try {
|
|
374
|
+
await fetchHealth(healthcheckUrl);
|
|
375
|
+
return;
|
|
376
|
+
} catch (error) {
|
|
377
|
+
lastError = error;
|
|
378
|
+
await wait(intervalMs);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const message =
|
|
383
|
+
lastError instanceof Error
|
|
384
|
+
? lastError.message
|
|
385
|
+
: 'Timed out waiting for persona runtime healthcheck.';
|
|
386
|
+
throw new Error(`Timed out waiting for persona runtime healthcheck: ${message}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function buildLocalPersonaRuntime({
|
|
390
|
+
localPort,
|
|
391
|
+
healthPath,
|
|
392
|
+
}) {
|
|
393
|
+
const port = Number(localPort);
|
|
394
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
395
|
+
throw new Error('Local port is required.');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const endpoint = `http://127.0.0.1:${port}`;
|
|
399
|
+
return {
|
|
400
|
+
transport: 'local',
|
|
401
|
+
endpoint,
|
|
402
|
+
localPort: port,
|
|
403
|
+
healthcheckUrl: `${endpoint}${healthPath}`,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function defaultPersonaWorkspaceRoot() {
|
|
408
|
+
return resolveOpenclawPersonasDir();
|
|
409
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
function ensureDir(dirPath) {
|
|
5
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function readJsonSafe(filePath) {
|
|
9
|
+
if (!fs.existsSync(filePath)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function trimString(value) {
|
|
21
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolvePersonaWorkspacePath({ workspaceRoot, slug }) {
|
|
25
|
+
const safeRoot = trimString(workspaceRoot);
|
|
26
|
+
const safeSlug = trimString(slug);
|
|
27
|
+
if (!safeRoot) {
|
|
28
|
+
throw new Error('Workspace root is required.');
|
|
29
|
+
}
|
|
30
|
+
if (!safeSlug) {
|
|
31
|
+
throw new Error('Persona slug is required.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return path.resolve(safeRoot, safeSlug);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolvePersonaRuntimeDir(workspacePath) {
|
|
38
|
+
return path.join(workspacePath, '.oomi');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolvePersonaRuntimeStatePath(workspacePath) {
|
|
42
|
+
return path.join(resolvePersonaRuntimeDir(workspacePath), 'runtime.json');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resolvePersonaRuntimeLogPath(workspacePath) {
|
|
46
|
+
return path.join(resolvePersonaRuntimeDir(workspacePath), 'runtime.log');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function readPersonaRuntimeState(workspacePath) {
|
|
50
|
+
return readJsonSafe(resolvePersonaRuntimeStatePath(workspacePath)) || {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function writePersonaRuntimeState(workspacePath, state) {
|
|
54
|
+
const runtimeDir = resolvePersonaRuntimeDir(workspacePath);
|
|
55
|
+
const statePath = resolvePersonaRuntimeStatePath(workspacePath);
|
|
56
|
+
ensureDir(runtimeDir);
|
|
57
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
58
|
+
return state;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function updatePersonaRuntimeState(workspacePath, partial) {
|
|
62
|
+
const current = readPersonaRuntimeState(workspacePath);
|
|
63
|
+
return writePersonaRuntimeState(workspacePath, {
|
|
64
|
+
...current,
|
|
65
|
+
...partial,
|
|
66
|
+
});
|
|
67
|
+
}
|