oomi-ai 0.2.38 → 0.2.39
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 +20 -12
- package/agent_instructions.md +9 -0
- package/bin/oomi-ai.js +1 -1
- package/lib/openclawPaths.js +27 -18
- package/lib/personaRuntimeManager.js +160 -44
- package/lib/personaRuntimeSupervisor.js +20 -7
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,10 +4,10 @@ OpenClaw channel plugin and bridge tooling for Oomi managed chat and voice.
|
|
|
4
4
|
|
|
5
5
|
## Current Focus
|
|
6
6
|
|
|
7
|
-
`0.2.
|
|
7
|
+
`0.2.39` keeps the persona automation lane, adds a stable local persona runtime manager, upgrades the Docker dev harness from a package simulator to a real OpenClaw runtime, and introduces a shared OpenClaw profile contract so local onboarding, Docker bootstrap, and future hosted agents use the same setup model:
|
|
8
8
|
- WebSpatial-based persona scaffolding for generated Oomi apps
|
|
9
9
|
- a high-level `oomi personas create-managed` command for agent-driven persona creation
|
|
10
|
-
- a stable `oomi personas launch-managed` flow for local persona hosting under
|
|
10
|
+
- a stable `oomi personas launch-managed` flow for local persona hosting under `OPENCLAW_WORKSPACE/personas`
|
|
11
11
|
- a matching `oomi personas delete` flow that stops managed runtimes and removes the persona workspace from the OpenClaw machine
|
|
12
12
|
- shared OpenClaw path handling for isolated local or containerized dev roots
|
|
13
13
|
- versioned `oomi openclaw profile init|apply` commands for deterministic local/dev or hosted setup flows
|
|
@@ -288,7 +288,7 @@ The local harness uses the `openrouter-free` preset for direct-provider smoke. I
|
|
|
288
288
|
Use the scaffold flow when OpenClaw needs to build a managed persona app that will live inside Oomi:
|
|
289
289
|
|
|
290
290
|
```bash
|
|
291
|
-
oomi personas scaffold market-analyst --name "Market Analyst" --description "Private app for reviewing my broker positions and risk." --out ~/.openclaw/personas/market-analyst
|
|
291
|
+
oomi personas scaffold market-analyst --name "Market Analyst" --description "Private app for reviewing my broker positions and risk." --out ~/.openclaw/workspace/personas/market-analyst
|
|
292
292
|
```
|
|
293
293
|
|
|
294
294
|
Use:
|
|
@@ -307,7 +307,7 @@ oomi personas delete market-analyst
|
|
|
307
307
|
oomi personas runtime-register market-analyst --local-port 4789
|
|
308
308
|
oomi personas heartbeat market-analyst --local-port 4789
|
|
309
309
|
oomi persona-jobs start pj_123
|
|
310
|
-
oomi persona-jobs succeed pj_123 --workspace-path ~/.openclaw/personas/market-analyst --local-port 4789
|
|
310
|
+
oomi persona-jobs succeed pj_123 --workspace-path ~/.openclaw/workspace/personas/market-analyst --local-port 4789
|
|
311
311
|
oomi persona-jobs fail pj_123 --code JOB_FAILED --message "Scaffold generation failed."
|
|
312
312
|
```
|
|
313
313
|
|
|
@@ -325,14 +325,22 @@ If you want to explicitly host or reuse the persona app on the OpenClaw machine
|
|
|
325
325
|
oomi personas launch-managed cooking-persona --entry-url https://your-relay.example/oomi/cooking-persona
|
|
326
326
|
```
|
|
327
327
|
|
|
328
|
-
This command:
|
|
329
|
-
|
|
330
|
-
- reuses
|
|
331
|
-
- scaffolds only when the workspace is missing
|
|
332
|
-
- installs dependencies only when needed or forced
|
|
333
|
-
- allocates or reuses a free local port
|
|
334
|
-
- starts or reuses the local runtime
|
|
335
|
-
- registers the runtime URL back to Oomi unless `--no-register` is set
|
|
328
|
+
This command:
|
|
329
|
+
|
|
330
|
+
- reuses `OPENCLAW_WORKSPACE/personas/<slug>` as the stable workspace
|
|
331
|
+
- scaffolds only when the workspace is missing
|
|
332
|
+
- installs dependencies only when needed or forced
|
|
333
|
+
- allocates or reuses a free local port
|
|
334
|
+
- starts or reuses the local runtime
|
|
335
|
+
- registers the runtime URL back to Oomi unless `--no-register` is set
|
|
336
|
+
|
|
337
|
+
For existing managed personas that are already open in Oomi, the safe edit flow is:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
oomi personas status <slug> --json
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
The agent should use `editableWorkspacePath` from that output as the authoritative directory for reads, edits, and verification. `compatibilityWorkspacePath` is only a fallback for older installs.
|
|
336
344
|
|
|
337
345
|
## Bridge Health States
|
|
338
346
|
|
package/agent_instructions.md
CHANGED
|
@@ -209,6 +209,15 @@ When generating a managed persona app for Oomi:
|
|
|
209
209
|
4. Preserve the scaffolded WebSpatial/Vite shell, `public/oomi.health.json`, `oomi.runtime.json`, and `public/manifest.webmanifest`.
|
|
210
210
|
5. After customization, start the app and register the runtime with Oomi using the current runtime contract.
|
|
211
211
|
|
|
212
|
+
When editing an existing managed persona that is already open in Oomi:
|
|
213
|
+
|
|
214
|
+
1. Do not ask the user to find the app path manually if Oomi already selected the persona tab for you.
|
|
215
|
+
2. First run `oomi personas status <slug> --json`.
|
|
216
|
+
3. Use `editableWorkspacePath` from that command as the authoritative directory for reads, edits, and verification.
|
|
217
|
+
4. Treat `compatibilityWorkspacePath` only as a fallback or migration clue.
|
|
218
|
+
5. Preserve the scaffolded WebSpatial shell and runtime health files unless the user explicitly asks for a deeper structural change.
|
|
219
|
+
6. Do not claim the persona changed unless you have verified the file contents changed in `editableWorkspacePath` or the runtime reflects the update.
|
|
220
|
+
|
|
212
221
|
When executing a structured persona job from Oomi:
|
|
213
222
|
|
|
214
223
|
1. Prefer `oomi persona-jobs execute --message-file <job.json>` when the backend has already produced a machine-readable job payload.
|
package/bin/oomi-ai.js
CHANGED
|
@@ -329,7 +329,7 @@ Common flags:
|
|
|
329
329
|
--health-path PATH Runtime health path (default: /oomi.health.json)
|
|
330
330
|
--healthcheck-url URL Runtime healthcheck URL override
|
|
331
331
|
--transport TEXT Runtime transport label (default: local, relay when --entry-url is used)
|
|
332
|
-
--workspace-root PATH Persona workspace root (default:
|
|
332
|
+
--workspace-root PATH Persona workspace root (default: OPENCLAW_WORKSPACE/personas)
|
|
333
333
|
--restart Restart an existing managed persona runtime before launch
|
|
334
334
|
--started-at ISO Start timestamp override
|
|
335
335
|
--observed-at ISO Heartbeat timestamp override
|
package/lib/openclawPaths.js
CHANGED
|
@@ -15,11 +15,11 @@ export function resolveOpenclawHome() {
|
|
|
15
15
|
return path.join(os.homedir(), '.openclaw');
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function resolveOpenclawWorkspaceRoot() {
|
|
19
|
-
const explicitWorkspace = trimString(process.env.OPENCLAW_WORKSPACE);
|
|
20
|
-
if (explicitWorkspace) {
|
|
21
|
-
return path.resolve(explicitWorkspace);
|
|
22
|
-
}
|
|
18
|
+
export function resolveOpenclawWorkspaceRoot() {
|
|
19
|
+
const explicitWorkspace = trimString(process.env.OPENCLAW_WORKSPACE);
|
|
20
|
+
if (explicitWorkspace) {
|
|
21
|
+
return path.resolve(explicitWorkspace);
|
|
22
|
+
}
|
|
23
23
|
|
|
24
24
|
const openclawHome = resolveOpenclawHome();
|
|
25
25
|
const managedWorkspace = path.join(openclawHome, 'workspace');
|
|
@@ -27,12 +27,16 @@ export function resolveOpenclawWorkspaceRoot() {
|
|
|
27
27
|
return managedWorkspace;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
return openclawHome;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function
|
|
34
|
-
return
|
|
35
|
-
}
|
|
30
|
+
return openclawHome;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveOpenclawLegacyPersonasDir() {
|
|
34
|
+
return resolveOpenclawPath('personas');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveOpenclawPath(...parts) {
|
|
38
|
+
return path.join(resolveOpenclawHome(), ...parts);
|
|
39
|
+
}
|
|
36
40
|
|
|
37
41
|
export function resolveOpenclawConfigCandidates() {
|
|
38
42
|
return [
|
|
@@ -41,13 +45,18 @@ export function resolveOpenclawConfigCandidates() {
|
|
|
41
45
|
];
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
export function resolveOpenclawSkillsDir() {
|
|
45
|
-
return resolveOpenclawPath('skills');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function resolveOpenclawPersonasDir() {
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
export function resolveOpenclawSkillsDir() {
|
|
49
|
+
return resolveOpenclawPath('skills');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveOpenclawPersonasDir() {
|
|
53
|
+
const explicitPersonas = trimString(process.env.OPENCLAW_PERSONAS_DIR);
|
|
54
|
+
if (explicitPersonas) {
|
|
55
|
+
return path.resolve(explicitPersonas);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return path.join(resolveOpenclawWorkspaceRoot(), 'personas');
|
|
59
|
+
}
|
|
51
60
|
|
|
52
61
|
export function resolveOpenclawIdentityPath() {
|
|
53
62
|
return resolveOpenclawPath('identity', 'device.json');
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
import { findAvailablePort } from './personaPortAllocator.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
|
|
4
|
+
import { findAvailablePort } from './personaPortAllocator.js';
|
|
5
|
+
import { resolveOpenclawLegacyPersonasDir } from './openclawPaths.js';
|
|
6
|
+
import {
|
|
7
|
+
buildLocalPersonaRuntime,
|
|
8
|
+
defaultPersonaWorkspaceRoot,
|
|
8
9
|
installPersonaWorkspace,
|
|
9
10
|
isPersonaWorkspaceProcessRunning,
|
|
10
11
|
resolvePersonaDevCommand,
|
|
@@ -21,9 +22,110 @@ import {
|
|
|
21
22
|
} from './personaRuntimeRegistry.js';
|
|
22
23
|
import { scaffoldPersonaApp } from './scaffold.js';
|
|
23
24
|
|
|
24
|
-
function trimString(value) {
|
|
25
|
-
return typeof value === 'string' ? value.trim() : '';
|
|
26
|
-
}
|
|
25
|
+
function trimString(value) {
|
|
26
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function samePath(a, b) {
|
|
30
|
+
return path.resolve(String(a || '')) === path.resolve(String(b || ''));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pathExists(targetPath) {
|
|
34
|
+
return Boolean(targetPath) && fs.existsSync(targetPath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureDir(dirPath) {
|
|
38
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureDirectoryLink(linkPath, targetPath) {
|
|
42
|
+
if (!linkPath || !targetPath || samePath(linkPath, targetPath) || pathExists(linkPath)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
ensureDir(path.dirname(linkPath));
|
|
47
|
+
try {
|
|
48
|
+
fs.symlinkSync(
|
|
49
|
+
targetPath,
|
|
50
|
+
linkPath,
|
|
51
|
+
process.platform === 'win32' ? 'junction' : 'dir'
|
|
52
|
+
);
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveManagedPersonaWorkspacePaths({
|
|
60
|
+
slug,
|
|
61
|
+
workspaceRoot = defaultPersonaWorkspaceRoot(),
|
|
62
|
+
}) {
|
|
63
|
+
const canonicalWorkspaceRoot = path.resolve(workspaceRoot);
|
|
64
|
+
const canonicalWorkspacePath = resolvePersonaWorkspacePath({
|
|
65
|
+
workspaceRoot: canonicalWorkspaceRoot,
|
|
66
|
+
slug,
|
|
67
|
+
});
|
|
68
|
+
const legacyWorkspaceRoot = path.resolve(resolveOpenclawLegacyPersonasDir());
|
|
69
|
+
const legacyWorkspacePath = samePath(canonicalWorkspaceRoot, legacyWorkspaceRoot)
|
|
70
|
+
? ''
|
|
71
|
+
: resolvePersonaWorkspacePath({
|
|
72
|
+
workspaceRoot: legacyWorkspaceRoot,
|
|
73
|
+
slug,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
canonicalWorkspaceRoot,
|
|
78
|
+
canonicalWorkspacePath,
|
|
79
|
+
legacyWorkspaceRoot: legacyWorkspacePath ? legacyWorkspaceRoot : '',
|
|
80
|
+
legacyWorkspacePath,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveManagedPersonaWorkspace({
|
|
85
|
+
slug,
|
|
86
|
+
workspaceRoot = defaultPersonaWorkspaceRoot(),
|
|
87
|
+
}) {
|
|
88
|
+
const paths = resolveManagedPersonaWorkspacePaths({ slug, workspaceRoot });
|
|
89
|
+
ensureDir(paths.canonicalWorkspaceRoot);
|
|
90
|
+
|
|
91
|
+
let migratedFromLegacy = false;
|
|
92
|
+
let canonicalProxyCreated = false;
|
|
93
|
+
let legacyProxyCreated = false;
|
|
94
|
+
|
|
95
|
+
if (!pathExists(paths.canonicalWorkspacePath) && pathExists(paths.legacyWorkspacePath)) {
|
|
96
|
+
try {
|
|
97
|
+
fs.renameSync(paths.legacyWorkspacePath, paths.canonicalWorkspacePath);
|
|
98
|
+
migratedFromLegacy = true;
|
|
99
|
+
} catch {
|
|
100
|
+
canonicalProxyCreated = ensureDirectoryLink(
|
|
101
|
+
paths.canonicalWorkspacePath,
|
|
102
|
+
paths.legacyWorkspacePath
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (pathExists(paths.canonicalWorkspacePath) && paths.legacyWorkspacePath && !pathExists(paths.legacyWorkspacePath)) {
|
|
108
|
+
legacyProxyCreated = ensureDirectoryLink(
|
|
109
|
+
paths.legacyWorkspacePath,
|
|
110
|
+
paths.canonicalWorkspacePath
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const workspacePath = pathExists(paths.canonicalWorkspacePath)
|
|
115
|
+
? paths.canonicalWorkspacePath
|
|
116
|
+
: (pathExists(paths.legacyWorkspacePath) ? paths.legacyWorkspacePath : paths.canonicalWorkspacePath);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
...paths,
|
|
120
|
+
workspacePath,
|
|
121
|
+
editableWorkspacePath: pathExists(paths.canonicalWorkspacePath)
|
|
122
|
+
? paths.canonicalWorkspacePath
|
|
123
|
+
: workspacePath,
|
|
124
|
+
migratedFromLegacy,
|
|
125
|
+
canonicalProxyCreated,
|
|
126
|
+
legacyProxyCreated,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
27
129
|
|
|
28
130
|
export function slugifyPersonaName(name) {
|
|
29
131
|
const normalized = trimString(name)
|
|
@@ -143,14 +245,15 @@ export async function launchManagedPersonaRuntime({
|
|
|
143
245
|
const safeDescription = trimString(description) || safeName;
|
|
144
246
|
if (!safeName) {
|
|
145
247
|
throw new Error('Persona name is required.');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const safeSlug = trimString(slug) || slugifyPersonaName(safeName);
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
});
|
|
153
|
-
const
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const safeSlug = trimString(slug) || slugifyPersonaName(safeName);
|
|
251
|
+
const workspaceResolution = resolveManagedPersonaWorkspace({
|
|
252
|
+
slug: safeSlug,
|
|
253
|
+
workspaceRoot,
|
|
254
|
+
});
|
|
255
|
+
const workspacePath = workspaceResolution.workspacePath;
|
|
256
|
+
const previousState = readPersonaRuntimeState(workspacePath);
|
|
154
257
|
|
|
155
258
|
const scaffoldInfo = await ensureWorkspaceScaffold({
|
|
156
259
|
slug: safeSlug,
|
|
@@ -238,13 +341,16 @@ export async function launchManagedPersonaRuntime({
|
|
|
238
341
|
devCommand: resolvePersonaDevCommand({ workspacePath, localPort }),
|
|
239
342
|
});
|
|
240
343
|
|
|
241
|
-
return {
|
|
242
|
-
ok: true,
|
|
243
|
-
slug: safeSlug,
|
|
244
|
-
workspacePath,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
344
|
+
return {
|
|
345
|
+
ok: true,
|
|
346
|
+
slug: safeSlug,
|
|
347
|
+
workspacePath,
|
|
348
|
+
editableWorkspacePath: workspaceResolution.editableWorkspacePath,
|
|
349
|
+
compatibilityWorkspacePath: workspaceResolution.legacyWorkspacePath || '',
|
|
350
|
+
migratedFromLegacy: workspaceResolution.migratedFromLegacy,
|
|
351
|
+
scaffolded: scaffoldInfo.scaffolded,
|
|
352
|
+
installed,
|
|
353
|
+
reusedRunningProcess: reusingRunningProcess,
|
|
248
354
|
runtime: registration,
|
|
249
355
|
localRuntime,
|
|
250
356
|
state: runtimeState,
|
|
@@ -256,26 +362,33 @@ export function getManagedPersonaRuntimeStatus({
|
|
|
256
362
|
workspaceRoot = defaultPersonaWorkspaceRoot(),
|
|
257
363
|
}) {
|
|
258
364
|
const safeSlug = trimString(slug);
|
|
259
|
-
if (!safeSlug) {
|
|
260
|
-
throw new Error('Persona slug is required.');
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
});
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
365
|
+
if (!safeSlug) {
|
|
366
|
+
throw new Error('Persona slug is required.');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const workspaceResolution = resolveManagedPersonaWorkspace({
|
|
370
|
+
slug: safeSlug,
|
|
371
|
+
workspaceRoot,
|
|
372
|
+
});
|
|
373
|
+
const workspacePath = workspaceResolution.workspacePath;
|
|
374
|
+
const state = readPersonaRuntimeState(workspacePath);
|
|
375
|
+
const pid = Number.isInteger(state.pid) ? state.pid : null;
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
slug: safeSlug,
|
|
379
|
+
workspaceRoot: workspaceResolution.canonicalWorkspaceRoot,
|
|
380
|
+
workspacePath,
|
|
381
|
+
editableWorkspacePath: workspaceResolution.editableWorkspacePath,
|
|
382
|
+
compatibilityWorkspacePath: workspaceResolution.legacyWorkspacePath || '',
|
|
383
|
+
workspaceExists: fs.existsSync(workspacePath),
|
|
384
|
+
runtimeStatePath: resolvePersonaRuntimeStatePath(workspacePath),
|
|
385
|
+
processRunning: pid ? isPersonaWorkspaceProcessRunning(pid) : false,
|
|
386
|
+
migratedFromLegacy: workspaceResolution.migratedFromLegacy,
|
|
387
|
+
canonicalProxyCreated: workspaceResolution.canonicalProxyCreated,
|
|
388
|
+
legacyProxyCreated: workspaceResolution.legacyProxyCreated,
|
|
389
|
+
state,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
279
392
|
|
|
280
393
|
export async function stopManagedPersonaRuntime({
|
|
281
394
|
slug,
|
|
@@ -352,8 +465,11 @@ export async function destroyManagedPersonaRuntime({
|
|
|
352
465
|
slug,
|
|
353
466
|
workspaceRoot,
|
|
354
467
|
});
|
|
355
|
-
ensureWorkspacePathWithinRoot(status.workspacePath, workspaceRoot);
|
|
468
|
+
ensureWorkspacePathWithinRoot(status.workspacePath, status.workspaceRoot || workspaceRoot);
|
|
356
469
|
fs.rmSync(status.workspacePath, { recursive: true, force: true });
|
|
470
|
+
if (status.compatibilityWorkspacePath && pathExists(status.compatibilityWorkspacePath)) {
|
|
471
|
+
fs.rmSync(status.compatibilityWorkspacePath, { recursive: true, force: true });
|
|
472
|
+
}
|
|
357
473
|
|
|
358
474
|
return {
|
|
359
475
|
ok: true,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { resolveOpenclawLegacyPersonasDir } from './openclawPaths.js';
|
|
4
5
|
import { createPersonaApiClient } from './personaApiClient.js';
|
|
5
6
|
import { launchManagedPersonaRuntime } from './personaRuntimeManager.js';
|
|
6
7
|
import { readPersonaRuntimeState } from './personaRuntimeRegistry.js';
|
|
@@ -17,15 +18,27 @@ function wait(ms) {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function listWorkspacePaths(workspaceRoot) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const roots = [trimString(workspaceRoot), trimString(resolveOpenclawLegacyPersonasDir())]
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.filter((root, index, values) => values.findIndex((candidate) => path.resolve(candidate) === path.resolve(root)) === index)
|
|
24
|
+
.filter((root) => fs.existsSync(root));
|
|
25
|
+
|
|
26
|
+
const workspacePaths = new Set();
|
|
27
|
+
for (const root of roots) {
|
|
28
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
29
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
30
|
+
const candidatePath = path.join(root, entry.name);
|
|
31
|
+
let dedupeKey = candidatePath;
|
|
32
|
+
try {
|
|
33
|
+
dedupeKey = fs.realpathSync(candidatePath);
|
|
34
|
+
} catch {
|
|
35
|
+
// fall back to the visible path when the real path is unavailable
|
|
36
|
+
}
|
|
37
|
+
workspacePaths.add(dedupeKey);
|
|
38
|
+
}
|
|
23
39
|
}
|
|
24
40
|
|
|
25
|
-
return
|
|
26
|
-
.readdirSync(safeRoot, { withFileTypes: true })
|
|
27
|
-
.filter((entry) => entry.isDirectory())
|
|
28
|
-
.map((entry) => path.join(safeRoot, entry.name));
|
|
41
|
+
return Array.from(workspacePaths);
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
async function healthcheckOk(url) {
|
package/openclaw.plugin.json
CHANGED