mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-03 03:03:24 -04:00
chore: Enable "curly" rule to avoid single-statement if confusion/errors.
This commit is contained in:
@@ -4,13 +4,17 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
export function resolveBundledHooksDir(): string | undefined {
|
||||
const override = process.env.OPENCLAW_BUNDLED_HOOKS_DIR?.trim();
|
||||
if (override) return override;
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
|
||||
// bun --compile: ship a sibling `hooks/bundled/` next to the executable.
|
||||
try {
|
||||
const execDir = path.dirname(process.execPath);
|
||||
const sibling = path.join(execDir, "hooks", "bundled");
|
||||
if (fs.existsSync(sibling)) return sibling;
|
||||
if (fs.existsSync(sibling)) {
|
||||
return sibling;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -20,7 +24,9 @@ export function resolveBundledHooksDir(): string | undefined {
|
||||
try {
|
||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const distBundled = path.join(moduleDir, "bundled");
|
||||
if (fs.existsSync(distBundled)) return distBundled;
|
||||
if (fs.existsSync(distBundled)) {
|
||||
return distBundled;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -31,7 +37,9 @@ export function resolveBundledHooksDir(): string | undefined {
|
||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(moduleDir, "..", "..");
|
||||
const srcBundled = path.join(root, "src", "hooks", "bundled");
|
||||
if (fs.existsSync(srcBundled)) return srcBundled;
|
||||
if (fs.existsSync(srcBundled)) {
|
||||
return srcBundled;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@@ -6,21 +6,31 @@ import { applySoulEvilOverride, resolveSoulEvilConfigFromHook } from "../../soul
|
||||
const HOOK_KEY = "soul-evil";
|
||||
|
||||
const soulEvilHook: HookHandler = async (event) => {
|
||||
if (!isAgentBootstrapEvent(event)) return;
|
||||
if (!isAgentBootstrapEvent(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = event.context;
|
||||
if (context.sessionKey && isSubagentSessionKey(context.sessionKey)) return;
|
||||
if (context.sessionKey && isSubagentSessionKey(context.sessionKey)) {
|
||||
return;
|
||||
}
|
||||
const cfg = context.cfg;
|
||||
const hookConfig = resolveHookConfig(cfg, HOOK_KEY);
|
||||
if (!hookConfig || hookConfig.enabled === false) return;
|
||||
if (!hookConfig || hookConfig.enabled === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const soulConfig = resolveSoulEvilConfigFromHook(hookConfig as Record<string, unknown>, {
|
||||
warn: (message) => console.warn(`[soul-evil] ${message}`),
|
||||
});
|
||||
if (!soulConfig) return;
|
||||
if (!soulConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceDir = context.workspaceDir;
|
||||
if (!workspaceDir || !Array.isArray(context.bootstrapFiles)) return;
|
||||
if (!workspaceDir || !Array.isArray(context.bootstrapFiles)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await applySoulEvilOverride({
|
||||
files: context.bootstrapFiles,
|
||||
|
||||
@@ -11,10 +11,18 @@ const DEFAULT_CONFIG_VALUES: Record<string, boolean> = {
|
||||
};
|
||||
|
||||
function isTruthy(value: unknown): boolean {
|
||||
if (value === undefined || value === null) return false;
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "number") return value !== 0;
|
||||
if (typeof value === "string") return value.trim().length > 0;
|
||||
if (value === undefined || value === null) {
|
||||
return false;
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return value !== 0;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -22,7 +30,9 @@ export function resolveConfigPath(config: OpenClawConfig | undefined, pathStr: s
|
||||
const parts = pathStr.split(".").filter(Boolean);
|
||||
let current: unknown = config;
|
||||
for (const part of parts) {
|
||||
if (typeof current !== "object" || current === null) return undefined;
|
||||
if (typeof current !== "object" || current === null) {
|
||||
return undefined;
|
||||
}
|
||||
current = (current as Record<string, unknown>)[part];
|
||||
}
|
||||
return current;
|
||||
@@ -41,9 +51,13 @@ export function resolveHookConfig(
|
||||
hookKey: string,
|
||||
): HookConfig | undefined {
|
||||
const hooks = config?.hooks?.internal?.entries;
|
||||
if (!hooks || typeof hooks !== "object") return undefined;
|
||||
if (!hooks || typeof hooks !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const entry = (hooks as Record<string, HookConfig | undefined>)[hookKey];
|
||||
if (!entry || typeof entry !== "object") return undefined;
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -79,7 +93,9 @@ export function shouldIncludeHook(params: {
|
||||
const remotePlatforms = eligibility?.remote?.platforms ?? [];
|
||||
|
||||
// Check if explicitly disabled
|
||||
if (!pluginManaged && hookConfig?.enabled === false) return false;
|
||||
if (!pluginManaged && hookConfig?.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check OS requirement
|
||||
if (
|
||||
@@ -99,8 +115,12 @@ export function shouldIncludeHook(params: {
|
||||
const requiredBins = entry.metadata?.requires?.bins ?? [];
|
||||
if (requiredBins.length > 0) {
|
||||
for (const bin of requiredBins) {
|
||||
if (hasBinary(bin)) continue;
|
||||
if (eligibility?.remote?.hasBin?.(bin)) continue;
|
||||
if (hasBinary(bin)) {
|
||||
continue;
|
||||
}
|
||||
if (eligibility?.remote?.hasBin?.(bin)) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -111,15 +131,21 @@ export function shouldIncludeHook(params: {
|
||||
const anyFound =
|
||||
requiredAnyBins.some((bin) => hasBinary(bin)) ||
|
||||
eligibility?.remote?.hasAnyBin?.(requiredAnyBins);
|
||||
if (!anyFound) return false;
|
||||
if (!anyFound) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check required environment variables
|
||||
const requiredEnv = entry.metadata?.requires?.env ?? [];
|
||||
if (requiredEnv.length > 0) {
|
||||
for (const envName of requiredEnv) {
|
||||
if (process.env[envName]) continue;
|
||||
if (hookConfig?.env?.[envName]) continue;
|
||||
if (process.env[envName]) {
|
||||
continue;
|
||||
}
|
||||
if (hookConfig?.env?.[envName]) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -128,7 +154,9 @@ export function shouldIncludeHook(params: {
|
||||
const requiredConfig = entry.metadata?.requires?.config ?? [];
|
||||
if (requiredConfig.length > 0) {
|
||||
for (const configPath of requiredConfig) {
|
||||
if (!isConfigPathTruthy(config, configPath)) return false;
|
||||
if (!isConfigPathTruthy(config, configPath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ export function parseFrontmatter(content: string): ParsedHookFrontmatter {
|
||||
}
|
||||
|
||||
function normalizeStringList(input: unknown): string[] {
|
||||
if (!input) return [];
|
||||
if (!input) {
|
||||
return [];
|
||||
}
|
||||
if (Array.isArray(input)) {
|
||||
return input.map((value) => String(value).trim()).filter(Boolean);
|
||||
}
|
||||
@@ -30,7 +32,9 @@ function normalizeStringList(input: unknown): string[] {
|
||||
}
|
||||
|
||||
function parseInstallSpec(input: unknown): HookInstallSpec | undefined {
|
||||
if (!input || typeof input !== "object") return undefined;
|
||||
if (!input || typeof input !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const raw = input as Record<string, unknown>;
|
||||
const kindRaw =
|
||||
typeof raw.kind === "string" ? raw.kind : typeof raw.type === "string" ? raw.type : "";
|
||||
@@ -43,12 +47,22 @@ function parseInstallSpec(input: unknown): HookInstallSpec | undefined {
|
||||
kind: kind,
|
||||
};
|
||||
|
||||
if (typeof raw.id === "string") spec.id = raw.id;
|
||||
if (typeof raw.label === "string") spec.label = raw.label;
|
||||
if (typeof raw.id === "string") {
|
||||
spec.id = raw.id;
|
||||
}
|
||||
if (typeof raw.label === "string") {
|
||||
spec.label = raw.label;
|
||||
}
|
||||
const bins = normalizeStringList(raw.bins);
|
||||
if (bins.length > 0) spec.bins = bins;
|
||||
if (typeof raw.package === "string") spec.package = raw.package;
|
||||
if (typeof raw.repository === "string") spec.repository = raw.repository;
|
||||
if (bins.length > 0) {
|
||||
spec.bins = bins;
|
||||
}
|
||||
if (typeof raw.package === "string") {
|
||||
spec.package = raw.package;
|
||||
}
|
||||
if (typeof raw.repository === "string") {
|
||||
spec.repository = raw.repository;
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
@@ -67,10 +81,14 @@ export function resolveOpenClawMetadata(
|
||||
frontmatter: ParsedHookFrontmatter,
|
||||
): OpenClawHookMetadata | undefined {
|
||||
const raw = getFrontmatterValue(frontmatter, "metadata");
|
||||
if (!raw) return undefined;
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON5.parse(raw);
|
||||
if (!parsed || typeof parsed !== "object") return undefined;
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const metadataRawCandidates = [MANIFEST_KEY, ...LEGACY_MANIFEST_KEYS];
|
||||
let metadataRaw: unknown;
|
||||
for (const key of metadataRawCandidates) {
|
||||
@@ -80,7 +98,9 @@ export function resolveOpenClawMetadata(
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!metadataRaw || typeof metadataRaw !== "object") return undefined;
|
||||
if (!metadataRaw || typeof metadataRaw !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const metadataObj = metadataRaw as Record<string, unknown>;
|
||||
const requiresRaw =
|
||||
typeof metadataObj.requires === "object" && metadataObj.requires !== null
|
||||
|
||||
@@ -332,7 +332,9 @@ export async function runGmailService(opts: GmailRunOptions) {
|
||||
}, renewMs);
|
||||
|
||||
const shutdown = () => {
|
||||
if (shuttingDown) return;
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
shuttingDown = true;
|
||||
clearInterval(renewTimer);
|
||||
child.kill("SIGTERM");
|
||||
@@ -342,10 +344,14 @@ export async function runGmailService(opts: GmailRunOptions) {
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
child.on("exit", () => {
|
||||
if (shuttingDown) return;
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log("gog watch serve exited; restarting in 2s");
|
||||
setTimeout(() => {
|
||||
if (shuttingDown) return;
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
child = spawnGogServe(runtimeConfig);
|
||||
}, 2000);
|
||||
});
|
||||
@@ -365,7 +371,9 @@ async function startGmailWatch(
|
||||
const result = await runCommandWithTimeout(args, { timeoutMs: 120_000 });
|
||||
if (result.code !== 0) {
|
||||
const message = result.stderr || result.stdout || "gog watch start failed";
|
||||
if (fatal) throw new Error(message);
|
||||
if (fatal) {
|
||||
throw new Error(message);
|
||||
}
|
||||
defaultRuntime.error(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,12 @@ const MAX_OUTPUT_CHARS = 800;
|
||||
|
||||
function trimOutput(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return "";
|
||||
if (trimmed.length <= MAX_OUTPUT_CHARS) return trimmed;
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
if (trimmed.length <= MAX_OUTPUT_CHARS) {
|
||||
return trimmed;
|
||||
}
|
||||
return `${trimmed.slice(0, MAX_OUTPUT_CHARS)}…`;
|
||||
}
|
||||
|
||||
@@ -23,8 +27,12 @@ function formatCommandFailure(command: string, result: SpawnResult): string {
|
||||
const stderr = trimOutput(result.stderr);
|
||||
const stdout = trimOutput(result.stdout);
|
||||
const lines = [`${command} failed (code=${code}${signal}${killed})`];
|
||||
if (stderr) lines.push(`stderr: ${stderr}`);
|
||||
if (stdout) lines.push(`stdout: ${stdout}`);
|
||||
if (stderr) {
|
||||
lines.push(`stderr: ${stderr}`);
|
||||
}
|
||||
if (stdout) {
|
||||
lines.push(`stdout: ${stdout}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
@@ -35,8 +43,12 @@ function formatCommandResult(command: string, result: SpawnResult): string {
|
||||
const stderr = trimOutput(result.stderr);
|
||||
const stdout = trimOutput(result.stdout);
|
||||
const lines = [`${command} exited (code=${code}${signal}${killed})`];
|
||||
if (stderr) lines.push(`stderr: ${stderr}`);
|
||||
if (stdout) lines.push(`stdout: ${stdout}`);
|
||||
if (stderr) {
|
||||
lines.push(`stderr: ${stderr}`);
|
||||
}
|
||||
if (stdout) {
|
||||
lines.push(`stdout: ${stdout}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
@@ -57,7 +69,9 @@ function findExecutablesOnPath(bins: string[]): string[] {
|
||||
for (const part of parts) {
|
||||
for (const bin of bins) {
|
||||
const candidate = path.join(part, bin);
|
||||
if (seen.has(candidate)) continue;
|
||||
if (seen.has(candidate)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
fs.accessSync(candidate, fs.constants.X_OK);
|
||||
matches.push(candidate);
|
||||
@@ -73,13 +87,17 @@ function findExecutablesOnPath(bins: string[]): string[] {
|
||||
function ensurePathIncludes(dirPath: string, position: "append" | "prepend") {
|
||||
const pathEnv = process.env.PATH ?? "";
|
||||
const parts = pathEnv.split(path.delimiter).filter(Boolean);
|
||||
if (parts.includes(dirPath)) return;
|
||||
if (parts.includes(dirPath)) {
|
||||
return;
|
||||
}
|
||||
const next = position === "prepend" ? [dirPath, ...parts] : [...parts, dirPath];
|
||||
process.env.PATH = next.join(path.delimiter);
|
||||
}
|
||||
|
||||
function ensureGcloudOnPath(): boolean {
|
||||
if (hasBinary("gcloud")) return true;
|
||||
if (hasBinary("gcloud")) {
|
||||
return true;
|
||||
}
|
||||
const candidates = [
|
||||
"/opt/homebrew/share/google-cloud-sdk/bin/gcloud",
|
||||
"/usr/local/share/google-cloud-sdk/bin/gcloud",
|
||||
@@ -108,9 +126,13 @@ export async function resolvePythonExecutablePath(): Promise<string | undefined>
|
||||
[candidate, "-c", "import os, sys; print(os.path.realpath(sys.executable))"],
|
||||
{ timeoutMs: 2_000 },
|
||||
);
|
||||
if (res.code !== 0) continue;
|
||||
if (res.code !== 0) {
|
||||
continue;
|
||||
}
|
||||
const resolved = res.stdout.trim().split(/\s+/)[0];
|
||||
if (!resolved) continue;
|
||||
if (!resolved) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
fs.accessSync(resolved, fs.constants.X_OK);
|
||||
cachedPythonPath = resolved;
|
||||
@@ -124,9 +146,13 @@ export async function resolvePythonExecutablePath(): Promise<string | undefined>
|
||||
}
|
||||
|
||||
async function gcloudEnv(): Promise<NodeJS.ProcessEnv | undefined> {
|
||||
if (process.env.CLOUDSDK_PYTHON) return undefined;
|
||||
if (process.env.CLOUDSDK_PYTHON) {
|
||||
return undefined;
|
||||
}
|
||||
const pythonPath = await resolvePythonExecutablePath();
|
||||
if (!pythonPath) return undefined;
|
||||
if (!pythonPath) {
|
||||
return undefined;
|
||||
}
|
||||
return { CLOUDSDK_PYTHON: pythonPath };
|
||||
}
|
||||
|
||||
@@ -141,8 +167,12 @@ async function runGcloudCommand(
|
||||
}
|
||||
|
||||
export async function ensureDependency(bin: string, brewArgs: string[]) {
|
||||
if (bin === "gcloud" && ensureGcloudOnPath()) return;
|
||||
if (hasBinary(bin)) return;
|
||||
if (bin === "gcloud" && ensureGcloudOnPath()) {
|
||||
return;
|
||||
}
|
||||
if (hasBinary(bin)) {
|
||||
return;
|
||||
}
|
||||
if (process.platform !== "darwin") {
|
||||
throw new Error(`${bin} not installed; install it and retry`);
|
||||
}
|
||||
@@ -167,7 +197,9 @@ export async function ensureGcloudAuth() {
|
||||
["auth", "list", "--filter", "status:ACTIVE", "--format", "value(account)"],
|
||||
30_000,
|
||||
);
|
||||
if (res.code === 0 && res.stdout.trim()) return;
|
||||
if (res.code === 0 && res.stdout.trim()) {
|
||||
return;
|
||||
}
|
||||
const login = await runGcloudCommand(["auth", "login"], 600_000);
|
||||
if (login.code !== 0) {
|
||||
throw new Error(login.stderr || "gcloud auth login failed");
|
||||
@@ -187,7 +219,9 @@ export async function ensureTopic(projectId: string, topicName: string) {
|
||||
["pubsub", "topics", "describe", topicName, "--project", projectId],
|
||||
30_000,
|
||||
);
|
||||
if (describe.code === 0) return;
|
||||
if (describe.code === 0) {
|
||||
return;
|
||||
}
|
||||
await runGcloud(["pubsub", "topics", "create", topicName, "--project", projectId]);
|
||||
}
|
||||
|
||||
@@ -235,7 +269,9 @@ export async function ensureTailscaleEndpoint(params: {
|
||||
target?: string;
|
||||
token?: string;
|
||||
}): Promise<string> {
|
||||
if (params.mode === "off") return "";
|
||||
if (params.mode === "off") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const statusArgs = ["status", "--json"];
|
||||
const statusCommand = formatCommand("tailscale", statusArgs);
|
||||
@@ -283,13 +319,17 @@ export async function ensureTailscaleEndpoint(params: {
|
||||
export async function resolveProjectIdFromGogCredentials(): Promise<string | null> {
|
||||
const candidates = gogCredentialsPaths();
|
||||
for (const candidate of candidates) {
|
||||
if (!fs.existsSync(candidate)) continue;
|
||||
if (!fs.existsSync(candidate)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const raw = fs.readFileSync(candidate, "utf-8");
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
const clientId = extractGogClientId(parsed);
|
||||
const projectNumber = extractProjectNumber(clientId);
|
||||
if (!projectNumber) continue;
|
||||
if (!projectNumber) {
|
||||
continue;
|
||||
}
|
||||
const res = await runGcloudCommand(
|
||||
[
|
||||
"projects",
|
||||
@@ -301,9 +341,13 @@ export async function resolveProjectIdFromGogCredentials(): Promise<string | nul
|
||||
],
|
||||
30_000,
|
||||
);
|
||||
if (res.code !== 0) continue;
|
||||
if (res.code !== 0) {
|
||||
continue;
|
||||
}
|
||||
const projectId = res.stdout.trim().split(/\s+/)[0];
|
||||
if (projectId) return projectId;
|
||||
if (projectId) {
|
||||
return projectId;
|
||||
}
|
||||
} catch {
|
||||
// keep scanning
|
||||
}
|
||||
@@ -332,7 +376,9 @@ function extractGogClientId(parsed: Record<string, unknown>): string | null {
|
||||
}
|
||||
|
||||
function extractProjectNumber(clientId: string | null): string | null {
|
||||
if (!clientId) return null;
|
||||
if (!clientId) {
|
||||
return null;
|
||||
}
|
||||
const match = clientId.match(/^(\d+)-/);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
@@ -75,12 +75,16 @@ function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess {
|
||||
|
||||
child.stdout?.on("data", (data: Buffer) => {
|
||||
const line = data.toString().trim();
|
||||
if (line) log.info(`[gog] ${line}`);
|
||||
if (line) {
|
||||
log.info(`[gog] ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (data: Buffer) => {
|
||||
const line = data.toString().trim();
|
||||
if (!line) return;
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
if (isAddressInUseError(line)) {
|
||||
addressInUse = true;
|
||||
}
|
||||
@@ -92,7 +96,9 @@ function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess {
|
||||
});
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
if (shuttingDown) return;
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
if (addressInUse) {
|
||||
log.warn(
|
||||
"gog serve failed to bind (address already in use); stopping restarts. " +
|
||||
@@ -104,7 +110,9 @@ function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess {
|
||||
log.warn(`gog exited (code=${code}, signal=${signal}); restarting in 5s`);
|
||||
watcherProcess = null;
|
||||
setTimeout(() => {
|
||||
if (shuttingDown || !currentConfig) return;
|
||||
if (shuttingDown || !currentConfig) {
|
||||
return;
|
||||
}
|
||||
watcherProcess = spawnGogServe(currentConfig);
|
||||
}, 5000);
|
||||
});
|
||||
@@ -180,7 +188,9 @@ export async function startGmailWatcher(cfg: OpenClawConfig): Promise<GmailWatch
|
||||
// Set up renewal interval
|
||||
const renewMs = runtimeConfig.renewEveryMinutes * 60_000;
|
||||
renewInterval = setInterval(() => {
|
||||
if (shuttingDown) return;
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
void startGmailWatch(runtimeConfig);
|
||||
}, renewMs);
|
||||
|
||||
|
||||
@@ -71,7 +71,9 @@ export function mergeHookPresets(existing: string[] | undefined, preset: string)
|
||||
|
||||
export function normalizeHooksPath(raw?: string): string {
|
||||
const base = raw?.trim() || DEFAULT_HOOKS_PATH;
|
||||
if (base === "/") return DEFAULT_HOOKS_PATH;
|
||||
if (base === "/") {
|
||||
return DEFAULT_HOOKS_PATH;
|
||||
}
|
||||
const withSlash = base.startsWith("/") ? base : `/${base}`;
|
||||
return withSlash.replace(/\/+$/, "");
|
||||
}
|
||||
@@ -80,7 +82,9 @@ export function normalizeServePath(raw?: string): string {
|
||||
const base = raw?.trim() || DEFAULT_GMAIL_SERVE_PATH;
|
||||
// Tailscale funnel/serve strips the set-path prefix before proxying.
|
||||
// To accept requests at /<path> externally, gog must listen on "/".
|
||||
if (base === "/") return "/";
|
||||
if (base === "/") {
|
||||
return "/";
|
||||
}
|
||||
const withSlash = base.startsWith("/") ? base : `/${base}`;
|
||||
return withSlash.replace(/\/+$/, "");
|
||||
}
|
||||
@@ -253,7 +257,9 @@ export function buildTopicPath(projectId: string, topicName: string): string {
|
||||
|
||||
export function parseTopicPath(topic: string): { projectId: string; topicName: string } | null {
|
||||
const match = topic.trim().match(/^projects\/([^/]+)\/topics\/([^/]+)$/i);
|
||||
if (!match) return null;
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return { projectId: match[1] ?? "", topicName: match[2] ?? "" };
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,9 @@ describe("hooks install (e2e)", () => {
|
||||
const { installHooksFromPath } = await import("./install.js");
|
||||
const installResult = await installHooksFromPath({ path: packDir });
|
||||
expect(installResult.ok).toBe(true);
|
||||
if (!installResult.ok) return;
|
||||
if (!installResult.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { clearInternalHooks, createInternalHookEvent, triggerInternalHook } =
|
||||
await import("./internal-hooks.js");
|
||||
|
||||
@@ -65,7 +65,9 @@ function resolveHookKey(entry: HookEntry): string {
|
||||
|
||||
function normalizeInstallOptions(entry: HookEntry): HookInstallOption[] {
|
||||
const install = entry.metadata?.install ?? [];
|
||||
if (install.length === 0) return [];
|
||||
if (install.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// For hooks, we just list all install options
|
||||
return install.map((spec, index) => {
|
||||
@@ -115,8 +117,12 @@ function buildHookStatus(
|
||||
const requiredOs = entry.metadata?.os ?? [];
|
||||
|
||||
const missingBins = requiredBins.filter((bin) => {
|
||||
if (hasBinary(bin)) return false;
|
||||
if (eligibility?.remote?.hasBin?.(bin)) return false;
|
||||
if (hasBinary(bin)) {
|
||||
return false;
|
||||
}
|
||||
if (eligibility?.remote?.hasBin?.(bin)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -138,8 +144,12 @@ function buildHookStatus(
|
||||
|
||||
const missingEnv: string[] = [];
|
||||
for (const envName of requiredEnv) {
|
||||
if (process.env[envName]) continue;
|
||||
if (hookConfig?.env?.[envName]) continue;
|
||||
if (process.env[envName]) {
|
||||
continue;
|
||||
}
|
||||
if (hookConfig?.env?.[envName]) {
|
||||
continue;
|
||||
}
|
||||
missingEnv.push(envName);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,9 @@ describe("installHooksFromArchive", () => {
|
||||
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.hookPackId).toBe("zip-hooks");
|
||||
expect(result.hooks).toContain("zip-hook");
|
||||
expect(result.targetDir).toBe(path.join(stateDir, "hooks", "zip-hooks"));
|
||||
@@ -109,7 +111,9 @@ describe("installHooksFromArchive", () => {
|
||||
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.hookPackId).toBe("tar-hooks");
|
||||
expect(result.hooks).toContain("tar-hook");
|
||||
expect(result.targetDir).toBe(path.join(stateDir, "hooks", "tar-hooks"));
|
||||
@@ -142,7 +146,9 @@ describe("installHooksFromPath", () => {
|
||||
const result = await installHooksFromPath({ path: hookDir, hooksDir });
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.hookPackId).toBe("my-hook");
|
||||
expect(result.hooks).toEqual(["my-hook"]);
|
||||
expect(result.targetDir).toBe(path.join(stateDir, "hooks", "my-hook"));
|
||||
|
||||
@@ -39,13 +39,17 @@ const defaultLogger: HookInstallLogger = {};
|
||||
|
||||
function unscopedPackageName(name: string): string {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed.includes("/") ? (trimmed.split("/").pop() ?? trimmed) : trimmed;
|
||||
}
|
||||
|
||||
function safeDirName(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
if (!trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed.replaceAll("/", "__");
|
||||
}
|
||||
|
||||
@@ -349,7 +353,9 @@ export async function installHooksFromNpmSpec(params: {
|
||||
const dryRun = params.dryRun ?? false;
|
||||
const expectedHookPackId = params.expectedHookPackId;
|
||||
const spec = params.spec.trim();
|
||||
if (!spec) return { ok: false, error: "missing npm spec" };
|
||||
if (!spec) {
|
||||
return { ok: false, error: "missing npm spec" };
|
||||
}
|
||||
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hook-pack-"));
|
||||
logger.info?.(`Downloading ${spec}…`);
|
||||
|
||||
@@ -167,9 +167,15 @@ export function createInternalHookEvent(
|
||||
}
|
||||
|
||||
export function isAgentBootstrapEvent(event: InternalHookEvent): event is AgentBootstrapHookEvent {
|
||||
if (event.type !== "agent" || event.action !== "bootstrap") return false;
|
||||
if (event.type !== "agent" || event.action !== "bootstrap") {
|
||||
return false;
|
||||
}
|
||||
const context = event.context as Partial<AgentBootstrapHookContext> | null;
|
||||
if (!context || typeof context !== "object") return false;
|
||||
if (typeof context.workspaceDir !== "string") return false;
|
||||
if (!context || typeof context !== "object") {
|
||||
return false;
|
||||
}
|
||||
if (typeof context.workspaceDir !== "string") {
|
||||
return false;
|
||||
}
|
||||
return Array.isArray(context.bootstrapFiles);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ export type PluginHookLoadResult = {
|
||||
};
|
||||
|
||||
function resolveHookDir(api: OpenClawPluginApi, dir: string): string {
|
||||
if (path.isAbsolute(dir)) return dir;
|
||||
if (path.isAbsolute(dir)) {
|
||||
return dir;
|
||||
}
|
||||
return path.resolve(path.dirname(api.source), dir);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,9 @@ export function resolveSoulEvilConfigFromHook(
|
||||
entry: Record<string, unknown> | undefined,
|
||||
log?: SoulEvilLog,
|
||||
): SoulEvilConfig | null {
|
||||
if (!entry) return null;
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
const file = typeof entry.file === "string" ? entry.file : undefined;
|
||||
if (entry.file !== undefined && !file) {
|
||||
log?.warn?.("soul-evil config: file must be a string");
|
||||
@@ -80,23 +82,33 @@ export function resolveSoulEvilConfigFromHook(
|
||||
log?.warn?.("soul-evil config: purge must be an object");
|
||||
}
|
||||
|
||||
if (!file && chance === undefined && !purge) return null;
|
||||
if (!file && chance === undefined && !purge) {
|
||||
return null;
|
||||
}
|
||||
return { file, chance, purge };
|
||||
}
|
||||
|
||||
function clampChance(value?: number): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(1, Math.max(0, value));
|
||||
}
|
||||
|
||||
function parsePurgeAt(raw?: string): number | null {
|
||||
if (!raw) return null;
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const trimmed = raw.trim();
|
||||
const match = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(trimmed);
|
||||
if (!match) return null;
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const hour = Number.parseInt(match[1] ?? "", 10);
|
||||
const minute = Number.parseInt(match[2] ?? "", 10);
|
||||
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return null;
|
||||
if (!Number.isFinite(hour) || !Number.isFinite(minute)) {
|
||||
return null;
|
||||
}
|
||||
return hour * 60 + minute;
|
||||
}
|
||||
|
||||
@@ -111,9 +123,13 @@ function timeOfDayMsInTimezone(date: Date, timeZone: string): number | null {
|
||||
}).formatToParts(date);
|
||||
const map: Record<string, string> = {};
|
||||
for (const part of parts) {
|
||||
if (part.type !== "literal") map[part.type] = part.value;
|
||||
if (part.type !== "literal") {
|
||||
map[part.type] = part.value;
|
||||
}
|
||||
}
|
||||
if (!map.hour || !map.minute || !map.second) {
|
||||
return null;
|
||||
}
|
||||
if (!map.hour || !map.minute || !map.second) return null;
|
||||
const hour = Number.parseInt(map.hour, 10);
|
||||
const minute = Number.parseInt(map.minute, 10);
|
||||
const second = Number.parseInt(map.second, 10);
|
||||
@@ -132,9 +148,13 @@ function isWithinDailyPurgeWindow(params: {
|
||||
now: Date;
|
||||
timeZone: string;
|
||||
}): boolean {
|
||||
if (!params.at || !params.duration) return false;
|
||||
if (!params.at || !params.duration) {
|
||||
return false;
|
||||
}
|
||||
const startMinutes = parsePurgeAt(params.at);
|
||||
if (startMinutes === null) return false;
|
||||
if (startMinutes === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let durationMs: number;
|
||||
try {
|
||||
@@ -142,13 +162,19 @@ function isWithinDailyPurgeWindow(params: {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (!Number.isFinite(durationMs) || durationMs <= 0) return false;
|
||||
if (!Number.isFinite(durationMs) || durationMs <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dayMs = 24 * 60 * 60 * 1000;
|
||||
if (durationMs >= dayMs) return true;
|
||||
if (durationMs >= dayMs) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const nowMs = timeOfDayMsInTimezone(params.now, params.timeZone);
|
||||
if (nowMs === null) return false;
|
||||
if (nowMs === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const startMs = startMinutes * 60 * 1000;
|
||||
const endMs = startMs + durationMs;
|
||||
@@ -204,7 +230,9 @@ export async function applySoulEvilOverride(params: {
|
||||
now: params.now,
|
||||
random: params.random,
|
||||
});
|
||||
if (!decision.useEvil) return params.files;
|
||||
if (!decision.useEvil) {
|
||||
return params.files;
|
||||
}
|
||||
|
||||
const workspaceDir = resolveUserPath(params.workspaceDir);
|
||||
const evilPath = path.join(workspaceDir, decision.fileName);
|
||||
@@ -235,11 +263,15 @@ export async function applySoulEvilOverride(params: {
|
||||
|
||||
let replaced = false;
|
||||
const updated = params.files.map((file) => {
|
||||
if (file.name !== "SOUL.md") return file;
|
||||
if (file.name !== "SOUL.md") {
|
||||
return file;
|
||||
}
|
||||
replaced = true;
|
||||
return { ...file, content: evilContent, missing: false };
|
||||
});
|
||||
if (!replaced) return params.files;
|
||||
if (!replaced) {
|
||||
return params.files;
|
||||
}
|
||||
|
||||
params.log?.debug?.(
|
||||
`SOUL_EVIL active (${decision.reason ?? "unknown"}) using ${decision.fileName}`,
|
||||
|
||||
@@ -34,7 +34,9 @@ function filterHookEntries(
|
||||
|
||||
function readHookPackageManifest(dir: string): HookPackageManifest | null {
|
||||
const manifestPath = path.join(dir, "package.json");
|
||||
if (!fs.existsSync(manifestPath)) return null;
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const raw = fs.readFileSync(manifestPath, "utf-8");
|
||||
return JSON.parse(raw) as HookPackageManifest;
|
||||
@@ -45,7 +47,9 @@ function readHookPackageManifest(dir: string): HookPackageManifest | null {
|
||||
|
||||
function resolvePackageHooks(manifest: HookPackageManifest): string[] {
|
||||
const raw = manifest[MANIFEST_KEY]?.hooks;
|
||||
if (!Array.isArray(raw)) return [];
|
||||
if (!Array.isArray(raw)) {
|
||||
return [];
|
||||
}
|
||||
return raw.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -56,7 +60,9 @@ function loadHookFromDir(params: {
|
||||
nameHint?: string;
|
||||
}): Hook | null {
|
||||
const hookMdPath = path.join(params.hookDir, "HOOK.md");
|
||||
if (!fs.existsSync(hookMdPath)) return null;
|
||||
if (!fs.existsSync(hookMdPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(hookMdPath, "utf-8");
|
||||
@@ -101,16 +107,22 @@ function loadHookFromDir(params: {
|
||||
function loadHooksFromDir(params: { dir: string; source: HookSource; pluginId?: string }): Hook[] {
|
||||
const { dir, source, pluginId } = params;
|
||||
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
if (!fs.existsSync(dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stat = fs.statSync(dir);
|
||||
if (!stat.isDirectory()) return [];
|
||||
if (!stat.isDirectory()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const hooks: Hook[] = [];
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hookDir = path.join(dir, entry.name);
|
||||
const manifest = readHookPackageManifest(hookDir);
|
||||
@@ -125,7 +137,9 @@ function loadHooksFromDir(params: { dir: string; source: HookSource; pluginId?:
|
||||
pluginId,
|
||||
nameHint: path.basename(resolvedHookDir),
|
||||
});
|
||||
if (hook) hooks.push(hook);
|
||||
if (hook) {
|
||||
hooks.push(hook);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -136,7 +150,9 @@ function loadHooksFromDir(params: { dir: string; source: HookSource; pluginId?:
|
||||
pluginId,
|
||||
nameHint: entry.name,
|
||||
});
|
||||
if (hook) hooks.push(hook);
|
||||
if (hook) {
|
||||
hooks.push(hook);
|
||||
}
|
||||
}
|
||||
|
||||
return hooks;
|
||||
@@ -214,10 +230,18 @@ function loadHookEntries(
|
||||
|
||||
const merged = new Map<string, Hook>();
|
||||
// Precedence: extra < bundled < managed < workspace (workspace wins)
|
||||
for (const hook of extraHooks) merged.set(hook.name, hook);
|
||||
for (const hook of bundledHooks) merged.set(hook.name, hook);
|
||||
for (const hook of managedHooks) merged.set(hook.name, hook);
|
||||
for (const hook of workspaceHooks) merged.set(hook.name, hook);
|
||||
for (const hook of extraHooks) {
|
||||
merged.set(hook.name, hook);
|
||||
}
|
||||
for (const hook of bundledHooks) {
|
||||
merged.set(hook.name, hook);
|
||||
}
|
||||
for (const hook of managedHooks) {
|
||||
merged.set(hook.name, hook);
|
||||
}
|
||||
for (const hook of workspaceHooks) {
|
||||
merged.set(hook.name, hook);
|
||||
}
|
||||
|
||||
return Array.from(merged.values()).map((hook) => {
|
||||
let frontmatter: ParsedHookFrontmatter = {};
|
||||
|
||||
Reference in New Issue
Block a user