diff --git a/index.ts b/index.ts index 99759d3..b0630be 100644 --- a/index.ts +++ b/index.ts @@ -64,7 +64,8 @@ import type { UserConfig } from "./lib/types.js"; * ``` */ export const OpenAIAuthPlugin: Plugin = async ({ client, directory }: PluginInput) => { - configureLogger({ client, directory }); + const pluginConfig = loadPluginConfig(); + configureLogger({ client, directory, pluginConfig }); setTimeout(() => { logWarn( "The OpenAI Codex plugin is intended for personal use with your own ChatGPT Plus/Pro subscription. Ensure your usage complies with OpenAI's Terms of Service.", @@ -98,7 +99,6 @@ export const OpenAIAuthPlugin: Plugin = async ({ client, directory }: PluginInpu }; // Load plugin configuration and determine CODEX_MODE - const pluginConfig = loadPluginConfig(); const codexMode = getCodexMode(pluginConfig); const promptCachingEnabled = pluginConfig.enablePromptCaching ?? true; if (!promptCachingEnabled) { diff --git a/lib/config.ts b/lib/config.ts index e4db55a..3616dd2 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -14,6 +14,9 @@ const DEFAULT_CONFIG: PluginConfig = { enablePromptCaching: true, enableCodexCompaction: true, autoCompactMinMessages: 8, + logging: { + showWarningToasts: false, + }, }; let cachedPluginConfig: PluginConfig | undefined; @@ -39,16 +42,21 @@ export function loadPluginConfig(options: { forceReload?: boolean } = {}): Plugi const fileContent = safeReadFile(CONFIG_PATH); if (!fileContent) { logWarn("Plugin config file not found, using defaults", { path: CONFIG_PATH }); - cachedPluginConfig = DEFAULT_CONFIG; + cachedPluginConfig = { ...DEFAULT_CONFIG }; return cachedPluginConfig; } const userConfig = JSON.parse(fileContent) as Partial; + const userLogging = userConfig.logging ?? {}; - // Merge with defaults + // Merge with defaults (shallow merge + nested logging merge) cachedPluginConfig = { ...DEFAULT_CONFIG, ...userConfig, + logging: { + ...DEFAULT_CONFIG.logging, + ...userLogging, + }, }; return cachedPluginConfig; } catch (error) { @@ -56,7 +64,7 @@ export function loadPluginConfig(options: { forceReload?: boolean } = {}): Plugi path: CONFIG_PATH, error: (error as Error).message, }); - cachedPluginConfig = DEFAULT_CONFIG; + cachedPluginConfig = { ...DEFAULT_CONFIG }; return cachedPluginConfig; } } diff --git a/lib/logger.ts b/lib/logger.ts index 23d525a..4cc6d81 100644 --- a/lib/logger.ts +++ b/lib/logger.ts @@ -1,26 +1,41 @@ import type { OpencodeClient } from "@opencode-ai/sdk"; import { appendFile, rename, rm, stat, writeFile } from "node:fs/promises"; import { join } from "node:path"; +import type { LoggingConfig, PluginConfig } from "./types.js"; import { PLUGIN_NAME } from "./constants.js"; import { ensureDirectory, getOpenCodePath } from "./utils/file-system-utils.js"; -export const LOGGING_ENABLED = process.env.ENABLE_PLUGIN_REQUEST_LOGGING === "1"; -const DEBUG_FLAG_ENABLED = process.env.DEBUG_CODEX_PLUGIN === "1"; -const DEBUG_ENABLED = DEBUG_FLAG_ENABLED || LOGGING_ENABLED; const IS_TEST_ENV = process.env.NODE_ENV === "test"; -const CONSOLE_LOGGING_ENABLED = DEBUG_FLAG_ENABLED && !IS_TEST_ENV; const LOG_DIR = getOpenCodePath("logs", "codex-plugin"); const ROLLING_LOG_FILE = join(LOG_DIR, "codex-plugin.log"); -const LOG_ROTATION_MAX_BYTES = Math.max(1, getEnvNumber("CODEX_LOG_MAX_BYTES", 5 * 1024 * 1024)); -const LOG_ROTATION_MAX_FILES = Math.max(1, getEnvNumber("CODEX_LOG_MAX_FILES", 5)); -const LOG_QUEUE_MAX_LENGTH = Math.max(1, getEnvNumber("CODEX_LOG_QUEUE_MAX", 1000)); +const envLoggingDefaults = { + loggingEnabled: process.env.ENABLE_PLUGIN_REQUEST_LOGGING === "1", + debugFlagEnabled: process.env.DEBUG_CODEX_PLUGIN === "1", + showWarningToasts: process.env.CODEX_SHOW_WARNING_TOASTS === "1", + logRotationMaxBytes: getEnvNumber("CODEX_LOG_MAX_BYTES", 5 * 1024 * 1024), + logRotationMaxFiles: getEnvNumber("CODEX_LOG_MAX_FILES", 5), + logQueueMaxLength: getEnvNumber("CODEX_LOG_QUEUE_MAX", 1000), +}; + +let LOGGING_ENABLED = envLoggingDefaults.loggingEnabled; +export function isLoggingEnabled(): boolean { + return LOGGING_ENABLED; +} +let DEBUG_FLAG_ENABLED = envLoggingDefaults.debugFlagEnabled; +let WARN_TOASTS_ENABLED = envLoggingDefaults.showWarningToasts ?? false; +let LOG_ROTATION_MAX_BYTES = Math.max(1, envLoggingDefaults.logRotationMaxBytes); +let LOG_ROTATION_MAX_FILES = Math.max(1, envLoggingDefaults.logRotationMaxFiles); +let LOG_QUEUE_MAX_LENGTH = Math.max(1, envLoggingDefaults.logQueueMaxLength); +let DEBUG_ENABLED = DEBUG_FLAG_ENABLED || LOGGING_ENABLED; +let CONSOLE_LOGGING_ENABLED = DEBUG_FLAG_ENABLED && !IS_TEST_ENV; type LogLevel = "debug" | "info" | "warn" | "error"; type LoggerOptions = { client?: OpencodeClient; directory?: string; + pluginConfig?: PluginConfig; }; type OpencodeClientWithTui = OpencodeClient & { @@ -46,6 +61,35 @@ type RollingLogEntry = { extra?: Record; }; +function refreshLoggingState(): void { + DEBUG_ENABLED = DEBUG_FLAG_ENABLED || LOGGING_ENABLED; + CONSOLE_LOGGING_ENABLED = DEBUG_FLAG_ENABLED && !IS_TEST_ENV; +} + +function ensurePositiveNumber(value: number | undefined, fallback: number): number { + if (typeof value === "number" && Number.isFinite(value) && value > 0) { + return value; + } + return fallback; +} + +function applyLoggingOverrides(logging?: LoggingConfig): void { + if (!logging) { + refreshLoggingState(); + return; + } + + LOGGING_ENABLED = logging.enableRequestLogging ?? LOGGING_ENABLED; + DEBUG_FLAG_ENABLED = logging.debug ?? DEBUG_FLAG_ENABLED; + WARN_TOASTS_ENABLED = logging.showWarningToasts ?? WARN_TOASTS_ENABLED; + LOG_ROTATION_MAX_BYTES = ensurePositiveNumber(logging.logMaxBytes, LOG_ROTATION_MAX_BYTES); + LOG_ROTATION_MAX_FILES = ensurePositiveNumber(logging.logMaxFiles, LOG_ROTATION_MAX_FILES); + LOG_QUEUE_MAX_LENGTH = ensurePositiveNumber(logging.logQueueMax, LOG_QUEUE_MAX_LENGTH); + refreshLoggingState(); +} + +refreshLoggingState(); + let requestCounter = 0; let loggerClient: OpencodeClient | undefined; let projectDirectory: string | undefined; @@ -66,6 +110,7 @@ export function configureLogger(options: LoggerOptions = {}): void { if (options.directory) { projectDirectory = options.directory; } + applyLoggingOverrides(options.pluginConfig?.logging); if (announcedState || !(LOGGING_ENABLED || DEBUG_ENABLED)) { return; } @@ -127,6 +172,7 @@ export async function flushRollingLogsForTest(): Promise { function emit(level: LogLevel, message: string, extra?: Record): void { const sanitizedExtra = sanitizeExtra(extra); const supportsToast = loggerClient ? hasTuiShowToast(loggerClient) : false; + const warnToastEnabled = supportsToast && WARN_TOASTS_ENABLED; const entry: RollingLogEntry = { timestamp: new Date().toISOString(), service: PLUGIN_NAME, @@ -139,7 +185,7 @@ function emit(level: LogLevel, message: string, extra?: Record) appendRollingLog(entry); } - const shouldForwardToAppLog = level !== "warn" || !supportsToast; + const shouldForwardToAppLog = level !== "warn" || !warnToastEnabled; if (shouldForwardToAppLog && loggerClient?.app?.log) { void loggerClient.app @@ -154,11 +200,11 @@ function emit(level: LogLevel, message: string, extra?: Record) ); } - if (level === "error" || (level === "warn" && supportsToast)) { + if (level === "error" || (level === "warn" && warnToastEnabled)) { notifyToast(level, message, sanitizedExtra); } - const shouldLogToConsole = level !== "warn" || !supportsToast; + const shouldLogToConsole = level !== "warn" || !warnToastEnabled; if (shouldLogToConsole) { logToConsole(level, message, sanitizedExtra); } diff --git a/lib/request/response-handler.ts b/lib/request/response-handler.ts index 3dd6cc2..781e90b 100644 --- a/lib/request/response-handler.ts +++ b/lib/request/response-handler.ts @@ -1,5 +1,5 @@ import { PLUGIN_NAME } from "../constants.js"; -import { LOGGING_ENABLED, logError, logRequest } from "../logger.js"; +import { isLoggingEnabled, logError, logRequest } from "../logger.js"; import type { SSEEventData } from "../types.js"; /** @@ -50,7 +50,7 @@ export async function convertSseToJson(response: Response, headers: Headers): Pr fullText += decoder.decode(value, { stream: true }); } - if (LOGGING_ENABLED) { + if (isLoggingEnabled()) { logRequest("stream-full", { fullContent: fullText }); } diff --git a/lib/types.ts b/lib/types.ts index 0c4439d..c939039 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -32,6 +32,26 @@ export interface PluginConfig { * Minimum number of conversation messages before auto-compacting */ autoCompactMinMessages?: number; + + /** + * Logging configuration that can override environment variables + */ + logging?: LoggingConfig; +} + +export interface LoggingConfig { + /** When true, persist detailed request logs regardless of env var */ + enableRequestLogging?: boolean; + /** When true, enable debug logging regardless of env var */ + debug?: boolean; + /** Whether warning-level toasts should be shown (default: false) */ + showWarningToasts?: boolean; + /** Override max bytes before rolling log rotation */ + logMaxBytes?: number; + /** Override number of rotated log files to keep */ + logMaxFiles?: number; + /** Override rolling log queue length */ + logQueueMax?: number; } /** diff --git a/package-lock.json b/package-lock.json index 2c5dc17..36843d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@openhax/codex", - "version": "0.3.5", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@openhax/codex", - "version": "0.3.5", + "version": "0.4.1", "license": "GPL-3.0-only", "dependencies": { "@openauthjs/openauth": "^0.4.3", diff --git a/spec/environment-variables.md b/spec/environment-variables.md new file mode 100644 index 0000000..d662158 --- /dev/null +++ b/spec/environment-variables.md @@ -0,0 +1,43 @@ +# Environment Variables Audit + +## Context + +- User requested a list/summary of all environment variables used by this repository. + +## Sources (code refs) + +- lib/logger.ts:12-27,428-435 (logging flags, rotation tuning via env, generic accessor) +- lib/config.ts:72-76 (CODEX_MODE override) +- scripts/sync-github-secrets.mjs:5-10,63-65,106-114 (default secret names, repo detection, env lookup) +- scripts/review-response-context.mjs:7-10,104-110 (GitHub Actions paths/outputs) +- scripts/detect-release-type.mjs:18-21,30-31 (release base ref/head sha overrides) + +## Environment variables and purposes + +- `ENABLE_PLUGIN_REQUEST_LOGGING` (`lib/logger.ts:7`): when "1", persist detailed request logs. +- `DEBUG_CODEX_PLUGIN` (`lib/logger.ts:8`): when "1", enable debug logging/console output (unless NODE_ENV=test). +- `NODE_ENV` (`lib/logger.ts:10`): if "test", suppress console logging during tests. +- `CODEX_LOG_MAX_BYTES` (`lib/logger.ts:16`): max rolling log file size before rotation (default 5MB). +- `CODEX_LOG_MAX_FILES` (`lib/logger.ts:17`): how many rotated log files to keep (default 5). +- `CODEX_LOG_QUEUE_MAX` (`lib/logger.ts:18`): max queued log entries before overflow warning (default 1000). +- `CODEX_SHOW_WARNING_TOASTS` (`lib/logger.ts:15`): when "1", allow warning-level toasts (config default keeps them off). +- `CODEX_MODE` (`lib/config.ts:72-76`): if set, overrides config; "1" enables Codex bridge/tool mapping, otherwise disables. +- `GITHUB_REPOSITORY` (`scripts/sync-github-secrets.mjs:63-65`): optional repo inference fallback for syncing secrets. +- `NPM_TOKEN`, `OPENCODE_API_KEY`, `OPENCODE_API_URL`, `RELEASE_BASE_REF` (`scripts/sync-github-secrets.mjs:5-10`): default env names expected when syncing secrets (first two required, latter two optional unless explicitly requested). +- `GITHUB_EVENT_PATH` (`scripts/review-response-context.mjs:7-10`): required path to event payload in review-comment workflow. +- `GITHUB_OUTPUT` (`scripts/review-response-context.mjs:104-110`): optional path to append action outputs. +- `RELEASE_BASE_REF` (`scripts/detect-release-type.mjs:18-21`): optional override for release comparison base. +- `GITHUB_SHA` (`scripts/detect-release-type.mjs:30-31`): optional head sha override (falls back to git rev-parse HEAD). + +## Existing issues/PRs + +- None identified during this audit. + +## Definition of done + +- Enumerate all environment variables referenced in code/scripts with locations and purposes; provide user-facing summary. + +## Notes + +- Secret sync script can read any env name specified via CLI args in addition to defaults; above list reflects defaults plus repo inference variables. +- Logging-related env vars can be overridden in ~/.opencode/openhax-codex-config.json via the `logging` block. diff --git a/test/README.md b/test/README.md index 6adddfb..365a388 100644 --- a/test/README.md +++ b/test/README.md @@ -79,7 +79,7 @@ Tests SSE to JSON conversion: Tests logging functionality: -- LOGGING_ENABLED constant +- isLoggingEnabled() function - logRequest function parameter handling - Complex data structure support diff --git a/test/cache-warming.test.ts b/test/cache-warming.test.ts index 04852f3..02870d1 100644 --- a/test/cache-warming.test.ts +++ b/test/cache-warming.test.ts @@ -20,7 +20,7 @@ vi.mock("../lib/logger.js", () => ({ logDebug: vi.fn(), logWarn: vi.fn(), logRequest: vi.fn(), - LOGGING_ENABLED: false, + isLoggingEnabled: () => false, })); const mockGetCodexInstructions = getCodexInstructions as ReturnType; diff --git a/test/logger.test.ts b/test/logger.test.ts index ff0c863..9929631 100644 --- a/test/logger.test.ts +++ b/test/logger.test.ts @@ -65,10 +65,14 @@ afterEach(() => { }); describe("logger", () => { - it("LOGGING_ENABLED reflects env state", async () => { + it("isLoggingEnabled reflects env state", async () => { process.env.ENABLE_PLUGIN_REQUEST_LOGGING = "1"; - const { LOGGING_ENABLED } = await import("../lib/logger.js"); - expect(LOGGING_ENABLED).toBe(true); + const { isLoggingEnabled, configureLogger } = await import("../lib/logger.js"); + expect(isLoggingEnabled()).toBe(true); + + // Test that config overrides are reflected + configureLogger({ pluginConfig: { logging: { enableRequestLogging: false } } }); + expect(isLoggingEnabled()).toBe(false); }); it("logRequest writes stage file and rolling log when enabled", async () => { @@ -107,6 +111,25 @@ describe("logger", () => { expect(fsMocks.appendFile).not.toHaveBeenCalled(); }); + it("config overrides env-enabled request logging when disabled in file", async () => { + process.env.ENABLE_PLUGIN_REQUEST_LOGGING = "1"; + fsMocks.existsSync.mockReturnValue(true); + const { configureLogger, logRequest, flushRollingLogsForTest, isLoggingEnabled } = await import( + "../lib/logger.js" + ); + + configureLogger({ pluginConfig: { logging: { enableRequestLogging: false } } }); + + // Verify isLoggingEnabled reflects the config override + expect(isLoggingEnabled()).toBe(false); + + logRequest("stage-one", { foo: "bar" }); + await flushRollingLogsForTest(); + + expect(fsMocks.writeFile).not.toHaveBeenCalled(); + expect(fsMocks.appendFile).not.toHaveBeenCalled(); + }); + it("logDebug appends to rolling log only when enabled", async () => { process.env.ENABLE_PLUGIN_REQUEST_LOGGING = "1"; fsMocks.existsSync.mockReturnValue(true); @@ -129,7 +152,7 @@ describe("logger", () => { expect(warnSpy).toHaveBeenCalledWith("[openhax/codex] warning"); }); - it("logWarn sends toast and avoids console/app log when tui available", async () => { + it("logWarn does not send warning toasts by default even when tui is available", async () => { fsMocks.existsSync.mockReturnValue(true); const showToast = vi.fn(); const appLog = vi.fn().mockResolvedValue(undefined); @@ -145,6 +168,27 @@ describe("logger", () => { logWarn("toast-warning"); await flushRollingLogsForTest(); + expect(showToast).not.toHaveBeenCalled(); + expect(appLog).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith("[openhax/codex] toast-warning"); + }); + + it("logWarn sends warning toasts only when enabled via config", async () => { + fsMocks.existsSync.mockReturnValue(true); + const showToast = vi.fn(); + const appLog = vi.fn().mockResolvedValue(undefined); + const { configureLogger, logWarn, flushRollingLogsForTest } = await import("../lib/logger.js"); + + const client = { + app: { log: appLog }, + tui: { showToast }, + } as unknown as OpencodeClient; + + configureLogger({ client, pluginConfig: { logging: { showWarningToasts: true } } }); + + logWarn("toast-warning"); + await flushRollingLogsForTest(); + expect(showToast).toHaveBeenCalledWith({ body: { title: "openhax/codex warning", @@ -167,7 +211,7 @@ describe("logger", () => { tui: { showToast }, } as unknown as OpencodeClient; - configureLogger({ client }); + configureLogger({ client, pluginConfig: { logging: { showWarningToasts: true } } }); logWarn( "prefix mismatch detected while warming the session cache; reconnecting with fallback account boundaries", diff --git a/test/plugin-config.test.ts b/test/plugin-config.test.ts index 2ffa5e1..6a30af5 100644 --- a/test/plugin-config.test.ts +++ b/test/plugin-config.test.ts @@ -54,7 +54,9 @@ describe("Plugin Configuration", () => { enablePromptCaching: true, enableCodexCompaction: true, autoCompactMinMessages: 8, + logging: { showWarningToasts: false }, }); + expect(mockExistsSync).toHaveBeenCalledWith( path.join(os.homedir(), ".opencode", "openhax-codex-config.json"), ); @@ -71,6 +73,7 @@ describe("Plugin Configuration", () => { enablePromptCaching: true, enableCodexCompaction: true, autoCompactMinMessages: 8, + logging: { showWarningToasts: false }, }); }); @@ -85,6 +88,22 @@ describe("Plugin Configuration", () => { enablePromptCaching: true, enableCodexCompaction: true, autoCompactMinMessages: 8, + logging: { showWarningToasts: false }, + }); + }); + + it("should merge nested logging config with defaults", () => { + mockExistsSync.mockReturnValue(true); + mockReadFileSync.mockReturnValue( + JSON.stringify({ logging: { enableRequestLogging: false, logMaxFiles: 2 } }), + ); + + const config = loadPluginConfig({ forceReload: true }); + + expect(config.logging).toEqual({ + enableRequestLogging: false, + logMaxFiles: 2, + showWarningToasts: false, }); }); @@ -100,6 +119,7 @@ describe("Plugin Configuration", () => { enablePromptCaching: true, enableCodexCompaction: true, autoCompactMinMessages: 8, + logging: { showWarningToasts: false }, }); expect(logWarnSpy).toHaveBeenCalled(); logWarnSpy.mockRestore(); @@ -119,6 +139,7 @@ describe("Plugin Configuration", () => { enablePromptCaching: true, enableCodexCompaction: true, autoCompactMinMessages: 8, + logging: { showWarningToasts: false }, }); expect(logWarnSpy).toHaveBeenCalled(); logWarnSpy.mockRestore();