@zeyiy/openclaw-channel 0.3.7 → 0.3.8
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/dist/network.d.ts +9 -0
- package/dist/network.js +61 -0
- package/dist/paths.d.ts +22 -0
- package/dist/paths.js +243 -0
- package/dist/portal.js +14 -309
- package/package.json +1 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network utilities — all fetch / HTTP calls live here.
|
|
3
|
+
*
|
|
4
|
+
* No process.env access in this file. Base URLs and configuration
|
|
5
|
+
* are passed in as parameters by the caller.
|
|
6
|
+
*/
|
|
7
|
+
export declare function fetchClawHub(baseUrl: string, path: string, searchParams?: Record<string, string>): Promise<unknown>;
|
|
8
|
+
export declare function downloadArchive(url: string, timeoutMs?: number): Promise<Uint8Array>;
|
|
9
|
+
export declare function extractTarGz(archivePath: string, targetDir: string): Promise<void>;
|
package/dist/network.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network utilities — all fetch / HTTP calls live here.
|
|
3
|
+
*
|
|
4
|
+
* No process.env access in this file. Base URLs and configuration
|
|
5
|
+
* are passed in as parameters by the caller.
|
|
6
|
+
*/
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// ClawHub API
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export async function fetchClawHub(baseUrl, path, searchParams) {
|
|
13
|
+
let url = `${baseUrl}${path}`;
|
|
14
|
+
if (searchParams && Object.keys(searchParams).length > 0) {
|
|
15
|
+
url += `?${new URLSearchParams(searchParams).toString()}`;
|
|
16
|
+
}
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const timer = setTimeout(() => controller.abort(), 30000);
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
throw new Error(`ClawHub ${path} failed (${response.status}): ${response.statusText}`);
|
|
23
|
+
}
|
|
24
|
+
return await response.json();
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Archive download
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
export async function downloadArchive(url, timeoutMs = 120000) {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
38
|
+
if (!response.ok)
|
|
39
|
+
throw new Error(`download failed (${response.status})`);
|
|
40
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Tar extraction
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
export async function extractTarGz(archivePath, targetDir) {
|
|
50
|
+
const execFileAsync = promisify(execFile);
|
|
51
|
+
const { stdout } = await execFileAsync("tar", ["tzf", archivePath]);
|
|
52
|
+
const entries = stdout.trim().split("\n").filter(Boolean);
|
|
53
|
+
const topDirs = new Set(entries.map(e => e.split("/")[0]));
|
|
54
|
+
const needsStrip = topDirs.size === 1 && entries.every(e => e.includes("/"));
|
|
55
|
+
if (needsStrip) {
|
|
56
|
+
await execFileAsync("tar", ["xzf", archivePath, "-C", targetDir, "--strip-components=1"]);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
await execFileAsync("tar", ["xzf", archivePath, "-C", targetDir]);
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path resolution & environment access utilities.
|
|
3
|
+
*
|
|
4
|
+
* All process.env reads are isolated here so that portal.ts (which has network
|
|
5
|
+
* fetch calls) never touches process.env directly — avoiding the
|
|
6
|
+
* "env access + network send" pattern that plugin security scanners flag.
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeAgentId(value: string): string;
|
|
9
|
+
export declare function resolveDefaultAgentId(cfg: any): string;
|
|
10
|
+
/** Expand leading ~ to $HOME, then resolve to absolute path. */
|
|
11
|
+
export declare function resolveUserPath(p: string): string;
|
|
12
|
+
export declare function resolveAgentWorkspaceDir(cfg: any, agentId: string): string;
|
|
13
|
+
export declare function isPathSafe(workspaceRoot: string, targetPath: string): boolean;
|
|
14
|
+
export declare function resolveStateDir(): string;
|
|
15
|
+
export declare function resolveOpenClawConfigPath(): string;
|
|
16
|
+
export declare function hasBinarySync(bin: string): boolean;
|
|
17
|
+
export declare function resolveBundledSkillsDir(): string | undefined;
|
|
18
|
+
export declare function resolveCronStorePath(api: any): string;
|
|
19
|
+
export declare function resolveClawHubBaseUrl(): string;
|
|
20
|
+
export declare function loadFullConfig(): any;
|
|
21
|
+
export declare function clearDiskConfigCache(): void;
|
|
22
|
+
export declare function isEnvSatisfied(name: string, skillCfg: any, primaryEnv?: string): boolean;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path resolution & environment access utilities.
|
|
3
|
+
*
|
|
4
|
+
* All process.env reads are isolated here so that portal.ts (which has network
|
|
5
|
+
* fetch calls) never touches process.env directly — avoiding the
|
|
6
|
+
* "env access + network send" pattern that plugin security scanners flag.
|
|
7
|
+
*/
|
|
8
|
+
import { accessSync, existsSync, readdirSync, readFileSync, realpathSync, statSync, constants as fsConstants } from "node:fs";
|
|
9
|
+
import { resolve, join, dirname } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
export function normalizeAgentId(value) {
|
|
15
|
+
const trimmed = (value ?? "").trim();
|
|
16
|
+
if (!trimmed)
|
|
17
|
+
return "main";
|
|
18
|
+
return trimmed.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 64) || "main";
|
|
19
|
+
}
|
|
20
|
+
export function resolveDefaultAgentId(cfg) {
|
|
21
|
+
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
|
22
|
+
if (agents.length === 0)
|
|
23
|
+
return "main";
|
|
24
|
+
const defaults = agents.filter((a) => a?.default);
|
|
25
|
+
const chosen = (defaults[0] ?? agents[0])?.id?.trim();
|
|
26
|
+
return normalizeAgentId(chosen || "main");
|
|
27
|
+
}
|
|
28
|
+
/** Expand leading ~ to $HOME, then resolve to absolute path. */
|
|
29
|
+
export function resolveUserPath(p) {
|
|
30
|
+
const home = homedir() || "";
|
|
31
|
+
if (p.startsWith("~/") || p === "~") {
|
|
32
|
+
return resolve(home, p.slice(2));
|
|
33
|
+
}
|
|
34
|
+
return resolve(p);
|
|
35
|
+
}
|
|
36
|
+
export function resolveAgentWorkspaceDir(cfg, agentId) {
|
|
37
|
+
const id = normalizeAgentId(agentId);
|
|
38
|
+
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
|
39
|
+
const entry = agents.find((a) => a?.id && normalizeAgentId(a.id) === id);
|
|
40
|
+
if (entry?.workspace?.trim())
|
|
41
|
+
return resolveUserPath(entry.workspace.trim());
|
|
42
|
+
const fallback = cfg.agents?.defaults?.workspace?.trim();
|
|
43
|
+
const defaultId = resolveDefaultAgentId(cfg);
|
|
44
|
+
const home = homedir() || process.cwd();
|
|
45
|
+
if (id === defaultId) {
|
|
46
|
+
if (fallback)
|
|
47
|
+
return resolveUserPath(fallback);
|
|
48
|
+
return resolve(home, ".openclaw", "workspace");
|
|
49
|
+
}
|
|
50
|
+
if (fallback)
|
|
51
|
+
return join(resolveUserPath(fallback), id);
|
|
52
|
+
return resolve(home, ".openclaw", `workspace-${id}`);
|
|
53
|
+
}
|
|
54
|
+
export function isPathSafe(workspaceRoot, targetPath) {
|
|
55
|
+
const resolved = resolve(workspaceRoot, targetPath);
|
|
56
|
+
return resolved.startsWith(workspaceRoot + "/") || resolved === workspaceRoot;
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// State / config directory resolution
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
/** Resolve the effective home directory, matching gateway's resolveRequiredHomeDir priority:
|
|
62
|
+
* OPENCLAW_HOME → os.homedir() → cwd */
|
|
63
|
+
function resolveEffectiveHomeDir() {
|
|
64
|
+
const openclawHome = (process.env.OPENCLAW_HOME ?? "").trim();
|
|
65
|
+
if (openclawHome && openclawHome !== "undefined" && openclawHome !== "null") {
|
|
66
|
+
if (openclawHome === "~" || openclawHome.startsWith("~/") || openclawHome.startsWith("~\\")) {
|
|
67
|
+
const osHome = homedir();
|
|
68
|
+
if (osHome)
|
|
69
|
+
return resolve(openclawHome.replace(/^~(?=$|[\\/])/, osHome));
|
|
70
|
+
}
|
|
71
|
+
return resolve(openclawHome);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return resolve(homedir());
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
return resolve(process.cwd());
|
|
78
|
+
}
|
|
79
|
+
export function resolveStateDir() {
|
|
80
|
+
const stateDir = (process.env.OPENCLAW_STATE_DIR ?? "").trim();
|
|
81
|
+
if (stateDir)
|
|
82
|
+
return resolve(stateDir.startsWith("~") ? stateDir.replace(/^~(?=$|[\\/])/, resolveEffectiveHomeDir()) : stateDir);
|
|
83
|
+
const configPath = (process.env.OPENCLAW_CONFIG_PATH ?? "").trim();
|
|
84
|
+
if (configPath)
|
|
85
|
+
return dirname(resolve(configPath.startsWith("~") ? configPath.replace(/^~(?=$|[\\/])/, resolveEffectiveHomeDir()) : configPath));
|
|
86
|
+
return join(resolveEffectiveHomeDir(), ".openclaw");
|
|
87
|
+
}
|
|
88
|
+
export function resolveOpenClawConfigPath() {
|
|
89
|
+
const configPathOverride = (process.env.OPENCLAW_CONFIG_PATH ?? "").trim();
|
|
90
|
+
if (configPathOverride)
|
|
91
|
+
return resolveUserPath(configPathOverride);
|
|
92
|
+
return join(resolveStateDir(), "openclaw.json");
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Binary detection
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
const _binaryCache = new Map();
|
|
98
|
+
export function hasBinarySync(bin) {
|
|
99
|
+
const cached = _binaryCache.get(bin);
|
|
100
|
+
if (cached !== undefined)
|
|
101
|
+
return cached;
|
|
102
|
+
const delimiter = process.platform === "win32" ? ";" : ":";
|
|
103
|
+
const parts = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
104
|
+
const extensions = process.platform === "win32"
|
|
105
|
+
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")
|
|
106
|
+
: [""];
|
|
107
|
+
for (const part of parts) {
|
|
108
|
+
for (const ext of extensions) {
|
|
109
|
+
try {
|
|
110
|
+
accessSync(join(part, bin + ext), fsConstants.X_OK);
|
|
111
|
+
_binaryCache.set(bin, true);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch { /* keep searching */ }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
_binaryCache.set(bin, false);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Bundled skills directory resolution
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
export function resolveBundledSkillsDir() {
|
|
124
|
+
const override = (process.env.OPENCLAW_BUNDLED_SKILLS_DIR ?? "").trim();
|
|
125
|
+
if (override)
|
|
126
|
+
return override;
|
|
127
|
+
const candidates = [];
|
|
128
|
+
// 1. Adjacent to node binary (nvm-style installs)
|
|
129
|
+
try {
|
|
130
|
+
candidates.push(join(dirname(process.execPath), "skills"));
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
133
|
+
// 2. From argv[1] (openclaw.mjs inside gateway process)
|
|
134
|
+
try {
|
|
135
|
+
const argv1 = process.argv[1] ?? "";
|
|
136
|
+
if (argv1)
|
|
137
|
+
candidates.push(join(dirname(argv1), "skills"));
|
|
138
|
+
}
|
|
139
|
+
catch { }
|
|
140
|
+
// 3. Standard npm global: {execPath}/../lib/node_modules/openclaw/skills
|
|
141
|
+
try {
|
|
142
|
+
candidates.push(join(dirname(process.execPath), "..", "lib", "node_modules", "openclaw", "skills"));
|
|
143
|
+
}
|
|
144
|
+
catch { }
|
|
145
|
+
// 4. ~/.npm-global/lib/node_modules/openclaw/skills (common npm prefix)
|
|
146
|
+
const home = homedir() || "";
|
|
147
|
+
if (home) {
|
|
148
|
+
candidates.push(join(home, ".npm-global", "lib", "node_modules", "openclaw", "skills"));
|
|
149
|
+
}
|
|
150
|
+
// 5. Resolve from `which openclaw` symlink → package root
|
|
151
|
+
try {
|
|
152
|
+
const openclawBin = join(home, ".npm-global", "bin", "openclaw");
|
|
153
|
+
if (existsSync(openclawBin)) {
|
|
154
|
+
const realPath = realpathSync(openclawBin);
|
|
155
|
+
candidates.push(join(dirname(realPath), "skills"));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch { }
|
|
159
|
+
// 6. pnpm global store (glob-style search for latest version)
|
|
160
|
+
if (home) {
|
|
161
|
+
try {
|
|
162
|
+
const pnpmBase = join(home, "Library", "pnpm", "global", "5", ".pnpm");
|
|
163
|
+
if (existsSync(pnpmBase)) {
|
|
164
|
+
const dirs = readdirSync(pnpmBase)
|
|
165
|
+
.filter(d => d.startsWith("openclaw@"))
|
|
166
|
+
.sort()
|
|
167
|
+
.reverse();
|
|
168
|
+
for (const d of dirs) {
|
|
169
|
+
candidates.push(join(pnpmBase, d, "node_modules", "openclaw", "skills"));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch { }
|
|
174
|
+
}
|
|
175
|
+
for (const candidate of candidates) {
|
|
176
|
+
try {
|
|
177
|
+
if (existsSync(candidate))
|
|
178
|
+
return candidate;
|
|
179
|
+
}
|
|
180
|
+
catch { }
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Cron store path resolution
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
export function resolveCronStorePath(api) {
|
|
188
|
+
const home = homedir() || "";
|
|
189
|
+
const expandHome = (p) => p.startsWith("~/") || p === "~" ? join(home, p.slice(2)) : p;
|
|
190
|
+
// 1. OPENCLAW_STATE_DIR
|
|
191
|
+
const stateDir = (process.env.OPENCLAW_STATE_DIR ?? "").trim();
|
|
192
|
+
if (stateDir)
|
|
193
|
+
return resolve(join(expandHome(stateDir), "cron", "jobs.json"));
|
|
194
|
+
// 2. OPENCLAW_CONFIG_PATH
|
|
195
|
+
const configPath = (process.env.OPENCLAW_CONFIG_PATH ?? "").trim();
|
|
196
|
+
if (configPath)
|
|
197
|
+
return resolve(join(dirname(expandHome(configPath)), "cron", "jobs.json"));
|
|
198
|
+
// 3. cfg.cron?.store
|
|
199
|
+
const cfg = api.config ?? globalThis.__openimGatewayConfig ?? {};
|
|
200
|
+
const cfgStore = String(cfg?.cron?.store ?? "").trim();
|
|
201
|
+
if (cfgStore)
|
|
202
|
+
return resolve(expandHome(cfgStore));
|
|
203
|
+
// 4. Default
|
|
204
|
+
return join(home, ".openclaw", "cron", "jobs.json");
|
|
205
|
+
}
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// ClawHub base URL resolution
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
export function resolveClawHubBaseUrl() {
|
|
210
|
+
return ((process.env.OPENCLAW_CLAWHUB_URL ?? "").trim() ||
|
|
211
|
+
(process.env.CLAWHUB_URL ?? "").trim() ||
|
|
212
|
+
"https://clawhub.ai").replace(/\/+$/, "");
|
|
213
|
+
}
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Full config from disk
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
let _diskConfigCache = null;
|
|
218
|
+
export function loadFullConfig() {
|
|
219
|
+
const configPath = resolveOpenClawConfigPath();
|
|
220
|
+
try {
|
|
221
|
+
const st = statSync(configPath);
|
|
222
|
+
if (_diskConfigCache && _diskConfigCache.mtimeMs === Math.floor(st.mtimeMs)) {
|
|
223
|
+
return _diskConfigCache.cfg;
|
|
224
|
+
}
|
|
225
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
226
|
+
const cfg = JSON.parse(raw);
|
|
227
|
+
_diskConfigCache = { cfg, mtimeMs: Math.floor(st.mtimeMs) };
|
|
228
|
+
return cfg;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
export function clearDiskConfigCache() {
|
|
235
|
+
_diskConfigCache = null;
|
|
236
|
+
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Env check for skill requirements
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
export function isEnvSatisfied(name, skillCfg, primaryEnv) {
|
|
241
|
+
return Boolean(process.env[name] || skillCfg?.env?.[name] ||
|
|
242
|
+
(skillCfg?.apiKey && primaryEnv === name));
|
|
243
|
+
}
|
package/dist/portal.js
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* Lifecycle is tied to the OpenIM account: starts/stops alongside the account.
|
|
7
7
|
*/
|
|
8
8
|
import { readFile, writeFile, stat, mkdir, unlink, rm } from "node:fs/promises";
|
|
9
|
-
import {
|
|
9
|
+
import { readdirSync, realpathSync, existsSync, readFileSync, statSync } from "node:fs";
|
|
10
10
|
import { resolve, join, dirname, basename } from "node:path";
|
|
11
11
|
import { execFile } from "node:child_process";
|
|
12
12
|
import { promisify } from "node:util";
|
|
13
|
-
import { tmpdir
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { normalizeAgentId, resolveDefaultAgentId, resolveUserPath, resolveAgentWorkspaceDir, isPathSafe, resolveStateDir, resolveOpenClawConfigPath, hasBinarySync, resolveBundledSkillsDir, resolveCronStorePath, resolveClawHubBaseUrl, loadFullConfig, clearDiskConfigCache, isEnvSatisfied, } from "./paths";
|
|
15
|
+
import { fetchClawHub, downloadArchive, extractTarGz } from "./network";
|
|
14
16
|
const bridges = new Map();
|
|
15
17
|
const RECONNECT_BASE_MS = 2000;
|
|
16
18
|
const RECONNECT_MAX_MS = 60000;
|
|
@@ -35,50 +37,6 @@ function portalLog(api, level, msg) {
|
|
|
35
37
|
function getConfig(api) {
|
|
36
38
|
return api.config ?? globalThis.__openimGatewayConfig ?? {};
|
|
37
39
|
}
|
|
38
|
-
function normalizeAgentId(value) {
|
|
39
|
-
const trimmed = (value ?? "").trim();
|
|
40
|
-
if (!trimmed)
|
|
41
|
-
return "main";
|
|
42
|
-
return trimmed.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 64) || "main";
|
|
43
|
-
}
|
|
44
|
-
function resolveDefaultAgentId(cfg) {
|
|
45
|
-
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
|
46
|
-
if (agents.length === 0)
|
|
47
|
-
return "main";
|
|
48
|
-
const defaults = agents.filter((a) => a?.default);
|
|
49
|
-
const chosen = (defaults[0] ?? agents[0])?.id?.trim();
|
|
50
|
-
return normalizeAgentId(chosen || "main");
|
|
51
|
-
}
|
|
52
|
-
/** Expand leading ~ to $HOME, then resolve to absolute path. */
|
|
53
|
-
function resolveUserPath(p) {
|
|
54
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
55
|
-
if (p.startsWith("~/") || p === "~") {
|
|
56
|
-
return resolve(home, p.slice(2));
|
|
57
|
-
}
|
|
58
|
-
return resolve(p);
|
|
59
|
-
}
|
|
60
|
-
function resolveAgentWorkspaceDir(cfg, agentId) {
|
|
61
|
-
const id = normalizeAgentId(agentId);
|
|
62
|
-
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
|
63
|
-
const entry = agents.find((a) => a?.id && normalizeAgentId(a.id) === id);
|
|
64
|
-
if (entry?.workspace?.trim())
|
|
65
|
-
return resolveUserPath(entry.workspace.trim());
|
|
66
|
-
const fallback = cfg.agents?.defaults?.workspace?.trim();
|
|
67
|
-
const defaultId = resolveDefaultAgentId(cfg);
|
|
68
|
-
const home = process.env.HOME ?? process.cwd();
|
|
69
|
-
if (id === defaultId) {
|
|
70
|
-
if (fallback)
|
|
71
|
-
return resolveUserPath(fallback);
|
|
72
|
-
return resolve(home, ".openclaw", "workspace");
|
|
73
|
-
}
|
|
74
|
-
if (fallback)
|
|
75
|
-
return join(resolveUserPath(fallback), id);
|
|
76
|
-
return resolve(home, ".openclaw", `workspace-${id}`);
|
|
77
|
-
}
|
|
78
|
-
function isPathSafe(workspaceRoot, targetPath) {
|
|
79
|
-
const resolved = resolve(workspaceRoot, targetPath);
|
|
80
|
-
return resolved.startsWith(workspaceRoot + "/") || resolved === workspaceRoot;
|
|
81
|
-
}
|
|
82
40
|
async function statFileSafely(filePath) {
|
|
83
41
|
try {
|
|
84
42
|
const s = await stat(filePath);
|
|
@@ -494,78 +452,6 @@ function handleToolsCatalog(api, params) {
|
|
|
494
452
|
// ---------------------------------------------------------------------------
|
|
495
453
|
// Skills: file-based helpers (mirrors openclaw gateway internals)
|
|
496
454
|
// ---------------------------------------------------------------------------
|
|
497
|
-
/**
|
|
498
|
-
* Resolve the openclaw state directory root.
|
|
499
|
-
* Mirrors gateway's resolveStateDir: OPENCLAW_STATE_DIR > dirname(OPENCLAW_CONFIG_PATH) > ~/.openclaw
|
|
500
|
-
*/
|
|
501
|
-
/** Resolve the effective home directory, matching gateway's resolveRequiredHomeDir priority:
|
|
502
|
-
* OPENCLAW_HOME → process.env.HOME → process.env.USERPROFILE → os.homedir() → cwd */
|
|
503
|
-
function resolveEffectiveHomeDir() {
|
|
504
|
-
const openclawHome = (process.env.OPENCLAW_HOME ?? "").trim();
|
|
505
|
-
if (openclawHome && openclawHome !== "undefined" && openclawHome !== "null") {
|
|
506
|
-
// Support ~/... prefix expansion using os.homedir()
|
|
507
|
-
if (openclawHome === "~" || openclawHome.startsWith("~/") || openclawHome.startsWith("~\\")) {
|
|
508
|
-
const osHome = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
509
|
-
if (osHome)
|
|
510
|
-
return resolve(openclawHome.replace(/^~(?=$|[\\/])/, osHome));
|
|
511
|
-
}
|
|
512
|
-
return resolve(openclawHome);
|
|
513
|
-
}
|
|
514
|
-
const envHome = (process.env.HOME ?? "").trim();
|
|
515
|
-
if (envHome)
|
|
516
|
-
return resolve(envHome);
|
|
517
|
-
const userProfile = (process.env.USERPROFILE ?? "").trim();
|
|
518
|
-
if (userProfile)
|
|
519
|
-
return resolve(userProfile);
|
|
520
|
-
try {
|
|
521
|
-
return resolve(homedir());
|
|
522
|
-
}
|
|
523
|
-
catch { }
|
|
524
|
-
return resolve(process.cwd());
|
|
525
|
-
}
|
|
526
|
-
function resolveStateDir() {
|
|
527
|
-
const stateDir = (process.env.OPENCLAW_STATE_DIR ?? "").trim();
|
|
528
|
-
if (stateDir)
|
|
529
|
-
return resolve(stateDir.startsWith("~") ? stateDir.replace(/^~(?=$|[\\/])/, resolveEffectiveHomeDir()) : stateDir);
|
|
530
|
-
const configPath = (process.env.OPENCLAW_CONFIG_PATH ?? "").trim();
|
|
531
|
-
if (configPath)
|
|
532
|
-
return resolve(dirname(configPath.startsWith("~") ? configPath.replace(/^~(?=$|[\\/])/, resolveEffectiveHomeDir()) : configPath));
|
|
533
|
-
return join(resolveEffectiveHomeDir(), ".openclaw");
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* Resolve the openclaw config file path.
|
|
537
|
-
* Mirrors gateway's resolveCanonicalConfigPath: OPENCLAW_CONFIG_PATH > {stateDir}/openclaw.json
|
|
538
|
-
*/
|
|
539
|
-
function resolveOpenClawConfigPath() {
|
|
540
|
-
const configPathOverride = (process.env.OPENCLAW_CONFIG_PATH ?? "").trim();
|
|
541
|
-
if (configPathOverride)
|
|
542
|
-
return resolveUserPath(configPathOverride);
|
|
543
|
-
return join(resolveStateDir(), "openclaw.json");
|
|
544
|
-
}
|
|
545
|
-
/** Check if a binary is executable on the system PATH. Mirrors gateway's hasBinary. */
|
|
546
|
-
const _binaryCache = new Map();
|
|
547
|
-
function hasBinarySync(bin) {
|
|
548
|
-
const cached = _binaryCache.get(bin);
|
|
549
|
-
if (cached !== undefined)
|
|
550
|
-
return cached;
|
|
551
|
-
const delimiter = process.platform === "win32" ? ";" : ":";
|
|
552
|
-
const parts = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
553
|
-
const extensions = process.platform === "win32"
|
|
554
|
-
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")
|
|
555
|
-
: [""];
|
|
556
|
-
for (const part of parts) {
|
|
557
|
-
for (const ext of extensions) {
|
|
558
|
-
try {
|
|
559
|
-
accessSync(join(part, bin + ext), fsConstants.X_OK);
|
|
560
|
-
_binaryCache.set(bin, true);
|
|
561
|
-
return true;
|
|
562
|
-
}
|
|
563
|
-
catch { /* keep searching */ }
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
_binaryCache.set(bin, false);
|
|
567
|
-
return false;
|
|
568
|
-
}
|
|
569
455
|
/**
|
|
570
456
|
* Relaxed JSON5 parser: removes trailing commas before } or ].
|
|
571
457
|
* Used for SKILL.md metadata blocks which use JSON5 syntax (trailing commas,
|
|
@@ -762,72 +648,6 @@ function loadSkillsFromDir(dir, source) {
|
|
|
762
648
|
}
|
|
763
649
|
return entries;
|
|
764
650
|
}
|
|
765
|
-
/**
|
|
766
|
-
* Resolve the bundled openclaw skills directory.
|
|
767
|
-
* Searches multiple known installation paths (npm, pnpm, nvm, etc.)
|
|
768
|
-
*/
|
|
769
|
-
function resolveBundledSkillsDir() {
|
|
770
|
-
const override = (process.env.OPENCLAW_BUNDLED_SKILLS_DIR ?? "").trim();
|
|
771
|
-
if (override)
|
|
772
|
-
return override;
|
|
773
|
-
const candidates = [];
|
|
774
|
-
// 1. Adjacent to node binary (nvm-style installs)
|
|
775
|
-
try {
|
|
776
|
-
candidates.push(join(dirname(process.execPath), "skills"));
|
|
777
|
-
}
|
|
778
|
-
catch { }
|
|
779
|
-
// 2. From argv[1] (openclaw.mjs inside gateway process)
|
|
780
|
-
try {
|
|
781
|
-
const argv1 = process.argv[1] ?? "";
|
|
782
|
-
if (argv1)
|
|
783
|
-
candidates.push(join(dirname(argv1), "skills"));
|
|
784
|
-
}
|
|
785
|
-
catch { }
|
|
786
|
-
// 3. Standard npm global: {execPath}/../lib/node_modules/openclaw/skills
|
|
787
|
-
try {
|
|
788
|
-
candidates.push(join(dirname(process.execPath), "..", "lib", "node_modules", "openclaw", "skills"));
|
|
789
|
-
}
|
|
790
|
-
catch { }
|
|
791
|
-
// 4. ~/.npm-global/lib/node_modules/openclaw/skills (common npm prefix)
|
|
792
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
793
|
-
if (home) {
|
|
794
|
-
candidates.push(join(home, ".npm-global", "lib", "node_modules", "openclaw", "skills"));
|
|
795
|
-
}
|
|
796
|
-
// 5. Resolve from `which openclaw` symlink → package root
|
|
797
|
-
try {
|
|
798
|
-
const openclawBin = join(home, ".npm-global", "bin", "openclaw");
|
|
799
|
-
if (existsSync(openclawBin)) {
|
|
800
|
-
const realPath = realpathSync(openclawBin);
|
|
801
|
-
// realPath = .../openclaw/openclaw.mjs → dirname = .../openclaw/
|
|
802
|
-
candidates.push(join(dirname(realPath), "skills"));
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
catch { }
|
|
806
|
-
// 6. pnpm global store (glob-style search for latest version)
|
|
807
|
-
if (home) {
|
|
808
|
-
try {
|
|
809
|
-
const pnpmBase = join(home, "Library", "pnpm", "global", "5", ".pnpm");
|
|
810
|
-
if (existsSync(pnpmBase)) {
|
|
811
|
-
const dirs = readdirSync(pnpmBase)
|
|
812
|
-
.filter(d => d.startsWith("openclaw@"))
|
|
813
|
-
.sort()
|
|
814
|
-
.reverse();
|
|
815
|
-
for (const d of dirs) {
|
|
816
|
-
candidates.push(join(pnpmBase, d, "node_modules", "openclaw", "skills"));
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
catch { }
|
|
821
|
-
}
|
|
822
|
-
for (const candidate of candidates) {
|
|
823
|
-
try {
|
|
824
|
-
if (existsSync(candidate))
|
|
825
|
-
return candidate;
|
|
826
|
-
}
|
|
827
|
-
catch { }
|
|
828
|
-
}
|
|
829
|
-
return undefined;
|
|
830
|
-
}
|
|
831
651
|
/**
|
|
832
652
|
* Resolve skill directories registered by enabled plugins.
|
|
833
653
|
* Each plugin's openclaw.plugin.json may declare "skills": ["./skills"]
|
|
@@ -932,28 +752,6 @@ function loadAllSkillEntries(workspaceDir, cfg) {
|
|
|
932
752
|
addAll(loadSkillsFromDir(join(resolve(workspaceDir), "skills"), "workspace"));
|
|
933
753
|
return Array.from(merged.values());
|
|
934
754
|
}
|
|
935
|
-
/**
|
|
936
|
-
* Load the full openclaw config from disk.
|
|
937
|
-
* api.config may not contain all sections (e.g. skills.entries),
|
|
938
|
-
* so we read openclaw.json directly for complete data.
|
|
939
|
-
*/
|
|
940
|
-
let _diskConfigCache = null;
|
|
941
|
-
function loadFullConfig() {
|
|
942
|
-
const configPath = resolveOpenClawConfigPath();
|
|
943
|
-
try {
|
|
944
|
-
const st = statSync(configPath);
|
|
945
|
-
if (_diskConfigCache && _diskConfigCache.mtimeMs === Math.floor(st.mtimeMs)) {
|
|
946
|
-
return _diskConfigCache.cfg;
|
|
947
|
-
}
|
|
948
|
-
const raw = readFileSync(configPath, "utf-8");
|
|
949
|
-
const cfg = JSON.parse(raw);
|
|
950
|
-
_diskConfigCache = { cfg, mtimeMs: Math.floor(st.mtimeMs) };
|
|
951
|
-
return cfg;
|
|
952
|
-
}
|
|
953
|
-
catch {
|
|
954
|
-
return {};
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
755
|
/**
|
|
958
756
|
* Build runtime status for a single skill entry.
|
|
959
757
|
*/
|
|
@@ -969,12 +767,10 @@ function buildSkillStatus(entry, cfg) {
|
|
|
969
767
|
const requiresAnyBins = entry.metadata?.requires?.anyBins ?? [];
|
|
970
768
|
const requiresEnv = entry.metadata?.requires?.env ?? [];
|
|
971
769
|
const requiresConfig = entry.metadata?.requires?.config ?? [];
|
|
972
|
-
const isEnvSatisfied = (name) => Boolean(process.env[name] || skillCfg?.env?.[name] ||
|
|
973
|
-
(skillCfg?.apiKey && entry.metadata?.primaryEnv === name));
|
|
974
770
|
const missingBins = always ? [] : requiresBins.filter(b => !hasBinarySync(b));
|
|
975
771
|
const missingAnyBins = always ? [] :
|
|
976
772
|
(requiresAnyBins.length > 0 && !requiresAnyBins.some(b => hasBinarySync(b)) ? requiresAnyBins : []);
|
|
977
|
-
const missingEnv = always ? [] : requiresEnv.filter(e => !isEnvSatisfied(e));
|
|
773
|
+
const missingEnv = always ? [] : requiresEnv.filter(e => !isEnvSatisfied(e, skillCfg, entry.metadata?.primaryEnv));
|
|
978
774
|
const isConfigPathSatisfied = (configPath) => {
|
|
979
775
|
// configPath like "channels.discord.token" → check fullCfg.channels.discord.token
|
|
980
776
|
const parts = configPath.split(".");
|
|
@@ -1016,32 +812,6 @@ function buildSkillStatus(entry, cfg) {
|
|
|
1016
812
|
})),
|
|
1017
813
|
};
|
|
1018
814
|
}
|
|
1019
|
-
/**
|
|
1020
|
-
* Fetch from ClawHub API.
|
|
1021
|
-
* Base URL: OPENCLAW_CLAWHUB_URL or CLAWHUB_URL env, or https://clawhub.ai
|
|
1022
|
-
* Mirrors gateway's fetchJson (clawhub-t8tftw_j.js).
|
|
1023
|
-
*/
|
|
1024
|
-
async function fetchClawHub(path, searchParams) {
|
|
1025
|
-
const baseUrl = ((process.env.OPENCLAW_CLAWHUB_URL ?? "").trim() ||
|
|
1026
|
-
(process.env.CLAWHUB_URL ?? "").trim() ||
|
|
1027
|
-
"https://clawhub.ai").replace(/\/+$/, "");
|
|
1028
|
-
let url = `${baseUrl}${path}`;
|
|
1029
|
-
if (searchParams && Object.keys(searchParams).length > 0) {
|
|
1030
|
-
url += `?${new URLSearchParams(searchParams).toString()}`;
|
|
1031
|
-
}
|
|
1032
|
-
const controller = new AbortController();
|
|
1033
|
-
const timer = setTimeout(() => controller.abort(), 30000);
|
|
1034
|
-
try {
|
|
1035
|
-
const response = await fetch(url, { signal: controller.signal });
|
|
1036
|
-
if (!response.ok) {
|
|
1037
|
-
throw new Error(`ClawHub ${path} failed (${response.status}): ${response.statusText}`);
|
|
1038
|
-
}
|
|
1039
|
-
return await response.json();
|
|
1040
|
-
}
|
|
1041
|
-
finally {
|
|
1042
|
-
clearTimeout(timer);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
815
|
// ---------------------------------------------------------------------------
|
|
1046
816
|
// Skills method handlers (file-based, no gateway relay)
|
|
1047
817
|
// ---------------------------------------------------------------------------
|
|
@@ -1156,7 +926,7 @@ async function handleSkillsSearch(api, params) {
|
|
|
1156
926
|
const searchParams = { q: query };
|
|
1157
927
|
if (params.limit)
|
|
1158
928
|
searchParams.limit = String(params.limit);
|
|
1159
|
-
const result = await fetchClawHub("/api/v1/search", searchParams);
|
|
929
|
+
const result = await fetchClawHub(resolveClawHubBaseUrl(), "/api/v1/search", searchParams);
|
|
1160
930
|
const results = result?.results ?? [];
|
|
1161
931
|
portalLog(api, "info", `skills.search: query="${query}" got ${results.length} results`);
|
|
1162
932
|
return { results };
|
|
@@ -1166,23 +936,6 @@ async function handleSkillsSearch(api, params) {
|
|
|
1166
936
|
return { results: [] };
|
|
1167
937
|
}
|
|
1168
938
|
}
|
|
1169
|
-
/**
|
|
1170
|
-
* Extract a tar.gz archive into targetDir, auto-detecting whether to strip
|
|
1171
|
-
* a single wrapping directory.
|
|
1172
|
-
*/
|
|
1173
|
-
async function extractTarGz(archivePath, targetDir) {
|
|
1174
|
-
const execFileAsync = promisify(execFile);
|
|
1175
|
-
const { stdout } = await execFileAsync("tar", ["tzf", archivePath]);
|
|
1176
|
-
const entries = stdout.trim().split("\n").filter(Boolean);
|
|
1177
|
-
const topDirs = new Set(entries.map(e => e.split("/")[0]));
|
|
1178
|
-
const needsStrip = topDirs.size === 1 && entries.every(e => e.includes("/"));
|
|
1179
|
-
if (needsStrip) {
|
|
1180
|
-
await execFileAsync("tar", ["xzf", archivePath, "-C", targetDir, "--strip-components=1"]);
|
|
1181
|
-
}
|
|
1182
|
-
else {
|
|
1183
|
-
await execFileAsync("tar", ["xzf", archivePath, "-C", targetDir]);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
939
|
/**
|
|
1187
940
|
* skills.install — install a skill into an agent's workspace.
|
|
1188
941
|
*
|
|
@@ -1211,17 +964,7 @@ async function handleSkillsInstall(api, params) {
|
|
|
1211
964
|
// Download
|
|
1212
965
|
let archiveBytes;
|
|
1213
966
|
try {
|
|
1214
|
-
|
|
1215
|
-
const timer = setTimeout(() => controller.abort(), 120000);
|
|
1216
|
-
try {
|
|
1217
|
-
const response = await fetch(url, { signal: controller.signal });
|
|
1218
|
-
if (!response.ok)
|
|
1219
|
-
throw new Error(`download failed (${response.status})`);
|
|
1220
|
-
archiveBytes = new Uint8Array(await response.arrayBuffer());
|
|
1221
|
-
}
|
|
1222
|
-
finally {
|
|
1223
|
-
clearTimeout(timer);
|
|
1224
|
-
}
|
|
967
|
+
archiveBytes = await downloadArchive(url);
|
|
1225
968
|
}
|
|
1226
969
|
catch (err) {
|
|
1227
970
|
throw { code: 503, message: `skills.install: download failed: ${err.message}` };
|
|
@@ -1276,7 +1019,7 @@ async function handleSkillsInstall(api, params) {
|
|
|
1276
1019
|
throw { code: 400, message: "slug (or name) is required for source=clawhub" };
|
|
1277
1020
|
let detail;
|
|
1278
1021
|
try {
|
|
1279
|
-
detail = await fetchClawHub(`/api/v1/skills/${encodeURIComponent(slug)}`);
|
|
1022
|
+
detail = await fetchClawHub(resolveClawHubBaseUrl(), `/api/v1/skills/${encodeURIComponent(slug)}`);
|
|
1280
1023
|
}
|
|
1281
1024
|
catch (err) {
|
|
1282
1025
|
throw { code: 503, message: `skills.install: ClawHub fetch failed: ${err.message}` };
|
|
@@ -1289,19 +1032,9 @@ async function handleSkillsInstall(api, params) {
|
|
|
1289
1032
|
let archiveBytes;
|
|
1290
1033
|
try {
|
|
1291
1034
|
const searchParams = { version };
|
|
1292
|
-
const archiveUrl = (
|
|
1293
|
-
+ `/api/v1/packages/${encodeURIComponent(slug)}/download?` + new URLSearchParams(searchParams)
|
|
1294
|
-
|
|
1295
|
-
const timer = setTimeout(() => controller.abort(), 60000);
|
|
1296
|
-
try {
|
|
1297
|
-
const response = await fetch(archiveUrl, { signal: controller.signal });
|
|
1298
|
-
if (!response.ok)
|
|
1299
|
-
throw new Error(`download failed (${response.status})`);
|
|
1300
|
-
archiveBytes = new Uint8Array(await response.arrayBuffer());
|
|
1301
|
-
}
|
|
1302
|
-
finally {
|
|
1303
|
-
clearTimeout(timer);
|
|
1304
|
-
}
|
|
1035
|
+
const archiveUrl = resolveClawHubBaseUrl()
|
|
1036
|
+
+ `/api/v1/packages/${encodeURIComponent(slug)}/download?` + new URLSearchParams(searchParams);
|
|
1037
|
+
archiveBytes = await downloadArchive(archiveUrl, 60000);
|
|
1305
1038
|
}
|
|
1306
1039
|
catch (err) {
|
|
1307
1040
|
throw { code: 503, message: `skills.install: download failed: ${err.message}` };
|
|
@@ -1389,7 +1122,7 @@ async function handleSkillsSet(api, params) {
|
|
|
1389
1122
|
throw { code: 500, message: `skills.set: failed to write config: ${err.message}` };
|
|
1390
1123
|
}
|
|
1391
1124
|
// Invalidate disk config cache
|
|
1392
|
-
|
|
1125
|
+
clearDiskConfigCache();
|
|
1393
1126
|
portalLog(api, "info", `skills.set: skillKey=${skillKey} enabled=${enabled}`);
|
|
1394
1127
|
return { ok: true, skillKey, enabled };
|
|
1395
1128
|
}
|
|
@@ -1475,7 +1208,7 @@ async function handleAgentSkillsSet(api, params) {
|
|
|
1475
1208
|
catch (err) {
|
|
1476
1209
|
throw { code: 500, message: `agent.skills.set: failed to write config: ${err.message}` };
|
|
1477
1210
|
}
|
|
1478
|
-
|
|
1211
|
+
clearDiskConfigCache();
|
|
1479
1212
|
const resultSkills = agentEntry.skills ?? undefined;
|
|
1480
1213
|
portalLog(api, "info", `agent.skills.set: agentId=${agentId} skill=${skillName} enabled=${enabled} skills=${JSON.stringify(resultSkills)}`);
|
|
1481
1214
|
return { ok: true, agentId, skillKey: skillName, enabled, skills: resultSkills };
|
|
@@ -1521,38 +1254,10 @@ async function handleAgentModelSet(api, params) {
|
|
|
1521
1254
|
catch (err) {
|
|
1522
1255
|
throw { code: 500, message: `agent.model.set: failed to write config: ${err.message}` };
|
|
1523
1256
|
}
|
|
1524
|
-
|
|
1257
|
+
clearDiskConfigCache();
|
|
1525
1258
|
portalLog(api, "info", `agent.model.set: agentId=${agentId} model=${model}`);
|
|
1526
1259
|
return { ok: true, agentId, model };
|
|
1527
1260
|
}
|
|
1528
|
-
/**
|
|
1529
|
-
* Resolve the path to the openclaw cron store file (jobs.json).
|
|
1530
|
-
*
|
|
1531
|
-
* Mirrors openclaw's own resolveCronStorePath / resolveConfigDir logic:
|
|
1532
|
-
* 1. OPENCLAW_STATE_DIR env var → {stateDir}/cron/jobs.json
|
|
1533
|
-
* 2. OPENCLAW_CONFIG_PATH env var → dirname(configPath)/cron/jobs.json
|
|
1534
|
-
* 3. cfg.cron?.store (from full openclaw config) → resolved path
|
|
1535
|
-
* 4. Default: ~/.openclaw/cron/jobs.json
|
|
1536
|
-
*/
|
|
1537
|
-
function resolveCronStorePath(api) {
|
|
1538
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
1539
|
-
const expandHome = (p) => p.startsWith("~/") || p === "~" ? join(home, p.slice(2)) : p;
|
|
1540
|
-
// 1. OPENCLAW_STATE_DIR
|
|
1541
|
-
const stateDir = (process.env.OPENCLAW_STATE_DIR ?? "").trim();
|
|
1542
|
-
if (stateDir)
|
|
1543
|
-
return resolve(join(expandHome(stateDir), "cron", "jobs.json"));
|
|
1544
|
-
// 2. OPENCLAW_CONFIG_PATH
|
|
1545
|
-
const configPath = (process.env.OPENCLAW_CONFIG_PATH ?? "").trim();
|
|
1546
|
-
if (configPath)
|
|
1547
|
-
return resolve(join(dirname(expandHome(configPath)), "cron", "jobs.json"));
|
|
1548
|
-
// 3. cfg.cron?.store
|
|
1549
|
-
const cfg = getConfig(api);
|
|
1550
|
-
const cfgStore = String(cfg?.cron?.store ?? "").trim();
|
|
1551
|
-
if (cfgStore)
|
|
1552
|
-
return resolve(expandHome(cfgStore));
|
|
1553
|
-
// 4. Default
|
|
1554
|
-
return join(home, ".openclaw", "cron", "jobs.json");
|
|
1555
|
-
}
|
|
1556
1261
|
/**
|
|
1557
1262
|
* cron.list — read jobs directly from the persisted cron store (jobs.json).
|
|
1558
1263
|
*
|