diff --git a/_locales/en-US/messages.json b/_locales/en-US/messages.json index 54882db..591373d 100644 --- a/_locales/en-US/messages.json +++ b/_locales/en-US/messages.json @@ -13,5 +13,6 @@ "template.qwen": { "message": "Qwen" }, "template.mistral": { "message": "Mistral" }, "template.custom": { "message": "Custom" }, - "options.save": { "message": "Save" } + "options.save": { "message": "Save" }, + "options.debugLogging": { "message": "Enable debug logging" } } diff --git a/background.js b/background.js index 1e630c5..7ae1e8a 100644 --- a/background.js +++ b/background.js @@ -10,56 +10,59 @@ "use strict"; +let logger; // Startup -console.log("[ai-filter] background.js loaded – ready to classify"); (async () => { + logger = await import(browser.runtime.getURL("logger.js")); + logger.aiLog("background.js loaded – ready to classify", {debug: true}); try { - const store = await browser.storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams"]); + const store = await browser.storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging"]); + logger.setDebug(store.debugLogging); await browser.aiFilter.initConfig(store); - console.log("[ai-filter] configuration loaded", store); + logger.aiLog("configuration loaded", {debug: true}, store); try { await browser.DomContentScript.registerWindow( "chrome://messenger/content/FilterEditor.xhtml", "resource://aifilter/content/filterEditor.js" ); - console.log("[ai-filter] registered FilterEditor content script"); + logger.aiLog("registered FilterEditor content script", {debug: true}); } catch (e) { - console.error("[ai-filter] failed to register content script", e); + logger.aiLog("failed to register content script", {level: 'error'}, e); } } catch (err) { - console.error("[ai-filter] failed to load config:", err); + logger.aiLog("failed to load config", {level: 'error'}, err); } })(); // Listen for messages from UI/devtools browser.runtime.onMessage.addListener((msg) => { - console.log("[ai-filter] onMessage received:", msg); + logger.aiLog("onMessage received", {debug: true}, msg); if (msg?.type === "aiFilter:test") { const { text = "", criterion = "" } = msg; - console.log("[ai-filter] aiFilter:test – text:", text); - console.log("[ai-filter] aiFilter:test – criterion:", criterion); + logger.aiLog("aiFilter:test – text", {debug: true}, text); + logger.aiLog("aiFilter:test – criterion", {debug: true}, criterion); try { - console.log("[ai-filter] Calling browser.aiFilter.classify()"); + logger.aiLog("Calling browser.aiFilter.classify()", {debug: true}); const result = browser.aiFilter.classify(text, criterion); - console.log("[ai-filter] classify() returned:", result); + logger.aiLog("classify() returned", {debug: true}, result); return { match: result }; } catch (err) { - console.error("[ai-filter] Error in classify():", err); + logger.aiLog("Error in classify()", {level: 'error'}, err); // rethrow so the caller sees the failure throw err; } } else { - console.warn("[ai-filter] Unknown message type, ignoring:", msg?.type); + logger.aiLog("Unknown message type, ignoring", {level: 'warn'}, msg?.type); } }); // Catch any unhandled rejections window.addEventListener("unhandledrejection", ev => { - console.error("[ai-filter] Unhandled promise rejection:", ev.reason); + logger.aiLog("Unhandled promise rejection", {level: 'error'}, ev.reason); }); browser.runtime.onInstalled.addListener(async ({ reason }) => { diff --git a/experiment/api.js b/experiment/api.js index eb056cf..bee7c0c 100644 --- a/experiment/api.js +++ b/experiment/api.js @@ -1,20 +1,22 @@ var { ExtensionCommon } = ChromeUtils.importESModule("resource://gre/modules/ExtensionCommon.sys.mjs"); var { Services } = globalThis || ChromeUtils.importESModule("resource://gre/modules/Services.sys.mjs"); var { MailServices } = ChromeUtils.importESModule("resource:///modules/MailServices.sys.mjs"); +var aiLog = (...args) => console.log("[ai-filter][api]", ...args); +var setDebug = () => {}; -console.log("[ai-filter][api] Experiment API module loaded"); +console.log("[ai-filter][api] Experiment API module loading"); var resProto = Cc["@mozilla.org/network/protocol;1?name=resource"] .getService(Ci.nsISubstitutingProtocolHandler); function registerResourceUrl(extension, namespace) { - console.log(`[ai-filter][api] registerResourceUrl called for namespace="${namespace}"`); + aiLog(`[api] registerResourceUrl called for namespace="${namespace}"`, {debug: true}); if (resProto.hasSubstitution(namespace)) { - console.log(`[ai-filter][api] namespace="${namespace}" already registered, skipping`); + aiLog(`[api] namespace="${namespace}" already registered, skipping`, {debug: true}); return; } let uri = Services.io.newURI(".", null, extension.rootURI); - console.log(`[ai-filter][api] setting substitution for "${namespace}" → ${uri.spec}`); + aiLog(`[api] setting substitution for "${namespace}" → ${uri.spec}`, {debug: true}); resProto.setSubstitutionWithFlags(namespace, uri, resProto.ALLOW_CONTENT_ACCESS); } @@ -23,63 +25,71 @@ var AIFilterMod; var aiFilter = class extends ExtensionCommon.ExtensionAPI { async onStartup() { - console.log("[ai-filter][api] onStartup()"); let { extension } = this; + // Import logger after we have access to the extension root + let loggerMod = ChromeUtils.import(extension.rootURI.resolve("modules/logger.jsm")); + aiLog = loggerMod.aiLog; + setDebug = loggerMod.setDebug; + aiLog("[api] onStartup()", {debug: true}); + registerResourceUrl(extension, "aifilter"); try { - console.log("[ai-filter][api] importing ExpressionSearchFilter.jsm"); + aiLog("[api] importing ExpressionSearchFilter.jsm", {debug: true}); AIFilterMod = ChromeUtils.import("resource://aifilter/modules/ExpressionSearchFilter.jsm"); - console.log("[ai-filter][api] ExpressionSearchFilter.jsm import succeeded"); + aiLog("[api] ExpressionSearchFilter.jsm import succeeded", {debug: true}); } catch (err) { - console.error("[ai-filter][api] failed to import ExpressionSearchFilter.jsm:", err); + aiLog("[api] failed to import ExpressionSearchFilter.jsm", {level: 'error'}, err); } } onShutdown(isAppShutdown) { - console.log("[ai-filter][api] onShutdown(), isAppShutdown =", isAppShutdown); + aiLog("[api] onShutdown()", {debug: true}, isAppShutdown); if (!isAppShutdown && resProto.hasSubstitution("aifilter")) { - console.log("[ai-filter][api] removing substitution for namespace='aifilter'"); + aiLog("[api] removing substitution for namespace='aifilter'", {debug: true}); resProto.setSubstitution("aifilter", null); } } getAPI(context) { - console.log("[ai-filter][api] getAPI()"); + aiLog("[api] getAPI()", {debug: true}); return { aiFilter: { initConfig: async (config) => { try { if (AIFilterMod?.AIFilter?.setConfig) { AIFilterMod.AIFilter.setConfig(config); - console.log("[ai-filter][api] configuration applied", config); + if (typeof config.debugLogging === "boolean") { + setDebug(config.debugLogging); + } + aiLog("[api] configuration applied", {debug: true}, config); } } catch (err) { - console.error("[ai-filter][api] failed to apply config:", err); + aiLog("[api] failed to apply config", {level: 'error'}, err); } }, classify: (msg) => { - console.log("[ai-filter][api] classify() called with msg:", msg); + aiLog("[api] classify() called with msg", {debug: true}, msg); try { if (!gTerm) { - console.log("[ai-filter][api] instantiating new ClassificationTerm"); + aiLog("[api] instantiating new ClassificationTerm", {debug: true}); let mod = AIFilterMod || ChromeUtils.import("resource://aifilter/modules/ExpressionSearchFilter.jsm"); gTerm = new mod.ClassificationTerm(); } - console.log("[ai-filter][api] calling gTerm.match()"); + aiLog("[api] calling gTerm.match()", {debug: true}); let matchResult = gTerm.match( msg.msgHdr, msg.value, Ci.nsMsgSearchOp.Contains ); - console.log("[ai-filter][api] gTerm.match() returned:", matchResult); + aiLog("[api] gTerm.match() returned", {debug: true}, matchResult); return matchResult; } catch (err) { - console.error("[ai-filter][api] error in classify():", err); + aiLog("[api] error in classify()", {level: 'error'}, err); throw err; } } diff --git a/logger.js b/logger.js new file mode 100644 index 0000000..db90c0b --- /dev/null +++ b/logger.js @@ -0,0 +1,24 @@ +let debugEnabled = false; +export function setDebug(value) { + debugEnabled = !!value; +} + +function getCaller() { + try { + const stack = new Error().stack.split("\n"); + if (stack.length >= 3) { + return stack[2].trim().replace(/^at\s+/, ''); + } + } catch (e) {} + return ''; +} + +export function aiLog(message, opts = {}, ...args) { + const { level = 'log', debug = false } = opts; + if (debug && !debugEnabled) { + return; + } + const caller = getCaller(); + const prefix = caller ? `[ai-filter][${caller}]` : '[ai-filter]'; + console[level](`%c${prefix}`, 'color:#1c92d2;font-weight:bold', message, ...args); +} diff --git a/modules/ExpressionSearchFilter.jsm b/modules/ExpressionSearchFilter.jsm index 229e185..d543b1b 100644 --- a/modules/ExpressionSearchFilter.jsm +++ b/modules/ExpressionSearchFilter.jsm @@ -5,6 +5,7 @@ var { Services } = globalThis || ChromeUtils.importESModule("resource://g var { NetUtil } = ChromeUtils.importESModule("resource://gre/modules/NetUtil.sys.mjs"); var { MimeParser } = ChromeUtils.importESModule("resource:///modules/mimeParser.sys.mjs"); var { FileUtils } = ChromeUtils.importESModule("resource://gre/modules/FileUtils.sys.mjs"); +var { aiLog, setDebug } = ChromeUtils.import("resource://aifilter/modules/logger.jsm"); function sha256Hex(str) { const hasher = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); @@ -41,36 +42,36 @@ class CustomerTermBase { this._cacheFile.append("aifilter_cache.json"); this._loadCache(); - console.log(`[ai-filter][ExpressionSearchFilter] Initialized term base "${this.id}"`); + aiLog(`[ExpressionSearchFilter] Initialized term base "${this.id}"`, {debug: true}); } _loadCache() { - console.log(`[ai-filter][ExpressionSearchFilter] Loading cache from ${this._cacheFile.path}`); + aiLog(`[ExpressionSearchFilter] Loading cache from ${this._cacheFile.path}` , {debug: true}); try { if (this._cacheFile.exists()) { let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); stream.init(this._cacheFile, -1, 0, 0); let data = NetUtil.readInputStreamToString(stream, stream.available()); stream.close(); - console.log(`[ai-filter][ExpressionSearchFilter] Cache file contents: ${data}`); + aiLog(`[ExpressionSearchFilter] Cache file contents: ${data}`, {debug: true}); let obj = JSON.parse(data); for (let [k, v] of Object.entries(obj)) { - console.log(`[ai-filter][ExpressionSearchFilter] ⮡ Loaded entry '${k}' → ${v}`); + aiLog(`[ExpressionSearchFilter] ⮡ Loaded entry '${k}' → ${v}`, {debug: true}); this.cache.set(k, v); } - console.log(`[ai-filter][ExpressionSearchFilter] Loaded ${this.cache.size} cache entries`); + aiLog(`[ExpressionSearchFilter] Loaded ${this.cache.size} cache entries`, {debug: true}); } else { - console.log(`[ai-filter][ExpressionSearchFilter] Cache file does not exist`); + aiLog(`[ExpressionSearchFilter] Cache file does not exist`, {debug: true}); } } catch (e) { - console.error(`[ai-filter][ExpressionSearchFilter] Failed to load cache`, e); + aiLog(`Failed to load cache`, {level: 'error'}, e); } } _saveCache(updatedKey, updatedValue) { - console.log(`[ai-filter][ExpressionSearchFilter] Saving cache to ${this._cacheFile.path}`); + aiLog(`[ExpressionSearchFilter] Saving cache to ${this._cacheFile.path}`, {debug: true}); if (typeof updatedKey !== "undefined") { - console.log(`[ai-filter][ExpressionSearchFilter] ⮡ Persisting entry '${updatedKey}' → ${updatedValue}`); + aiLog(`[ExpressionSearchFilter] ⮡ Persisting entry '${updatedKey}' → ${updatedValue}`, {debug: true}); } try { let obj = Object.fromEntries(this.cache); @@ -83,39 +84,39 @@ class CustomerTermBase { stream.write(data, data.length); stream.close(); } catch (e) { - console.error(`[ai-filter][ExpressionSearchFilter] Failed to save cache`, e); + aiLog(`Failed to save cache`, {level: 'error'}, e); } } getEnabled() { - console.log(`[ai-filter][ExpressionSearchFilter] getEnabled() called on "${this.id}"`); + aiLog(`[ExpressionSearchFilter] getEnabled() called on "${this.id}"`, {debug: true}); return true; } getAvailable() { - console.log(`[ai-filter][ExpressionSearchFilter] getAvailable() called on "${this.id}"`); + aiLog(`[ExpressionSearchFilter] getAvailable() called on "${this.id}"`, {debug: true}); return true; } getAvailableOperators() { - console.log(`[ai-filter][ExpressionSearchFilter] getAvailableOperators() called on "${this.id}"`); + aiLog(`[ExpressionSearchFilter] getAvailableOperators() called on "${this.id}"`, {debug: true}); return this.operators; } getAvailableValues() { - console.log(`[ai-filter][ExpressionSearchFilter] getAvailableValues() called on "${this.id}"`); + aiLog(`[ExpressionSearchFilter] getAvailableValues() called on "${this.id}"`, {debug: true}); return null; } get attrib() { - console.log(`[ai-filter][ExpressionSearchFilter] attrib getter called for "${this.id}"`); + aiLog(`[ExpressionSearchFilter] attrib getter called for "${this.id}"`, {debug: true}); //return Ci.nsMsgSearchAttrib.Custom; } } function getPlainText(msgHdr) { - console.log(`[ai-filter][ExpressionSearchFilter] Extracting plain text for message ID ${msgHdr.messageId}`); + aiLog(`[ExpressionSearchFilter] Extracting plain text for message ID ${msgHdr.messageId}`, {debug: true}); let folder = msgHdr.folder; if (!folder.getMsgInputStream) return ""; let reusable = {}; @@ -161,7 +162,7 @@ function loadTemplate(name) { return xhr.responseText; } } catch (e) { - console.error(`[ai-filter][ExpressionSearchFilter] Failed to load template '${name}':`, e); + aiLog(`Failed to load template '${name}':`, {level: 'error'}, e); } return ""; } @@ -186,9 +187,12 @@ function setConfig(config = {}) { } } } + if (typeof config.debugLogging === "boolean") { + setDebug(config.debugLogging); + } gTemplateText = gTemplateName === "custom" ? gCustomTemplate : loadTemplate(gTemplateName); - console.log(`[ai-filter][ExpressionSearchFilter] Endpoint set to ${gEndpoint}`); - console.log(`[ai-filter][ExpressionSearchFilter] Template set to ${gTemplateName}`); + aiLog(`[ExpressionSearchFilter] Endpoint set to ${gEndpoint}`, {debug: true}); + aiLog(`[ExpressionSearchFilter] Template set to ${gTemplateName}`, {debug: true}); } function buildSystemPrompt() { @@ -196,7 +200,7 @@ function buildSystemPrompt() { } function buildPrompt(body, criterion) { - console.log(`[ai-filter][ExpressionSearchFilter] Building prompt with criterion: "${criterion}"`); + aiLog(`[ExpressionSearchFilter] Building prompt with criterion: "${criterion}"`, {debug: true}); const data = { system: buildSystemPrompt(), email: body, @@ -209,7 +213,7 @@ function buildPrompt(body, criterion) { class ClassificationTerm extends CustomerTermBase { constructor() { super("classification", [Ci.nsMsgSearchOp.Matches, Ci.nsMsgSearchOp.DoesntMatch]); - console.log(`[ai-filter][ExpressionSearchFilter] ClassificationTerm constructed`); + aiLog(`[ExpressionSearchFilter] ClassificationTerm constructed`, {debug: true}); } needsBody() { return true; } @@ -217,11 +221,11 @@ class ClassificationTerm extends CustomerTermBase { match(msgHdr, value, op) { const opName = op === Ci.nsMsgSearchOp.Matches ? "matches" : op === Ci.nsMsgSearchOp.DoesntMatch ? "doesn't match" : `unknown (${op})`; - console.log(`[ai-filter][ExpressionSearchFilter] Matching message ${msgHdr.messageId} using op "${opName}" and value "${value}"`); + aiLog(`[ExpressionSearchFilter] Matching message ${msgHdr.messageId} using op "${opName}" and value "${value}"`, {debug: true}); let key = [msgHdr.messageId, op, value].map(sha256Hex).join("|"); if (this.cache.has(key)) { - console.log(`[ai-filter][ExpressionSearchFilter] Cache hit for key: ${key}`); + aiLog(`[ExpressionSearchFilter] Cache hit for key: ${key}`, {debug: true}); return this.cache.get(key); } @@ -232,7 +236,7 @@ class ClassificationTerm extends CustomerTermBase { let payload = JSON.stringify(payloadObj); - console.log(`[ai-filter][ExpressionSearchFilter] Sending classification request to ${gEndpoint}`); + aiLog(`[ExpressionSearchFilter] Sending classification request to ${gEndpoint}`, {debug: true}); let matched = false; try { @@ -242,44 +246,44 @@ class ClassificationTerm extends CustomerTermBase { xhr.send(payload); if (xhr.status < 200 || xhr.status >= 300) { - console.warn(`[ai-filter][ExpressionSearchFilter] HTTP status ${xhr.status}`); + aiLog(`HTTP status ${xhr.status}`, {level: 'warn'}); } else { const result = JSON.parse(xhr.responseText); - console.log(`[ai-filter][ExpressionSearchFilter] Received response:`, result); + aiLog(`[ExpressionSearchFilter] Received response:`, {debug: true}, result); const rawText = result.choices?.[0]?.text || ""; const thinkText = rawText.match(/[\s\S]*?<\/think>/gi)?.join('') || ''; - console.log('[ai-filter][ExpressionSearchFilter] ⮡ Reasoning: ', thinkText); + aiLog('[ExpressionSearchFilter] ⮡ Reasoning:', {debug: true}, thinkText); const cleanedText = rawText.replace(/[\s\S]*?<\/think>/gi, "").trim(); - console.log('[ai-filter][ExpressionSearchFilter] ⮡ Cleaned Response Text: ', cleanedText); + aiLog('[ExpressionSearchFilter] ⮡ Cleaned Response Text:', {debug: true}, cleanedText); const obj = JSON.parse(cleanedText); matched = obj.matched === true || obj.match === true; - console.log(`[ai-filter][ExpressionSearchFilter] Caching entry '${key}' → ${matched}`); + aiLog(`[ExpressionSearchFilter] Caching entry '${key}' → ${matched}`, {debug: true}); this.cache.set(key, matched); this._saveCache(key, matched); } } catch (e) { - console.error(`[ai-filter][ExpressionSearchFilter] HTTP request failed:`, e); + aiLog(`HTTP request failed`, {level: 'error'}, e); } if (op === Ci.nsMsgSearchOp.DoesntMatch) { matched = !matched; - console.log(`[ai-filter][ExpressionSearchFilter] Operator is "doesn't match" → inverting to ${matched}`); + aiLog(`[ExpressionSearchFilter] Operator is "doesn't match" → inverting to ${matched}`, {debug: true}); } - console.log(`[ai-filter][ExpressionSearchFilter] Final match result: ${matched}`); + aiLog(`[ExpressionSearchFilter] Final match result: ${matched}`, {debug: true}); return matched; } } (function register() { - console.log(`[ai-filter][ExpressionSearchFilter] Registering custom filter term...`); + aiLog(`[ExpressionSearchFilter] Registering custom filter term...`, {debug: true}); let term = new ClassificationTerm(); if (!MailServices.filters.getCustomTerm(term.id)) { MailServices.filters.addCustomTerm(term); - console.log(`[ai-filter][ExpressionSearchFilter] Registered term: ${term.id}`); + aiLog(`[ExpressionSearchFilter] Registered term: ${term.id}`, {debug: true}); } else { - console.log(`[ai-filter][ExpressionSearchFilter] Term already registered: ${term.id}`); + aiLog(`[ExpressionSearchFilter] Term already registered: ${term.id}`, {debug: true}); } })(); diff --git a/modules/logger.jsm b/modules/logger.jsm new file mode 100644 index 0000000..7c64176 --- /dev/null +++ b/modules/logger.jsm @@ -0,0 +1,26 @@ +var EXPORTED_SYMBOLS = ['aiLog', 'setDebug']; +let debugEnabled = false; + +function setDebug(value) { + debugEnabled = !!value; +} + +function getCaller() { + try { + let stack = new Error().stack.split('\n'); + if (stack.length >= 3) { + return stack[2].trim().replace(/^@?\s*\(?/,'').replace(/^at\s+/, ''); + } + } catch (e) {} + return ''; +} + +function aiLog(message, opts = {}, ...args) { + const { level = 'log', debug = false } = opts; + if (debug && !debugEnabled) { + return; + } + const caller = getCaller(); + const prefix = caller ? `[ai-filter][${caller}]` : '[ai-filter]'; + console[level](`%c${prefix}`, 'color:#1c92d2;font-weight:bold', message, ...args); +} diff --git a/options/options.html b/options/options.html index a1a2fc5..b758309 100644 --- a/options/options.html +++ b/options/options.html @@ -147,6 +147,11 @@