@useorgx/openclaw-plugin 0.4.1 → 0.4.4

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.
@@ -1,14 +1,76 @@
1
- import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
- import { getOrgxPluginConfigDir, getOrgxPluginConfigPath } from "./paths.js";
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getOpenClawDir } from "./paths.js";
3
4
  import { backupCorruptFileSync, writeJsonFileAtomicSync } from "./fs-utils.js";
4
- function configDir() {
5
- return getOrgxPluginConfigDir();
5
+ const PROVIDER_PROFILE_MAP = {
6
+ openaiApiKey: { profileId: "openai-codex", provider: "openai-codex" },
7
+ anthropicApiKey: { profileId: "anthropic", provider: "anthropic" },
8
+ openrouterApiKey: { profileId: "openrouter", provider: "openrouter" },
9
+ };
10
+ function isSafePathSegment(value) {
11
+ const normalized = value.trim();
12
+ if (!normalized || normalized === "." || normalized === "..")
13
+ return false;
14
+ if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("\0")) {
15
+ return false;
16
+ }
17
+ if (normalized.includes(".."))
18
+ return false;
19
+ return true;
20
+ }
21
+ function parseJson(value) {
22
+ try {
23
+ return JSON.parse(value);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
6
28
  }
7
- function byokFile() {
8
- return getOrgxPluginConfigPath("byok.json");
29
+ function readObject(value) {
30
+ return value && typeof value === "object" && !Array.isArray(value)
31
+ ? value
32
+ : {};
9
33
  }
10
- function ensureConfigDir() {
11
- const dir = configDir();
34
+ function resolveDefaultAgentId() {
35
+ try {
36
+ const configPath = join(getOpenClawDir(), "openclaw.json");
37
+ if (!existsSync(configPath))
38
+ return "main";
39
+ const raw = parseJson(readFileSync(configPath, "utf8"));
40
+ const agents = readObject(raw?.agents);
41
+ const list = Array.isArray(agents.list) ? agents.list : [];
42
+ for (const entry of list) {
43
+ if (!entry || typeof entry !== "object")
44
+ continue;
45
+ const row = entry;
46
+ if (row.default !== true)
47
+ continue;
48
+ const id = typeof row.id === "string" ? row.id.trim() : "";
49
+ if (id && isSafePathSegment(id))
50
+ return id;
51
+ }
52
+ for (const entry of list) {
53
+ if (!entry || typeof entry !== "object")
54
+ continue;
55
+ const row = entry;
56
+ const id = typeof row.id === "string" ? row.id.trim() : "";
57
+ if (id && isSafePathSegment(id))
58
+ return id;
59
+ }
60
+ }
61
+ catch {
62
+ // fall through
63
+ }
64
+ return "main";
65
+ }
66
+ function authProfilesDir() {
67
+ return join(getOpenClawDir(), "agents", resolveDefaultAgentId(), "agent");
68
+ }
69
+ function authProfilesFile() {
70
+ return join(authProfilesDir(), "auth-profiles.json");
71
+ }
72
+ function ensureAuthProfilesDir() {
73
+ const dir = authProfilesDir();
12
74
  mkdirSync(dir, { recursive: true, mode: 0o700 });
13
75
  try {
14
76
  chmodSync(dir, 0o700);
@@ -17,12 +79,56 @@ function ensureConfigDir() {
17
79
  // best effort
18
80
  }
19
81
  }
20
- function parseJson(value) {
82
+ function normalizeAuthProfileEntry(value) {
83
+ if (!value || typeof value !== "object")
84
+ return null;
85
+ const row = value;
86
+ const type = typeof row.type === "string" ? row.type.trim() : "";
87
+ const provider = typeof row.provider === "string" ? row.provider.trim() : "";
88
+ const key = typeof row.key === "string" ? row.key.trim() : "";
89
+ if (!type || !provider || !key)
90
+ return null;
91
+ return { type, provider, key };
92
+ }
93
+ function readAuthProfiles() {
94
+ const file = authProfilesFile();
21
95
  try {
22
- return JSON.parse(value);
96
+ if (!existsSync(file))
97
+ return { file, parsed: null };
98
+ const raw = readFileSync(file, "utf8");
99
+ const parsed = parseJson(raw);
100
+ if (!parsed || typeof parsed !== "object") {
101
+ backupCorruptFileSync(file);
102
+ return { file, parsed: null };
103
+ }
104
+ const profilesRaw = parsed.profiles && typeof parsed.profiles === "object"
105
+ ? parsed.profiles
106
+ : {};
107
+ const profiles = {};
108
+ for (const [profileId, entry] of Object.entries(profilesRaw)) {
109
+ const normalized = normalizeAuthProfileEntry(entry);
110
+ if (!normalized)
111
+ continue;
112
+ profiles[profileId] = normalized;
113
+ }
114
+ return {
115
+ file,
116
+ parsed: {
117
+ version: typeof parsed.version === "number" && Number.isFinite(parsed.version)
118
+ ? Math.floor(parsed.version)
119
+ : 1,
120
+ profiles,
121
+ lastGood: parsed.lastGood && typeof parsed.lastGood === "object"
122
+ ? parsed.lastGood
123
+ : undefined,
124
+ usageStats: parsed.usageStats && typeof parsed.usageStats === "object"
125
+ ? parsed.usageStats
126
+ : undefined,
127
+ },
128
+ };
23
129
  }
24
130
  catch {
25
- return null;
131
+ return { file, parsed: null };
26
132
  }
27
133
  }
28
134
  function normalizeKey(value) {
@@ -33,29 +139,29 @@ function normalizeKey(value) {
33
139
  return null;
34
140
  return trimmed;
35
141
  }
142
+ function findProfileKey(profiles, provider) {
143
+ const entries = Object.entries(profiles);
144
+ if (provider === "openai") {
145
+ const codex = entries.find(([, entry]) => entry.provider === "openai-codex");
146
+ if (codex)
147
+ return codex[1].key;
148
+ const openai = entries.find(([, entry]) => entry.provider === "openai");
149
+ return openai?.[1].key ?? null;
150
+ }
151
+ return entries.find(([, entry]) => entry.provider === provider)?.[1].key ?? null;
152
+ }
36
153
  export function readByokKeys() {
37
- const file = byokFile();
154
+ const { file, parsed } = readAuthProfiles();
38
155
  try {
39
- if (!existsSync(file))
40
- return null;
41
- const raw = readFileSync(file, "utf8");
42
- const parsed = parseJson(raw);
43
- if (!parsed) {
44
- backupCorruptFileSync(file);
45
- return null;
46
- }
47
- if (!parsed || typeof parsed !== "object")
156
+ if (!parsed)
48
157
  return null;
49
- const createdAt = typeof parsed.createdAt === "string" && parsed.createdAt.trim().length > 0
50
- ? parsed.createdAt
51
- : new Date().toISOString();
52
- const updatedAt = typeof parsed.updatedAt === "string" && parsed.updatedAt.trim().length > 0
53
- ? parsed.updatedAt
54
- : createdAt;
158
+ const stats = statSync(file);
159
+ const createdAt = stats.birthtime.toISOString();
160
+ const updatedAt = stats.mtime.toISOString();
55
161
  return {
56
- openaiApiKey: normalizeKey(parsed.openaiApiKey) ?? null,
57
- anthropicApiKey: normalizeKey(parsed.anthropicApiKey) ?? null,
58
- openrouterApiKey: normalizeKey(parsed.openrouterApiKey) ?? null,
162
+ openaiApiKey: normalizeKey(findProfileKey(parsed.profiles, "openai")) ?? null,
163
+ anthropicApiKey: normalizeKey(findProfileKey(parsed.profiles, "anthropic")) ?? null,
164
+ openrouterApiKey: normalizeKey(findProfileKey(parsed.profiles, "openrouter")) ?? null,
59
165
  createdAt,
60
166
  updatedAt,
61
167
  };
@@ -65,33 +171,54 @@ export function readByokKeys() {
65
171
  }
66
172
  }
67
173
  export function writeByokKeys(input) {
68
- ensureConfigDir();
69
- const now = new Date().toISOString();
70
- const existing = readByokKeys();
174
+ ensureAuthProfilesDir();
175
+ const existingParsed = readAuthProfiles().parsed;
176
+ const next = existingParsed ?? {
177
+ version: 1,
178
+ profiles: {},
179
+ };
71
180
  const has = (key) => Object.prototype.hasOwnProperty.call(input, key);
72
- const next = {
73
- openaiApiKey: has("openaiApiKey")
74
- ? normalizeKey(input.openaiApiKey)
75
- : existing?.openaiApiKey ?? null,
76
- anthropicApiKey: has("anthropicApiKey")
77
- ? normalizeKey(input.anthropicApiKey)
78
- : existing?.anthropicApiKey ?? null,
79
- openrouterApiKey: has("openrouterApiKey")
80
- ? normalizeKey(input.openrouterApiKey)
81
- : existing?.openrouterApiKey ?? null,
82
- createdAt: existing?.createdAt ?? now,
83
- updatedAt: now,
181
+ const applyKey = (field, value) => {
182
+ const mapped = PROVIDER_PROFILE_MAP[field];
183
+ const normalized = normalizeKey(value);
184
+ if (normalized) {
185
+ next.profiles[mapped.profileId] = {
186
+ type: "api_key",
187
+ provider: mapped.provider,
188
+ key: normalized,
189
+ };
190
+ return;
191
+ }
192
+ delete next.profiles[mapped.profileId];
193
+ if (next.lastGood)
194
+ delete next.lastGood[mapped.profileId];
195
+ if (next.usageStats)
196
+ delete next.usageStats[mapped.profileId];
84
197
  };
85
- const file = byokFile();
198
+ if (has("openaiApiKey"))
199
+ applyKey("openaiApiKey", input.openaiApiKey);
200
+ if (has("anthropicApiKey"))
201
+ applyKey("anthropicApiKey", input.anthropicApiKey);
202
+ if (has("openrouterApiKey"))
203
+ applyKey("openrouterApiKey", input.openrouterApiKey);
204
+ const file = authProfilesFile();
86
205
  writeJsonFileAtomicSync(file, next, 0o600);
87
- return next;
206
+ const updated = readByokKeys();
207
+ if (updated)
208
+ return updated;
209
+ const now = new Date().toISOString();
210
+ return {
211
+ openaiApiKey: null,
212
+ anthropicApiKey: null,
213
+ openrouterApiKey: null,
214
+ createdAt: now,
215
+ updatedAt: now,
216
+ };
88
217
  }
89
218
  export function clearByokKeys() {
90
- const file = byokFile();
91
- try {
92
- rmSync(file, { force: true });
93
- }
94
- catch {
95
- // best effort
96
- }
219
+ writeByokKeys({
220
+ openaiApiKey: null,
221
+ anthropicApiKey: null,
222
+ openrouterApiKey: null,
223
+ });
97
224
  }
@@ -201,16 +201,36 @@ export class OrgXClient {
201
201
  // Spawn Guard (Quality Gate + Model Routing)
202
202
  // ===========================================================================
203
203
  async checkSpawnGuard(domain, taskId) {
204
- return this.post("/api/client/spawn", {
204
+ const response = await this.post("/api/client/spawn", {
205
205
  domain,
206
206
  taskId,
207
207
  });
208
+ // Newer servers wrap responses in { ok, data } while older clients expect the
209
+ // SpawnGuardResult fields at top-level.
210
+ if (response &&
211
+ typeof response === "object" &&
212
+ "data" in response &&
213
+ response.data) {
214
+ return response.data;
215
+ }
216
+ return response;
208
217
  }
209
218
  // ===========================================================================
210
219
  // Quality Scores
211
220
  // ===========================================================================
212
221
  async recordQuality(score) {
213
- return this.post("/api/client/quality", score);
222
+ const response = await this.post("/api/client/quality", score);
223
+ // Backwards-compatible: accept either { success: true } or { ok: true, data: ... }.
224
+ if (response &&
225
+ typeof response === "object" &&
226
+ "success" in response &&
227
+ typeof response.success === "boolean") {
228
+ return response;
229
+ }
230
+ if (response && typeof response === "object" && "ok" in response) {
231
+ return { success: Boolean(response.ok) };
232
+ }
233
+ return { success: true };
214
234
  }
215
235
  // ===========================================================================
216
236
  // Entity CRUD
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import { runGatewayWatchdogDaemon } from "./gateway-watchdog.js";
2
+ void runGatewayWatchdogDaemon().catch((err) => {
3
+ const message = err instanceof Error ? err.message : String(err);
4
+ console.error(`[orgx] gateway-watchdog crashed: ${message}`);
5
+ process.exit(1);
6
+ });
@@ -0,0 +1,11 @@
1
+ type Logger = {
2
+ info?: (message: string, meta?: Record<string, unknown>) => void;
3
+ warn?: (message: string, meta?: Record<string, unknown>) => void;
4
+ debug?: (message: string, meta?: Record<string, unknown>) => void;
5
+ };
6
+ export declare function runGatewayWatchdogDaemon(logger?: Logger): Promise<void>;
7
+ export declare function ensureGatewayWatchdog(logger: Logger): {
8
+ started: boolean;
9
+ pid: number | null;
10
+ };
11
+ export {};
@@ -0,0 +1,221 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import { join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { getOpenClawDir } from "./paths.js";
6
+ import { readOpenClawGatewayPort, readOpenClawSettingsSnapshot } from "./openclaw-settings.js";
7
+ const DEFAULT_MONITOR_INTERVAL_MS = 30_000;
8
+ const DEFAULT_FAILURES_BEFORE_RESTART = 2;
9
+ const DEFAULT_PROBE_TIMEOUT_MS = 2_500;
10
+ const WATCHDOG_PID_FILE = join(getOpenClawDir(), "orgx-gateway-watchdog.pid");
11
+ function readEnvNumber(name, fallback, min) {
12
+ const raw = (process.env[name] ?? "").trim();
13
+ if (!raw)
14
+ return fallback;
15
+ const parsed = Number.parseInt(raw, 10);
16
+ if (!Number.isFinite(parsed) || parsed < min)
17
+ return fallback;
18
+ return parsed;
19
+ }
20
+ function isPidAlive(pid) {
21
+ if (!Number.isFinite(pid) || pid <= 0)
22
+ return false;
23
+ try {
24
+ process.kill(pid, 0);
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function readWatchdogPid() {
32
+ try {
33
+ if (!existsSync(WATCHDOG_PID_FILE))
34
+ return null;
35
+ const raw = readFileSync(WATCHDOG_PID_FILE, "utf8").trim();
36
+ const parsed = Number.parseInt(raw, 10);
37
+ if (!Number.isFinite(parsed) || parsed <= 0)
38
+ return null;
39
+ return parsed;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function writeWatchdogPid(pid) {
46
+ const dir = getOpenClawDir();
47
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
48
+ writeFileSync(WATCHDOG_PID_FILE, `${pid}\n`, { mode: 0o600 });
49
+ }
50
+ function clearWatchdogPid() {
51
+ try {
52
+ rmSync(WATCHDOG_PID_FILE, { force: true });
53
+ }
54
+ catch {
55
+ // best effort
56
+ }
57
+ }
58
+ async function runCommandCollect(input) {
59
+ const timeoutMs = input.timeoutMs ?? 10_000;
60
+ return await new Promise((resolve, reject) => {
61
+ const child = spawn(input.command, input.args, {
62
+ env: process.env,
63
+ stdio: ["ignore", "pipe", "pipe"],
64
+ });
65
+ let stdout = "";
66
+ let stderr = "";
67
+ const timer = timeoutMs
68
+ ? setTimeout(() => {
69
+ try {
70
+ child.kill("SIGKILL");
71
+ }
72
+ catch {
73
+ // best effort
74
+ }
75
+ reject(new Error(`Command timed out after ${timeoutMs}ms`));
76
+ }, timeoutMs)
77
+ : null;
78
+ child.stdout?.on("data", (chunk) => {
79
+ stdout += chunk.toString("utf8");
80
+ });
81
+ child.stderr?.on("data", (chunk) => {
82
+ stderr += chunk.toString("utf8");
83
+ });
84
+ child.on("error", (err) => {
85
+ if (timer)
86
+ clearTimeout(timer);
87
+ reject(err);
88
+ });
89
+ child.on("close", (code) => {
90
+ if (timer)
91
+ clearTimeout(timer);
92
+ resolve({ stdout, stderr, exitCode: typeof code === "number" ? code : null });
93
+ });
94
+ });
95
+ }
96
+ async function probeGateway(port, timeoutMs) {
97
+ const controller = new AbortController();
98
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
99
+ try {
100
+ // Any HTTP response (including 404) means the gateway port is reachable.
101
+ await fetch(`http://127.0.0.1:${port}/`, {
102
+ method: "GET",
103
+ signal: controller.signal,
104
+ headers: {
105
+ "cache-control": "no-cache",
106
+ },
107
+ });
108
+ return true;
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ finally {
114
+ clearTimeout(timeout);
115
+ }
116
+ }
117
+ async function restartGateway(logger) {
118
+ const restart = await runCommandCollect({
119
+ command: "openclaw",
120
+ args: ["gateway", "restart", "--json"],
121
+ timeoutMs: 30_000,
122
+ });
123
+ if (restart.exitCode === 0) {
124
+ logger.warn?.("[orgx] Gateway watchdog restarted OpenClaw gateway service");
125
+ return;
126
+ }
127
+ const start = await runCommandCollect({
128
+ command: "openclaw",
129
+ args: ["gateway", "start", "--json"],
130
+ timeoutMs: 30_000,
131
+ });
132
+ if (start.exitCode !== 0) {
133
+ throw new Error(start.stderr.trim() || restart.stderr.trim() || "Failed to restart gateway");
134
+ }
135
+ logger.warn?.("[orgx] Gateway watchdog started OpenClaw gateway service");
136
+ }
137
+ export async function runGatewayWatchdogDaemon(logger = console) {
138
+ const monitorIntervalMs = readEnvNumber("ORGX_GATEWAY_WATCHDOG_INTERVAL_MS", DEFAULT_MONITOR_INTERVAL_MS, 5_000);
139
+ const failuresBeforeRestart = readEnvNumber("ORGX_GATEWAY_WATCHDOG_FAILURES", DEFAULT_FAILURES_BEFORE_RESTART, 1);
140
+ const probeTimeoutMs = readEnvNumber("ORGX_GATEWAY_WATCHDOG_TIMEOUT_MS", DEFAULT_PROBE_TIMEOUT_MS, 500);
141
+ let consecutiveFailures = 0;
142
+ let restartInFlight = false;
143
+ const cleanup = () => {
144
+ const pid = readWatchdogPid();
145
+ if (pid === process.pid) {
146
+ clearWatchdogPid();
147
+ }
148
+ };
149
+ process.on("SIGTERM", () => {
150
+ cleanup();
151
+ process.exit(0);
152
+ });
153
+ process.on("SIGINT", () => {
154
+ cleanup();
155
+ process.exit(0);
156
+ });
157
+ process.on("exit", cleanup);
158
+ writeWatchdogPid(process.pid);
159
+ logger.info?.("[orgx] Gateway watchdog daemon started", {
160
+ intervalMs: monitorIntervalMs,
161
+ failuresBeforeRestart,
162
+ });
163
+ const tick = async () => {
164
+ if (restartInFlight)
165
+ return;
166
+ const snapshot = readOpenClawSettingsSnapshot();
167
+ const port = readOpenClawGatewayPort(snapshot.raw);
168
+ const healthy = await probeGateway(port, probeTimeoutMs);
169
+ if (healthy) {
170
+ consecutiveFailures = 0;
171
+ return;
172
+ }
173
+ consecutiveFailures += 1;
174
+ logger.warn?.("[orgx] Gateway watchdog probe failed", {
175
+ port,
176
+ consecutiveFailures,
177
+ threshold: failuresBeforeRestart,
178
+ });
179
+ if (consecutiveFailures < failuresBeforeRestart) {
180
+ return;
181
+ }
182
+ restartInFlight = true;
183
+ try {
184
+ await restartGateway(logger);
185
+ consecutiveFailures = 0;
186
+ }
187
+ catch (err) {
188
+ logger.warn?.("[orgx] Gateway watchdog failed to restart gateway", {
189
+ error: err instanceof Error ? err.message : String(err),
190
+ });
191
+ }
192
+ finally {
193
+ restartInFlight = false;
194
+ }
195
+ };
196
+ await tick();
197
+ setInterval(() => {
198
+ void tick();
199
+ }, monitorIntervalMs);
200
+ }
201
+ export function ensureGatewayWatchdog(logger) {
202
+ if (process.env.ORGX_DISABLE_GATEWAY_WATCHDOG === "1") {
203
+ logger.debug?.("[orgx] Gateway watchdog disabled via ORGX_DISABLE_GATEWAY_WATCHDOG=1");
204
+ return { started: false, pid: null };
205
+ }
206
+ const existing = readWatchdogPid();
207
+ if (existing && isPidAlive(existing)) {
208
+ return { started: false, pid: existing };
209
+ }
210
+ if (existing && !isPidAlive(existing)) {
211
+ clearWatchdogPid();
212
+ }
213
+ const runnerPath = fileURLToPath(new URL("./gateway-watchdog-runner.js", import.meta.url));
214
+ const child = spawn(process.execPath, [runnerPath], {
215
+ env: process.env,
216
+ stdio: "ignore",
217
+ detached: true,
218
+ });
219
+ child.unref();
220
+ return { started: true, pid: child.pid ?? null };
221
+ }