diff --git a/package.json b/package.json index 9be5b41..047e6cc 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,8 @@ "marked": "^15.0.12", "marked-terminal": "^7.3.0", "nodemailer": "^8.0.5", + "pino": "^10.3.1", + "pino-pretty": "^13.1.3", "playwright": "^1.50.0", "postgres": "^3.4.7", "react": "^19.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aa8589..b676a54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,12 @@ importers: nodemailer: specifier: ^8.0.5 version: 8.0.5 + pino: + specifier: ^10.3.1 + version: 10.3.1 + pino-pretty: + specifier: ^13.1.3 + version: 13.1.3 playwright: specifier: ^1.50.0 version: 1.58.2 @@ -1615,6 +1621,9 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1816,9 +1825,15 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-copy@4.0.3: + resolution: {integrity: sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -1974,6 +1989,9 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -2106,6 +2124,10 @@ packages: jose@6.2.2: resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -2496,6 +2518,10 @@ packages: pino-abstract-transport@3.0.0: resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + pino-pretty@13.1.3: + resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + hasBin: true + pino-std-serializers@7.1.0: resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} @@ -2688,6 +2714,9 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} @@ -2827,6 +2856,10 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} @@ -4428,6 +4461,8 @@ snapshots: data-uri-to-buffer@4.0.1: {} + dateformat@4.6.3: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -4643,8 +4678,12 @@ snapshots: extend@3.0.2: {} + fast-copy@4.0.3: {} + fast-deep-equal@3.1.3: {} + fast-safe-stringify@2.1.1: {} + fast-uri@3.1.0: {} fdir@6.5.0(picomatch@4.0.3): @@ -4820,6 +4859,8 @@ snapshots: he@1.2.0: {} + help-me@5.0.0: {} + highlight.js@10.7.3: {} hono@4.12.14: {} @@ -4967,6 +5008,8 @@ snapshots: jose@6.2.2: {} + joycon@3.1.1: {} + jsesc@3.1.0: {} json-bigint@1.0.0: @@ -5365,6 +5408,22 @@ snapshots: dependencies: split2: 4.2.0 + pino-pretty@13.1.3: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 4.0.3 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pump: 3.0.3 + secure-json-parse: 4.1.0 + sonic-boom: 4.2.1 + strip-json-comments: 5.0.3 + pino-std-serializers@7.1.0: {} pino@10.3.1: @@ -5630,6 +5689,8 @@ snapshots: scheduler@0.27.0: {} + secure-json-parse@4.1.0: {} + selderee@0.11.0: dependencies: parseley: 0.12.1 @@ -5815,6 +5876,8 @@ snapshots: strip-json-comments@2.0.1: {} + strip-json-comments@5.0.3: {} + strtok3@10.3.4: dependencies: '@tokenizer/token': 0.3.0 diff --git a/settings/src/app/api/admin/wiki/route.ts b/settings/src/app/api/admin/wiki/route.ts index 44621e4..4fba8cf 100644 --- a/settings/src/app/api/admin/wiki/route.ts +++ b/settings/src/app/api/admin/wiki/route.ts @@ -19,8 +19,6 @@ export async function POST() { try { // Trigger wiki compilation via daemon gRPC command const { exec } = await import("node:child_process"); - const { promisify } = await import("node:util"); - const execAsync = promisify(exec); // Use the notify-daemon pattern to trigger compilation const path = await import("node:path"); diff --git a/settings/src/app/api/google/oauth/start/route.ts b/settings/src/app/api/google/oauth/start/route.ts index 16b3df8..803bc08 100644 --- a/settings/src/app/api/google/oauth/start/route.ts +++ b/settings/src/app/api/google/oauth/start/route.ts @@ -27,18 +27,12 @@ export async function POST() { // DB not available } const env = await readConfig( - [ - "GOOGLE_OAUTH_CLIENT_ID", - "GOOGLE_OAUTH_CLIENT_SECRET", - "GWS_SERVICES", - "GOOGLE_CLOUD_PROJECT", - ], + ["GOOGLE_OAUTH_CLIENT_ID", "GOOGLE_OAUTH_CLIENT_SECRET", "GOOGLE_CLOUD_PROJECT"], sql, ); const clientId = env.GOOGLE_OAUTH_CLIENT_ID; const clientSecret = env.GOOGLE_OAUTH_CLIENT_SECRET; - const gwsServices = env.GWS_SERVICES; const gcpProjectId = env.GOOGLE_CLOUD_PROJECT; if (!clientId || !clientSecret) { @@ -53,14 +47,12 @@ export async function POST() { // Write a valid client_secret.json (with project_id) for the CLI flow. // Same helper /api/env uses on save, so the two paths can't drift. - const { writeGwsClientSecret, gwsClientSecretPath } = - await import("@/lib/sync-gws-client-secret"); + const { writeGwsClientSecret } = await import("@/lib/sync-gws-client-secret"); writeGwsClientSecret({ clientId, clientSecret, projectId: gcpProjectId ?? "", }); - const clientSecretPath = gwsClientSecretPath(); // Build args for gws auth login with explicit scopes. // Using --scopes ensures Gmail/Calendar are included even if the gws CLI diff --git a/src/cate/integration.ts b/src/cate/integration.ts index a14cdbb..4d665d7 100644 --- a/src/cate/integration.ts +++ b/src/cate/integration.ts @@ -12,6 +12,9 @@ import type { PolicyConfig } from "@project-nomos/cate-sdk/types"; import { NomosKeystore } from "./nomos-keystore.ts"; import { NomosTransport } from "./nomos-transport.ts"; import { getDb } from "../db/client.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("cate-integration"); const AGENT_KEY_ID = "nomos-agent"; const USER_KEY_ID = "nomos-user"; @@ -43,14 +46,14 @@ export async function initCATEIntegration(options?: { let agentKey = await keystore.getKey(AGENT_KEY_ID); if (!agentKey) { agentKey = await keystore.generateKey(AGENT_KEY_ID); - console.log("[cate] Generated new agent key pair"); + log.info("Generated new agent key pair"); } // Create or load user key (for VC issuance) let userKey = await keystore.getKey(USER_KEY_ID); if (!userKey) { userKey = await keystore.generateKey(USER_KEY_ID); - console.log("[cate] Generated new user key pair"); + log.info("Generated new user key pair"); } const agentDid = createDIDKey(agentKey.publicKey); @@ -102,18 +105,23 @@ export async function initCATEIntegration(options?: { identity: { did: agentDid, keystore }, policy: policyConfig, onMessage: async (envelope, context) => { - console.log( - `[cate] Received envelope from ${context.senderDid} (${context.trustTier}, ${context.policyAction})`, + log.info( + { + senderDid: context.senderDid, + trustTier: context.trustTier, + policyAction: context.policyAction, + }, + "Received envelope", ); await options?.onMessage?.(envelope); }, onError: (error) => { - console.error(`[cate] Error: ${error.message}`); + log.error({ err: error }, "Error"); }, }); await server.listen({ transport }); - console.log(`[cate] Server started on port ${port} (DID: ${agentDid})`); + log.info({ port, agentDid }, "Server started"); return { server, agentDid, agentCard, keystore, transport }; } @@ -168,5 +176,5 @@ async function loadPolicyConfig(): Promise { */ export async function stopCATEIntegration(integration: CATEIntegration): Promise { await integration.server.close(); - console.log("[cate] Server stopped"); + log.info("Server stopped"); } diff --git a/src/cate/nomos-transport.ts b/src/cate/nomos-transport.ts index 1e27d81..c7cf2a1 100644 --- a/src/cate/nomos-transport.ts +++ b/src/cate/nomos-transport.ts @@ -13,6 +13,9 @@ import { type TransportOptions, type TransportEvents, } from "@project-nomos/cate-sdk/transport"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("cate-transport"); export class NomosTransport extends Transport { private server?: { close: () => void }; @@ -62,7 +65,7 @@ export class NomosTransport extends Transport { await new Promise((resolve) => { server.listen(this.port, () => { - console.log(`[cate] Transport listening on port ${this.port}`); + log.info({ port: this.port }, "Transport listening"); resolve(); }); }); diff --git a/src/config/file-sync.ts b/src/config/file-sync.ts index c82f48e..4407859 100644 --- a/src/config/file-sync.ts +++ b/src/config/file-sync.ts @@ -14,6 +14,9 @@ import fs from "node:fs"; import path from "node:path"; import { createHash } from "node:crypto"; import { getKysely } from "../db/client.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("file-sync"); // ── Paths ── @@ -42,11 +45,9 @@ async function syncOne(file: ManagedFile): Promise { // Check disk (first path that exists wins) let diskContent: string | null = null; - let diskPath: string | null = null; for (const p of file.diskPaths) { if (fs.existsSync(p)) { diskContent = fs.readFileSync(p, "utf-8"); - diskPath = p; break; } } @@ -85,7 +86,7 @@ async function syncOne(file: ManagedFile): Promise { const dir = path.dirname(restorePath); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(restorePath, row.content, "utf-8"); - console.log(`[file-sync] Restored ${file.dbPath} -> ${restorePath}`); + log.info({ dbPath: file.dbPath, restorePath }, "Restored"); return row.content; } @@ -112,10 +113,7 @@ export async function syncFileToDb(dbPath: string, content: string): Promise { restored += restoredPersonal; if (synced > 0 || restored > 0) { - console.log( - `[file-sync] Synced ${synced} file(s) to DB` + - (restored > 0 ? `, restored ${restored} from DB` : ""), + log.info( + { synced, restored }, + `Synced ${synced} file(s) to DB` + (restored > 0 ? `, restored ${restored} from DB` : ""), ); } } diff --git a/src/cron/scheduler.ts b/src/cron/scheduler.ts index cb6b521..489df56 100644 --- a/src/cron/scheduler.ts +++ b/src/cron/scheduler.ts @@ -1,5 +1,8 @@ import { CronExpressionParser } from "cron-parser"; import type { CronJob } from "./types.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("cron-scheduler"); export type CronCallback = (job: CronJob) => Promise; @@ -98,7 +101,7 @@ export class CronScheduler { this.timers.set(job.id, timer); } catch (error) { - console.error(`Failed to schedule job ${job.id}:`, error); + log.error({ err: error, jobId: job.id }, "Failed to schedule job"); } } @@ -130,7 +133,7 @@ export class CronScheduler { try { await this.callback(job); } catch (error) { - console.error(`Error executing cron job ${job.id}:`, error); + log.error({ err: error, jobId: job.id }, "Error executing cron job"); } } } diff --git a/src/daemon/agent-runtime.ts b/src/daemon/agent-runtime.ts index fc93515..fa28940 100644 --- a/src/daemon/agent-runtime.ts +++ b/src/daemon/agent-runtime.ts @@ -54,6 +54,9 @@ import { type Persona, } from "../config/personas.ts"; import { ShadowObserver } from "../memory/shadow-observer.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("agent-runtime"); export class AgentRuntime { // Cached at startup @@ -119,11 +122,9 @@ export class AgentRuntime { userId: ws.user_id, })); - console.log( - `[agent-runtime] Reloaded ${Object.keys(wsServers).length} workspace MCP server(s)`, - ); + log.info(`Reloaded ${Object.keys(wsServers).length} workspace MCP server(s)`); } catch (err) { - console.error("[agent-runtime] Failed to reload workspace MCPs:", err); + log.error({ err }, "Failed to reload workspace MCPs"); } } @@ -139,8 +140,8 @@ export class AgentRuntime { await runMigrations(); } catch (err) { const msg = err instanceof Error ? err.message : String(err); - console.warn( - `[agent-runtime] DB migrations skipped (${msg}). Continuing without DB so the setup wizard can configure it.`, + log.warn( + `DB migrations skipped (${msg}). Continuing without DB so the setup wizard can configure it.`, ); } @@ -172,8 +173,8 @@ export class AgentRuntime { // Load plugins (ensure defaults are installed on first run) const newlyInstalled = await ensureDefaultPlugins(); if (newlyInstalled.length > 0) { - console.log( - `[agent-runtime] Pre-installed ${newlyInstalled.length} default plugin(s): ${newlyInstalled.join(", ")}`, + log.info( + `Pre-installed ${newlyInstalled.length} default plugin(s): ${newlyInstalled.join(", ")}`, ); } const loadedPlugins = await loadInstalledPlugins(); @@ -356,36 +357,34 @@ export class AgentRuntime { } this.initialized = true; - console.log("[agent-runtime] Initialized"); - console.log(`[agent-runtime] Model: ${this.config.model}`); + log.info("Initialized"); + log.info(` Model: ${this.config.model}`); if (this.config.teamMode) { - console.log( - `[agent-runtime] Team mode: enabled (max ${this.config.maxTeamWorkers} workers)`, - ); + log.info(` Team mode: enabled (max ${this.config.maxTeamWorkers} workers)`); } if (this.config.adaptiveMemory) { - console.log( - `[agent-runtime] Adaptive memory: enabled${userModel?.length ? ` (${userModel.length} model entries)` : ""}`, + log.info( + ` Adaptive memory: enabled${userModel?.length ? ` (${userModel.length} model entries)` : ""}`, ); } if (this.config.shadowMode) { const stats = this.shadowObserver!.getStats(); - console.log( - `[agent-runtime] Shadow mode: enabled (${stats.tools} tool obs, ${stats.corrections} corrections)`, + log.info( + ` Shadow mode: enabled (${stats.tools} tool obs, ${stats.corrections} corrections)`, ); } if (this.config.anthropicBaseUrl) { - console.log(`[agent-runtime] API base URL: ${this.config.anthropicBaseUrl}`); + log.info(` API base URL: ${this.config.anthropicBaseUrl}`); } - console.log(`[agent-runtime] Identity: ${this.identity.emoji ?? ""} ${this.identity.name}`); - console.log(`[agent-runtime] MCP servers: ${Object.keys(this.mcpServers).join(", ")}`); + log.info(` Identity: ${this.identity.emoji ?? ""} ${this.identity.name}`); + log.info(` MCP servers: ${Object.keys(this.mcpServers).join(", ")}`); if (this.plugins.length > 0) { const pluginNames = this.plugins.map((p) => p.path.split("/").pop()).join(", "); - console.log(`[agent-runtime] Plugins: ${pluginNames}`); + log.info(` Plugins: ${pluginNames}`); } if (this.personas.length > 0) { const personaNames = this.personas.filter((p) => p.enabled).map((p) => p.name); - console.log(`[agent-runtime] Personas: ${personaNames.join(", ")}`); + log.info(` Personas: ${personaNames.join(", ")}`); } } @@ -530,8 +529,8 @@ export class AgentRuntime { if (this.config.smartRouting) { const classification = classifyQuery(message.content); model = this.config.modelTiers[classification.tier]; - console.log( - `[agent-runtime] Smart routing: "${classification.tier}" (confidence: ${classification.confidence.toFixed(2)}) → ${model}`, + log.info( + `Smart routing: "${classification.tier}" (confidence: ${classification.confidence.toFixed(2)}) → ${model}`, ); // Show routing decision in the chat const shortModel = model.replace("claude-", ""); @@ -544,11 +543,9 @@ export class AgentRuntime { // Check for team mode trigger (/team prefix) const teamTask = this.teamRuntime ? stripTeamPrefix(message.content) : null; - console.log( - `[agent-runtime] Team check: teamRuntime=${!!this.teamRuntime}, teamTask=${!!teamTask}`, - ); + log.info(`Team check: teamRuntime=${!!this.teamRuntime}, teamTask=${!!teamTask}`); if (teamTask && this.teamRuntime) { - console.log(`[agent-runtime] Executing team task: ${teamTask.slice(0, 100)}`); + log.info(`Executing team task: ${teamTask.slice(0, 100)}`); emit({ type: "system", @@ -578,7 +575,7 @@ export class AgentRuntime { ); const content = result || "_(no response)_"; - console.log(`[agent-runtime] Team result: ${content.length} chars`); + log.info(`Team result: ${content.length} chars`); // Store team result so subsequent turns have context const teamSummary = @@ -706,8 +703,9 @@ export class AgentRuntime { }; } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); - console.error( - `[agent-runtime] SDK error (model: ${model ?? this.config.model}, resume: ${!!resumeId}): ${errMsg}`, + log.error( + { err: errMsg }, + `SDK error (model: ${model ?? this.config.model}, resume: ${!!resumeId})`, ); // If resume failed, retry without resume. @@ -758,9 +756,7 @@ export class AgentRuntime { model !== this.config.modelTiers.moderate ) { const upgradeModel = this.config.modelTiers.moderate; - console.warn( - `[agent-runtime] Context too large for ${model}, upgrading to ${upgradeModel}`, - ); + log.warn(`Context too large for ${model}, upgrading to ${upgradeModel}`); emit({ type: "system", subtype: "status", @@ -863,7 +859,7 @@ export class AgentRuntime { stderr: (data: string) => { // Log SDK subprocess stderr so we can diagnose crash reasons const trimmed = data.trim(); - if (trimmed) console.error(`[agent-runtime:stderr] ${trimmed}`); + if (trimmed) log.error(`[stderr] ${trimmed}`); }, }); diff --git a/src/daemon/autonomous.ts b/src/daemon/autonomous.ts index f0c6e57..ae48263 100644 --- a/src/daemon/autonomous.ts +++ b/src/daemon/autonomous.ts @@ -13,6 +13,9 @@ import os from "node:os"; import path from "node:path"; import { getKysely } from "../db/client.ts"; import { parseFrontmatter } from "../skills/frontmatter.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("autonomous"); interface AutonomousLoop { name: string; @@ -159,9 +162,9 @@ export async function seedAutonomousLoops(): Promise { } if (seeded > 0) { - console.log(`[autonomous] Seeded ${seeded} autonomous loop(s)`); + log.info(`Seeded ${seeded} autonomous loop(s)`); } else { - console.log(`[autonomous] All ${loops.length} autonomous loops already seeded`); + log.info(`All ${loops.length} autonomous loops already seeded`); } } diff --git a/src/daemon/channel-manager.ts b/src/daemon/channel-manager.ts index 12a825f..4c4923a 100644 --- a/src/daemon/channel-manager.ts +++ b/src/daemon/channel-manager.ts @@ -5,6 +5,9 @@ import type { ChannelAdapter, IncomingMessage, OutgoingMessage } from "./types.ts"; import { MessageHookPipeline, type MessageHook } from "./message-hooks.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("channel-manager"); export class ChannelManager { private adapters = new Map(); @@ -34,7 +37,7 @@ export class ChannelManager { const hookList = this.hooks.listHooks(); if (hookList.length > 0) { - console.log(`[channel-manager] Loaded ${hookList.length} message hook(s)`); + log.info(`Loaded ${hookList.length} message hook(s)`); } // Group adapters by rate-limit domain (platform prefix before ":"). @@ -61,9 +64,9 @@ export class ChannelManager { } try { await adapter.start(); - console.log(`[channel-manager] Started: ${adapter.platform}`); + log.info(`Started: ${adapter.platform}`); } catch (err) { - console.error(`[channel-manager] Failed to start ${adapter.platform}:`, err); + log.error({ err }, `Failed to start ${adapter.platform}`); } } }), @@ -71,9 +74,7 @@ export class ChannelManager { const failures = results.filter((r) => r.status === "rejected"); if (failures.length > 0) { - console.warn( - `[channel-manager] ${failures.length}/${this.adapters.size} adapter group(s) failed to start`, - ); + log.warn(`${failures.length}/${this.adapters.size} adapter group(s) failed to start`); } } @@ -86,9 +87,9 @@ export class ChannelManager { [...this.adapters.values()].map(async (adapter) => { try { await adapter.stop(); - console.log(`[channel-manager] Stopped: ${adapter.platform}`); + log.info(`Stopped: ${adapter.platform}`); } catch (err) { - console.error(`[channel-manager] Error stopping ${adapter.platform}:`, err); + log.error({ err }, `Error stopping ${adapter.platform}`); } }), ); @@ -112,7 +113,7 @@ export class ChannelManager { const adapter = this.adapters.get(transformed.platform); if (!adapter) { - console.warn(`[channel-manager] No adapter for platform "${transformed.platform}"`); + log.warn(`No adapter for platform "${transformed.platform}"`); return; } await adapter.send(transformed); @@ -134,9 +135,9 @@ export class ChannelManager { this.adapters.set(adapter.platform, adapter); try { await adapter.start(); - console.log(`[channel-manager] Hot-loaded: ${adapter.platform}`); + log.info(`Hot-loaded: ${adapter.platform}`); } catch (err) { - console.error(`[channel-manager] Failed to hot-load ${adapter.platform}:`, err); + log.error({ err }, `Failed to hot-load ${adapter.platform}`); this.adapters.delete(adapter.platform); } } @@ -147,7 +148,7 @@ export class ChannelManager { if (!adapter) return false; try { await adapter.stop(); - console.log(`[channel-manager] Removed: ${platform}`); + log.info(`Removed: ${platform}`); } catch { // Ignore stop errors } diff --git a/src/daemon/channels/discord.ts b/src/daemon/channels/discord.ts index 32eb01c..1f28b1c 100644 --- a/src/daemon/channels/discord.ts +++ b/src/daemon/channels/discord.ts @@ -16,6 +16,9 @@ import { chunkResponse } from "../response-chunker.ts"; import { randomUUID } from "node:crypto"; import { Buffer } from "node:buffer"; import { AttachmentBuilder } from "discord.js"; +import { createLogger } from "../../lib/logger.ts"; + +const log = createLogger("discord"); export interface DiscordAdapterOptions { onMessage: (msg: IncomingMessage) => void; @@ -57,7 +60,7 @@ export class DiscordAdapter implements ChannelAdapter { }); this.client.once(Events.ClientReady, (c) => { - console.log(`[discord-adapter] Logged in as ${c.user.tag}`); + log.info(`Logged in as ${c.user.tag}`); }); this.client.on(Events.MessageCreate, (message) => { diff --git a/src/daemon/channels/email-imap.ts b/src/daemon/channels/email-imap.ts index 4c4efe8..23b900d 100644 --- a/src/daemon/channels/email-imap.ts +++ b/src/daemon/channels/email-imap.ts @@ -7,6 +7,9 @@ import { ImapFlow, type FetchMessageObject } from "imapflow"; import { simpleParser, type ParsedMail } from "mailparser"; +import { createLogger } from "../../lib/logger.ts"; + +const log = createLogger("email-imap"); export interface ImapConfig { host: string; @@ -55,7 +58,7 @@ export class ImapClient { }); await this.client.connect(); - console.log(`[email-imap] Connected to ${this.config.host}`); + log.info(`Connected to ${this.config.host}`); // Select INBOX and start IDLE const lock = await this.client.getMailboxLock("INBOX"); @@ -98,7 +101,7 @@ export class ImapClient { lock.release(); } } catch (err) { - console.warn("[email-imap] IDLE error:", err); + log.warn({ err }, "IDLE error"); } // Re-enter IDLE after timeout diff --git a/src/daemon/channels/email-smtp.ts b/src/daemon/channels/email-smtp.ts index 8038b00..db8f3b7 100644 --- a/src/daemon/channels/email-smtp.ts +++ b/src/daemon/channels/email-smtp.ts @@ -5,6 +5,9 @@ */ import { createTransport, type Transporter } from "nodemailer"; +import { createLogger } from "../../lib/logger.ts"; + +const log = createLogger("email-smtp"); export interface SmtpConfig { host: string; @@ -38,7 +41,7 @@ export class SmtpClient { // Verify connection await this.transporter.verify(); - console.log(`[email-smtp] Connected to ${this.config.host}`); + log.info(`Connected to ${this.config.host}`); } async send(opts: { @@ -61,7 +64,7 @@ export class SmtpClient { references: opts.references?.join(" "), }); - console.log(`[email-smtp] Sent to ${opts.to}: ${info.messageId}`); + log.info({ to: opts.to, messageId: info.messageId }, `Sent to ${opts.to}`); return info.messageId; } diff --git a/src/daemon/channels/email.ts b/src/daemon/channels/email.ts index 9431deb..8787ed9 100644 --- a/src/daemon/channels/email.ts +++ b/src/daemon/channels/email.ts @@ -10,6 +10,9 @@ import type { ChannelAdapter, IncomingMessage, OutgoingMessage } from "../types. import type { DraftManager } from "../draft-manager.ts"; import { ImapClient, type ImapConfig, type ParsedEmail } from "./email-imap.ts"; import { SmtpClient, type SmtpConfig } from "./email-smtp.ts"; +import { createLogger } from "../../lib/logger.ts"; + +const log = createLogger("email"); export interface EmailAdapterOptions { imap: ImapConfig; @@ -60,7 +63,7 @@ export class EmailAdapter implements ChannelAdapter { }); }); - console.log(`[email-adapter] Running (${this.userEmail})`); + log.info(`Running (${this.userEmail})`); } async stop(): Promise { diff --git a/src/daemon/channels/imessage-imsg.ts b/src/daemon/channels/imessage-imsg.ts index 0dccf67..982cfa2 100644 --- a/src/daemon/channels/imessage-imsg.ts +++ b/src/daemon/channels/imessage-imsg.ts @@ -20,6 +20,9 @@ import { randomUUID } from "node:crypto"; import { createInterface } from "node:readline"; import { promisify } from "node:util"; import type { IncomingMessage } from "../types.ts"; +import { createLogger } from "../../lib/logger.ts"; + +const log = createLogger("imessage-imsg"); const execFileAsync = promisify(execFile); @@ -98,11 +101,11 @@ export class ImsgAdapter { if (this.featureMode === "advanced") { try { await execFileAsync("imsg", ["launch"], { timeout: 15_000 }); - console.log("[imsg-adapter] Advanced IMCore bridge loaded"); + log.info("Advanced IMCore bridge loaded"); } catch (err) { const msg = err instanceof Error ? err.message : String(err); - console.warn( - `[imsg-adapter] Advanced mode requested but launch failed: ${msg}. ` + + log.warn( + `Advanced mode requested but launch failed: ${msg}. ` + "Disable SIP (csrutil disable in Recovery Mode) to use edit/unsend/typing.", ); } @@ -115,12 +118,12 @@ export class ImsgAdapter { }); this.watchProc.on("error", (err) => { - console.error("[imsg-adapter] Watch process error:", err.message); + log.error({ err: err.message }, "Watch process error"); }); this.watchProc.on("exit", (code) => { if (!this.stopping) { - console.warn(`[imsg-adapter] Watch process exited with code ${code}`); + log.warn(`Watch process exited with code ${code}`); } this.watchProc = null; }); @@ -132,10 +135,10 @@ export class ImsgAdapter { // Log stderr (progress + warnings) this.watchProc.stderr?.on("data", (chunk: Buffer) => { const text = chunk.toString().trim(); - if (text) console.log(`[imsg-adapter:stderr] ${text}`); + if (text) log.info({ stderr: text }, "imsg stderr"); }); - console.log(`[imsg-adapter] Started in ${this.featureMode} mode (watching chat.db)`); + log.info(`Started in ${this.featureMode} mode (watching chat.db)`); } async stop(): Promise { @@ -213,7 +216,7 @@ export class ImsgAdapter { const msg = JSON.parse(trimmed) as ImsgMessage; this.processMessage(msg); } catch (err) { - console.warn("[imsg-adapter] Failed to parse JSON line:", err); + log.warn({ err }, "Failed to parse JSON line"); } } diff --git a/src/daemon/channels/imessage.ts b/src/daemon/channels/imessage.ts index c43cca2..a943c1c 100644 --- a/src/daemon/channels/imessage.ts +++ b/src/daemon/channels/imessage.ts @@ -20,6 +20,9 @@ import { ImsgAdapter, type ImsgFeatureMode } from "./imessage-imsg.ts"; import type { ChannelAdapter, IncomingMessage, OutgoingMessage } from "../types.ts"; import type { DraftManager } from "../draft-manager.ts"; +import { createLogger } from "../../lib/logger.ts"; + +const log = createLogger("imessage"); const MAX_LENGTH = 4000; @@ -98,14 +101,14 @@ export class IMessageAdapter implements ChannelAdapter { } if (this.agentMode === "agent" && this.ownerIdentities.size === 0) { - console.warn( - "[imessage-adapter] Agent mode requires owner phone or Apple ID. " + + log.warn( + "Agent mode requires owner phone or Apple ID. " + "Set IMESSAGE_OWNER_PHONE or IMESSAGE_OWNER_APPLE_ID, or use passive mode.", ); } - console.log( - `[imessage-adapter] Starting (agent: ${this.agentMode}, features: ${this.featureMode}, imsg: ${check.version})` + + log.info( + `Starting (agent: ${this.agentMode}, features: ${this.featureMode}, imsg: ${check.version})` + (this.agentMode === "agent" ? `, owner: ${[...this.ownerIdentities].join(", ")}` : ""), ); diff --git a/src/daemon/channels/slack-mrkdwn.ts b/src/daemon/channels/slack-mrkdwn.ts index 334dbfd..af31201 100644 --- a/src/daemon/channels/slack-mrkdwn.ts +++ b/src/daemon/channels/slack-mrkdwn.ts @@ -17,18 +17,19 @@ export function markdownToSlackMrkdwn(text: string): string { let result = text; - // Protect code blocks from conversion (extract, convert around them, re-insert) + // Protect code blocks from conversion (extract, convert around them, re-insert). + //  is in the Unicode Private Use Area, so it won't collide with real text. const codeBlocks: string[] = []; result = result.replace(/```[\s\S]*?```/g, (match) => { codeBlocks.push(match); - return `\x00CODEBLOCK${codeBlocks.length - 1}\x00`; + return `CODEBLOCK${codeBlocks.length - 1}`; }); // Protect inline code const inlineCode: string[] = []; result = result.replace(/`[^`]+`/g, (match) => { inlineCode.push(match); - return `\x00INLINE${inlineCode.length - 1}\x00`; + return `INLINE${inlineCode.length - 1}`; }); // Headers: # Heading -> *Heading* (bold) @@ -55,10 +56,10 @@ export function markdownToSlackMrkdwn(text: string): string { result = result.replace(/^[-*]{3,}$/gm, "———"); // Restore inline code - result = result.replace(/\x00INLINE(\d+)\x00/g, (_, i) => inlineCode[Number(i)]); + result = result.replace(/INLINE(\d+)/g, (_, i) => inlineCode[Number(i)]); // Restore code blocks - result = result.replace(/\x00CODEBLOCK(\d+)\x00/g, (_, i) => codeBlocks[Number(i)]); + result = result.replace(/CODEBLOCK(\d+)/g, (_, i) => codeBlocks[Number(i)]); return result; } diff --git a/src/daemon/channels/slack-polling.ts b/src/daemon/channels/slack-polling.ts index e9effee..eaa3085 100644 --- a/src/daemon/channels/slack-polling.ts +++ b/src/daemon/channels/slack-polling.ts @@ -20,6 +20,9 @@ import type { ChannelAdapter, IncomingMessage, OutgoingMessage } from "../types. import type { DraftManager } from "../draft-manager.ts"; import { randomUUID } from "node:crypto"; import { markdownToSlackMrkdwn } from "./slack-mrkdwn.ts"; +import { createLogger } from "../../lib/logger.ts"; + +const log = createLogger("slack-polling"); export interface SlackPollingAdapterOptions { token: string; @@ -124,7 +127,7 @@ export class SlackPollingAdapter implements ChannelAdapter { const userName = (auth.user as string) ?? this.userId; this.teamName = (auth.team as string) ?? this.teamId; - console.log(`[slack-polling] Running (user: ${userName}, team: ${this.teamId})`); + log.info(`Running (user: ${userName}, team: ${this.teamId})`); this.running = true; @@ -147,9 +150,9 @@ export class SlackPollingAdapter implements ChannelAdapter { try { const botAuth = await this.botClient.auth.test(); this.botUserId = (botAuth.user_id as string) ?? null; - console.log(`[slack-polling] Bot identity loaded (${this.botUserId})`); + log.info(`Bot identity loaded (${this.botUserId})`); } catch { - console.warn(`[slack-polling] Bot token auth failed -- agent will post as user`); + log.warn(`Bot token auth failed -- agent will post as user`); this.botClient = null; } } @@ -190,9 +193,7 @@ export class SlackPollingAdapter implements ChannelAdapter { const nd = await getNotificationDefault(); if (nd && nd.platform === this.platform) { this.defaultChannelId = nd.channelId; - console.log( - `[slack-polling] Default channel: ${nd.channelId} (${nd.label ?? "unlabeled"})`, - ); + log.info(`Default channel: ${nd.channelId} (${nd.label ?? "unlabeled"})`); } } catch { // notification defaults not available @@ -204,23 +205,21 @@ export class SlackPollingAdapter implements ChannelAdapter { // Start polling after optional stagger delay (prevents burst of API calls // when multiple workspaces start simultaneously) const startPolling = () => { - console.log( - `[slack-polling] Polling started for team ${this.teamId} (every ${Math.round(this.pollIntervalMs / 1000)}s)`, + log.info( + `Polling started for team ${this.teamId} (every ${Math.round(this.pollIntervalMs / 1000)}s)`, ); // Run first poll immediately, then on interval - this.poll().catch((err) => - console.error(`[slack-polling] Poll error (team ${this.teamId}):`, err), - ); + this.poll().catch((err) => log.error({ err }, `Poll error (team ${this.teamId})`)); this.pollTimer = setInterval(() => { this.poll().catch((err) => { - console.error(`[slack-polling] Poll error (team ${this.teamId}):`, err); + log.error({ err }, `Poll error (team ${this.teamId})`); }); }, this.pollIntervalMs); }; if (this.startDelayMs > 0) { - console.log( - `[slack-polling] Delaying poll start by ${Math.round(this.startDelayMs / 1000)}s for team ${this.teamId}`, + log.info( + `Delaying poll start by ${Math.round(this.startDelayMs / 1000)}s for team ${this.teamId}`, ); setTimeout(startPolling, this.startDelayMs); } else { @@ -375,13 +374,13 @@ export class SlackPollingAdapter implements ChannelAdapter { if (this.defaultChannelId) { this.lastSeenTs.set(this.defaultChannelId, nowTs); } - console.log( - `[slack-polling] Baseline set for ${dmChannels.length} DMs` + + log.info( + `Baseline set for ${dmChannels.length} DMs` + (this.defaultChannelId ? ` + default channel` : "") + ` (using current time)`, ); } catch (err) { - console.warn("[slack-polling] Failed to initialize baselines:", err); + log.warn({ err }, "Failed to initialize baselines"); } } @@ -517,8 +516,8 @@ export class SlackPollingAdapter implements ChannelAdapter { } if (isFullScan) { - console.log( - `[slack-polling] Full scan: ${dmsToPoll.length}/${dmChannels.length} DMs polled (team ${this.teamId})`, + log.info( + `Full scan: ${dmsToPoll.length}/${dmChannels.length} DMs polled (team ${this.teamId})`, ); // Decay active set -- full scan re-adds any with messages this.activeChannels.clear(); @@ -533,7 +532,7 @@ export class SlackPollingAdapter implements ChannelAdapter { this.consecutiveErrors++; if (!this.authErrorFired && this.consecutiveErrors >= 3) { this.authErrorFired = true; - console.error(`[slack-polling] Token expired for team ${this.teamId} — stopping polling`); + log.error(`Token expired for team ${this.teamId} — stopping polling`); this.onAuthError?.(this.teamId, this.teamName ?? this.teamId); await this.stop(); } @@ -581,7 +580,7 @@ export class SlackPollingAdapter implements ChannelAdapter { if (messages.length === 0) return false; if (channelId === this.defaultChannelId) { - console.log(`[slack-polling] Default channel has ${messages.length} new message(s)`); + log.info(`Default channel has ${messages.length} new message(s)`); } // Update last seen to newest message @@ -634,7 +633,7 @@ export class SlackPollingAdapter implements ChannelAdapter { // Log errors for the default channel (important), skip others silently if (channelId === this.defaultChannelId) { const msg = err instanceof Error ? err.message : String(err); - console.error(`[slack-polling] Error polling default channel ${channelId}: ${msg}`); + log.error({ err: msg }, `Error polling default channel ${channelId}`); } return false; } @@ -778,7 +777,7 @@ export class SlackPollingAdapter implements ChannelAdapter { // user_not_found is expected for Slack Connect / cross-workspace users; // only warn for unexpected failures. if (!msg.includes("user_not_found")) { - console.warn(`[slack-polling] ${label} client failed to resolve user ${userId}: ${msg}`); + log.warn({ err: msg }, `${label} client failed to resolve user ${userId}`); } } } diff --git a/src/daemon/channels/slack-user.ts b/src/daemon/channels/slack-user.ts index 7a7b4a9..0928b2f 100644 --- a/src/daemon/channels/slack-user.ts +++ b/src/daemon/channels/slack-user.ts @@ -16,6 +16,9 @@ import type { ChannelAdapter, IncomingMessage, OutgoingMessage } from "../types. import type { DraftManager } from "../draft-manager.ts"; import { randomUUID } from "node:crypto"; import { markdownToSlackMrkdwn } from "./slack-mrkdwn.ts"; +import { createLogger } from "../../lib/logger.ts"; + +const log = createLogger("slack-user"); // CJS/ESM interop (same pattern as slack.ts) const slackBoltModule = SlackBolt as typeof import("@slack/bolt") & { @@ -103,9 +106,9 @@ export class SlackUserAdapter implements ChannelAdapter { try { const botAuth = await this.botClient.auth.test(); this.botUserId = (botAuth.user_id as string) ?? null; - console.log(`[slack-user-adapter] Bot identity loaded (${this.botUserId})`); + log.info(`Bot identity loaded (${this.botUserId})`); } catch { - console.warn(`[slack-user-adapter] Bot token auth failed -- agent will post as user`); + log.warn(`Bot token auth failed -- agent will post as user`); this.botClient = null; } } @@ -122,7 +125,7 @@ export class SlackUserAdapter implements ChannelAdapter { const uid = (ws.metadata as Record)?.user_id; if (typeof uid === "string") this.ownUserIds.add(uid); } - console.log(`[slack-user-adapter] Own user IDs: ${[...this.ownUserIds].join(", ")}`); + log.info(`Own user IDs: ${[...this.ownUserIds].join(", ")}`); } catch { // integrations not available } @@ -149,9 +152,7 @@ export class SlackUserAdapter implements ChannelAdapter { const nd = await getNotificationDefault(); if (nd && nd.platform === this.platform) { this.defaultChannelId = nd.channelId; - console.log( - `[slack-user-adapter] Default channel: ${nd.channelId} (${nd.label ?? "unlabeled"})`, - ); + log.info(`Default channel: ${nd.channelId} (${nd.label ?? "unlabeled"})`); } } catch { // notification defaults not available @@ -189,8 +190,8 @@ export class SlackUserAdapter implements ChannelAdapter { return; } - console.log( - `[slack-user-adapter] Processing message: user=${e.user}, channel=${e.channel}, type=${e.channel_type}, isDefault=${isDefaultChannel}, myUserId=${this.userId}`, + log.info( + `Processing message: user=${e.user}, channel=${e.channel}, type=${e.channel_type}, isDefault=${isDefaultChannel}, myUserId=${this.userId}`, ); if (isDefaultChannel) { @@ -299,8 +300,8 @@ export class SlackUserAdapter implements ChannelAdapter { }); await this.app.start(); - console.log( - `[slack-user-adapter] Running via Socket Mode (user: ${this.userId}, bot: ${this.botUserId}, team: ${this.teamId}, defaultChannel: ${this.defaultChannelId})`, + log.info( + `Running via Socket Mode (user: ${this.userId}, bot: ${this.botUserId}, team: ${this.teamId}, defaultChannel: ${this.defaultChannelId})`, ); } @@ -555,10 +556,7 @@ export class SlackUserAdapter implements ChannelAdapter { if (cached) return cached; // Try bot client first (more likely to have users:read scope), then user client - for (const [label, client] of [ - ["bot", this.botClient], - ["user", this.userClient], - ] as const) { + for (const client of [this.botClient, this.userClient]) { if (!client) continue; try { const result = await (client as WebClient).users.info({ user: userId }); diff --git a/src/daemon/channels/slack.ts b/src/daemon/channels/slack.ts index 4244515..7601abc 100644 --- a/src/daemon/channels/slack.ts +++ b/src/daemon/channels/slack.ts @@ -9,6 +9,9 @@ import type { ChannelAdapter, IncomingMessage, OutgoingMessage } from "../types. import type { DraftManager } from "../draft-manager.ts"; import { chunkResponse } from "../response-chunker.ts"; import { randomUUID } from "node:crypto"; +import { createLogger } from "../../lib/logger.ts"; + +const log = createLogger("slack"); // CJS/ESM interop const slackBoltModule = SlackBolt as typeof import("@slack/bolt") & { @@ -112,7 +115,7 @@ export class SlackAdapter implements ChannelAdapter { } await this.app.start(); - console.log(`[slack-adapter] Running (bot: ${this.botUserId})`); + log.info(`Running (bot: ${this.botUserId})`); } async stop(): Promise { diff --git a/src/daemon/channels/telegram.ts b/src/daemon/channels/telegram.ts index e4a71af..ed4404a 100644 --- a/src/daemon/channels/telegram.ts +++ b/src/daemon/channels/telegram.ts @@ -8,6 +8,9 @@ import type { DraftManager } from "../draft-manager.ts"; import { chunkResponse } from "../response-chunker.ts"; import { randomUUID } from "node:crypto"; import { Buffer } from "node:buffer"; +import { createLogger } from "../../lib/logger.ts"; + +const log = createLogger("telegram"); export interface TelegramAdapterOptions { onMessage: (msg: IncomingMessage) => void; @@ -37,7 +40,7 @@ export class TelegramAdapter implements ChannelAdapter { this.bot = new Bot(token); const me = await this.bot.api.getMe(); - console.log(`[telegram-adapter] Logged in as @${me.username}`); + log.info(`Logged in as @${me.username}`); this.bot.on("message:text", (ctx) => { const chat = ctx.chat; diff --git a/src/daemon/channels/whatsapp.ts b/src/daemon/channels/whatsapp.ts index ded7a00..2877e98 100644 --- a/src/daemon/channels/whatsapp.ts +++ b/src/daemon/channels/whatsapp.ts @@ -16,6 +16,9 @@ import * as os from "node:os"; import { randomUUID } from "node:crypto"; import type { ChannelAdapter, IncomingMessage, OutgoingMessage } from "../types.ts"; import type { DraftManager } from "../draft-manager.ts"; +import { createLogger } from "../../lib/logger.ts"; + +const log = createLogger("whatsapp"); const MAX_LENGTH = 4096; @@ -94,7 +97,7 @@ export class WhatsAppAdapter implements ChannelAdapter { await connectToWhatsApp(); } } else if (connection === "open") { - console.log(`[whatsapp-adapter] Connected as ${this.sock?.user?.id}`); + log.info(`Connected as ${this.sock?.user?.id}`); } }); diff --git a/src/daemon/cron-engine.ts b/src/daemon/cron-engine.ts index 36be35b..9759b2c 100644 --- a/src/daemon/cron-engine.ts +++ b/src/daemon/cron-engine.ts @@ -10,6 +10,9 @@ import { createCronSystem, type CronSystem, type CronJob } from "../cron/index.t import type { MessageQueue } from "./message-queue.ts"; import type { ChannelManager } from "./channel-manager.ts"; import type { AgentEvent } from "./types.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("cron-engine"); export class CronEngine { private cronSystem: CronSystem | null = null; @@ -39,11 +42,11 @@ export class CronEngine { // Listen for refresh events from MCP tools (schedule_task, delete_scheduled_task) process.on("cron:refresh" as never, () => { - this.refresh().catch((err) => console.error("[cron-engine] Refresh failed:", err)); + this.refresh().catch((err) => log.error({ err }, "Refresh failed")); }); const jobs = await this.cronSystem.store.listJobs({ enabled: true }); - console.log(`[cron-engine] Started with ${jobs.length} job(s)`); + log.info(`Started with ${jobs.length} job(s)`); } /** Stop the cron system. */ @@ -66,31 +69,28 @@ export class CronEngine { // instead of the agent message queue. if (job.prompt.startsWith("__delta_sync__:")) { const platform = job.prompt.slice("__delta_sync__:".length); - console.log(`[cron-engine] Firing delta sync for ${platform}`); + log.info(`Firing delta sync for ${platform}`); process.emit("ingest:trigger" as never, { platform, runType: "delta" } as never); return; } // Intercept wiki compilation sentinel -- run compiler directly if (job.prompt === "__wiki_compile__") { - console.log("[cron-engine] Firing wiki compilation"); + log.info("Firing wiki compilation"); import("../memory/knowledge-compiler.ts") .then(({ compileKnowledge }) => compileKnowledge()) .then((result) => { - console.log( - `[cron-engine] Wiki compilation: ${result.articlesCreated} created, ${result.articlesUpdated} updated`, + log.info( + `Wiki compilation: ${result.articlesCreated} created, ${result.articlesUpdated} updated`, ); }) .catch((err) => { - console.error( - "[cron-engine] Wiki compilation failed:", - err instanceof Error ? err.message : err, - ); + log.error({ err: err instanceof Error ? err.message : err }, "Wiki compilation failed"); }); return; } - console.log(`[cron-engine] Triggering job: ${job.name} (${job.id})`); + log.info(`Triggering job: ${job.name} (${job.id})`); const sessionKey = job.sessionTarget === "isolated" ? `cron:${job.id}:${Date.now()}` : `cron:${job.id}`; @@ -119,7 +119,7 @@ export class CronEngine { try { runId = await this.cronSystem.store.recordRunStart(job.id, job.name, sessionKey); } catch (err) { - console.error("[cron-engine] Failed to record run start:", err); + log.error({ err }, "Failed to record run start"); } } @@ -165,7 +165,7 @@ export class CronEngine { } } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); - console.error(`[cron-engine] Job ${job.id} failed:`, errMsg); + log.error({ err: errMsg }, `Job ${job.id} failed`); const durationMs = Date.now() - startTime; diff --git a/src/daemon/draft-manager.ts b/src/daemon/draft-manager.ts index 4a0e7be..83f2479 100644 --- a/src/daemon/draft-manager.ts +++ b/src/daemon/draft-manager.ts @@ -21,6 +21,9 @@ import type { DraftRow } from "../db/drafts.ts"; import type { OutgoingMessage, AgentEvent } from "./types.ts"; import { findContactByIdentity } from "../identity/identities.ts"; import { getConsentMode, type ConsentMode } from "../db/consent-config.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("draft-manager"); export interface DraftManagerOptions { /** Broadcast a system event to all connected clients (gRPC + WebSocket). */ @@ -102,7 +105,7 @@ export class DraftManager { } if (autonomy === "silent") { - console.log(`[draft-manager] Discarded message to ${message.channelId} (autonomy: silent)`); + log.info(`Discarded message to ${message.channelId} (autonomy: silent)`); this.notifyWs?.({ type: "system", subtype: "message_silenced", @@ -144,11 +147,11 @@ export class DraftManager { // Post to default Slack channel with Approve/Edit/Decline buttons if (this.notifyDefaultChannel) { this.notifyDefaultChannel(draft, context).catch((err) => - console.error("[draft-manager] Failed to post to default channel:", err), + log.error({ err }, "Failed to post to default channel"), ); } - console.log(`[draft-manager] Draft created: ${draft.id.slice(0, 8)} for ${userId}`); + log.info(`Draft created: ${draft.id.slice(0, 8)} for ${userId}`); return draft; } @@ -177,14 +180,11 @@ export class DraftManager { data: { draftId: draft.id }, }); - console.log(`[draft-manager] Draft approved and sent: ${draft.id.slice(0, 8)}`); + log.info(`Draft approved and sent: ${draft.id.slice(0, 8)}`); return { success: true }; } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); - console.error( - `[draft-manager] Failed to send approved draft ${draft.id.slice(0, 8)}:`, - errMsg, - ); + log.error({ err: errMsg }, `Failed to send approved draft ${draft.id.slice(0, 8)}`); return { success: false, error: `Send failed: ${errMsg}` }; } } @@ -208,8 +208,8 @@ export class DraftManager { try { // Send the EDITED content, not the original draft - console.log( - `[draft-manager] Sending edited draft ${draft.id.slice(0, 8)} to ${draft.platform}:${draft.channel_id}`, + log.info( + `Sending edited draft ${draft.id.slice(0, 8)} to ${draft.platform}:${draft.channel_id}`, ); await sendFn(draft.channel_id, editedContent, draft.thread_id ?? undefined); await markDraftSent(draft.id); @@ -226,11 +226,11 @@ export class DraftManager { data: { draftId: draft.id, edited: true }, }); - console.log(`[draft-manager] Draft approved (edited) and sent: ${draft.id.slice(0, 8)}`); + log.info(`Draft approved (edited) and sent: ${draft.id.slice(0, 8)}`); return { success: true }; } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); - console.error(`[draft-manager] Failed to send edited draft ${draft.id.slice(0, 8)}:`, errMsg); + log.error({ err: errMsg }, `Failed to send edited draft ${draft.id.slice(0, 8)}`); return { success: false, error: `Send failed: ${errMsg}` }; } } @@ -251,7 +251,7 @@ export class DraftManager { data: { draftId: draft.id }, }); - console.log(`[draft-manager] Draft rejected: ${draft.id.slice(0, 8)}`); + log.info(`Draft rejected: ${draft.id.slice(0, 8)}`); return { success: true }; } @@ -267,7 +267,7 @@ export class DraftManager { const sendFn = this.sendFns.get(message.platform); if (sendFn) { await sendFn(message.channelId, message.content, message.threadId); - console.log(`[draft-manager] Auto-sent to ${message.channelId} on ${message.platform}`); + log.info(`Auto-sent to ${message.channelId} on ${message.platform}`); this.notifyWs?.({ type: "system", @@ -287,9 +287,7 @@ export class DraftManager { message.channelId, message.content, context, - ).catch((err) => - console.error("[draft-manager] Failed to post FYI to default channel:", err), - ); + ).catch((err) => log.error({ err }, "Failed to post FYI to default channel")); } } return null; @@ -348,12 +346,9 @@ export class DraftManager { }, [], ); - console.log(`[draft-manager] Draft edit captured as learning signal`); + log.info(`Draft edit captured as learning signal`); } catch (err) { - console.warn( - "[draft-manager] Failed to capture draft edit:", - err instanceof Error ? err.message : err, - ); + log.warn({ err: err instanceof Error ? err.message : err }, "Failed to capture draft edit"); } } } diff --git a/src/daemon/gateway.ts b/src/daemon/gateway.ts index c3d35b3..0c152a0 100644 --- a/src/daemon/gateway.ts +++ b/src/daemon/gateway.ts @@ -41,6 +41,9 @@ import { } from "../cate/integration.ts"; import type { IncomingMessage, AgentEvent } from "./types.ts"; import type { DraftRow } from "../db/drafts.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("gateway"); export interface GatewayOptions { /** WebSocket server port (default: 8765) */ @@ -127,7 +130,7 @@ export class Gateway { /** Start the daemon. */ async start(): Promise { - console.log("[gateway] Starting daemon..."); + log.info("Starting daemon..."); // Write PID file writePidFile(); @@ -143,9 +146,9 @@ export class Gateway { try { await this.startSettingsServer(); } catch (err) { - console.warn( - "[gateway] Settings UI failed to start early:", - err instanceof Error ? err.message : err, + log.warn( + { err: err instanceof Error ? err.message : err }, + "Settings UI failed to start early", ); } } @@ -159,12 +162,12 @@ export class Gateway { await this.runtime.initialize(); } catch (err) { runtimeReady = false; - console.warn( - "[gateway] Agent runtime failed to initialize:", - err instanceof Error ? err.message : err, + log.warn( + { err: err instanceof Error ? err.message : err }, + "Agent runtime failed to initialize", ); - console.warn( - "[gateway] Daemon is running in setup-only mode. Finish setup at http://localhost:" + + log.warn( + "Daemon is running in setup-only mode. Finish setup at http://localhost:" + (this.options.settingsPort ?? 3456), ); } @@ -173,7 +176,7 @@ export class Gateway { // the Settings UI + signal handlers alive so the user can configure // and the daemon restart picks it up. if (!runtimeReady) { - console.log("[gateway] Daemon is running (setup-only)"); + log.info("Daemon is running (setup-only)"); return; } @@ -182,7 +185,7 @@ export class Gateway { const { syncAllFiles } = await import("../config/file-sync.ts"); await syncAllFiles(); } catch (err) { - console.warn("[gateway] File sync failed:", err instanceof Error ? err.message : err); + log.warn({ err: err instanceof Error ? err.message : err }, "File sync failed"); } // Verify LLM access before starting services @@ -193,7 +196,7 @@ export class Gateway { const { seedAutonomousLoops } = await import("./autonomous.ts"); await seedAutonomousLoops(); } catch (err) { - console.warn("[gateway] Failed to seed autonomous loops:", err); + log.warn({ err }, "Failed to seed autonomous loops"); } // Start WebSocket server @@ -218,14 +221,14 @@ export class Gateway { // Map the "reload-slack-workspaces" UI payload to the canonical command const cmd = payload === "slack-workspaces" ? "reload-slack-workspaces" : payload; this.handleCommand(cmd).catch((err) => { - console.warn("[gateway] NOTIFY handler failed for", payload, err); + log.warn({ payload, err }, "NOTIFY handler failed"); }); }); this.notifyListener = listener; } catch (err) { - console.warn( - "[gateway] Could not start NOTIFY listener (settings reloads will be best-effort):", - err instanceof Error ? err.message : err, + log.warn( + { err: err instanceof Error ? err.message : err }, + "Could not start NOTIFY listener (settings reloads will be best-effort)", ); } @@ -254,7 +257,7 @@ export class Gateway { try { await this.cronEngine.start(); } catch (err) { - console.warn("[gateway] Cron engine failed to start:", err); + log.warn({ err }, "Cron engine failed to start"); } } @@ -262,14 +265,14 @@ export class Gateway { try { await registerDeltaSyncJobs(); } catch (err) { - console.warn("[gateway] Delta sync registration failed:", err); + log.warn({ err }, "Delta sync registration failed"); } // Register proactive feature cron jobs try { await registerProactiveJobs(); } catch (err) { - console.warn("[gateway] Proactive jobs registration failed:", err); + log.warn({ err }, "Proactive jobs registration failed"); } // Register wiki compilation cron job (compile knowledge every 6 hours) @@ -288,11 +291,11 @@ export class Gateway { enabled: true, errorCount: 0, }); - console.log("[gateway] Registered wiki compilation cron job (every 6h)"); + log.info("Registered wiki compilation cron job (every 6h)"); process.emit("cron:refresh" as never); } } catch (err) { - console.warn("[gateway] Wiki cron registration failed:", err); + log.warn({ err }, "Wiki cron registration failed"); } // Start CATE protocol server (agent-to-agent trust layer) @@ -328,7 +331,7 @@ export class Gateway { }, }); } catch (err) { - console.warn("[gateway] CATE integration failed to start:", err); + log.warn({ err }, "CATE integration failed to start"); } // Listen for ingest trigger events (from cron engine delta-sync) @@ -373,18 +376,18 @@ export class Gateway { const wsPort = this.options.port ?? 8765; const grpcPort = this.options.grpcPort ?? wsPort + 1; const settingsPort = this.options.settingsPort ?? 3456; - console.log("[gateway] Daemon is running"); - console.log(`[gateway] gRPC: localhost:${grpcPort}`); - console.log(`[gateway] WebSocket: ws://localhost:${wsPort}`); + log.info("Daemon is running"); + log.info(` gRPC: localhost:${grpcPort}`); + log.info(` WebSocket: ws://localhost:${wsPort}`); if (this.settingsProcess) { - console.log(`[gateway] Settings: http://localhost:${settingsPort}`); + log.info(` Settings: http://localhost:${settingsPort}`); } - console.log(`[gateway] Channels: ${platforms.length > 0 ? platforms.join(", ") : "none"}`); + log.info(` Channels: ${platforms.length > 0 ? platforms.join(", ") : "none"}`); } /** Stop the daemon gracefully. */ async stop(): Promise { - console.log("[gateway] Stopping daemon..."); + log.info("Stopping daemon..."); // Stop in reverse order if (this.notifyListener) { @@ -401,7 +404,7 @@ export class Gateway { await this.wsServer.stop(); await closeBrowser(); - console.log("[gateway] Daemon stopped"); + log.info("Daemon stopped"); } /** @@ -494,16 +497,16 @@ export class Gateway { await this.channelManager.send(result); } indexConversationTurn(msg, result).catch((err) => - console.error("[gateway] Memory indexing failed:", err), + log.error({ err }, "Memory indexing failed"), ); }) .catch(async (err) => { await responder?.finalize("Sorry, an error occurred."); - console.error(`[gateway] Failed to process message from ${msg.platform}:`, err); + log.error({ err }, `Failed to process message from ${msg.platform}`); }); }) .catch((err) => { - console.error(`[gateway] Incoming hook transform failed:`, err); + log.error({ err }, `Incoming hook transform failed`); }); }; @@ -546,7 +549,7 @@ export class Gateway { try { await this.runtime.reloadSlackWorkspaceMcps(); } catch (err) { - console.error("[gateway] Failed to reload workspace MCP servers:", err); + log.error({ err }, "Failed to reload workspace MCP servers"); } // Auto-set default notification channel if none exists @@ -561,14 +564,14 @@ export class Gateway { channelId: ws.user_id, label: `DM in ${ws.team_name}`, }); - console.log(`[gateway] Auto-set notification default: DM in ${ws.team_name}`); + log.info(`Auto-set notification default: DM in ${ws.team_name}`); } } catch { // Non-critical } if (changes.length > 0) { - console.log(`[gateway] Slack workspace sync: ${changes.join(", ")}`); + log.info(`Slack workspace sync: ${changes.join(", ")}`); } return changes; @@ -595,7 +598,7 @@ export class Gateway { const settingsDir = this.findSettingsDir(); if (!settingsDir) { - console.warn("[gateway] Settings directory not found — skipping Settings UI"); + log.warn("Settings directory not found — skipping Settings UI"); return; } @@ -604,7 +607,7 @@ export class Gateway { // Check if .next build exists const buildId = path.join(settingsDir, ".next", "BUILD_ID"); if (!fs.existsSync(buildId)) { - console.warn("[gateway] Settings UI not built — run `cd settings && pnpm build`"); + log.warn("Settings UI not built — run `cd settings && pnpm build`"); return; } @@ -633,7 +636,7 @@ export class Gateway { ]; const nextBin = nextBinCandidates.find((p) => fs.existsSync(p)); if (!nextBin) { - console.warn("[gateway] Next.js binary not found — skipping Settings UI"); + log.warn("Next.js binary not found — skipping Settings UI"); return; } child = spawn(nextBin, ["start", "--port", port], { @@ -645,29 +648,29 @@ export class Gateway { child.stdout?.on("data", (data: Buffer) => { const line = data.toString().trim(); - if (line) console.log(`[settings] ${line}`); + if (line) log.info(`[settings] ${line}`); }); child.stderr?.on("data", (data: Buffer) => { const line = data.toString().trim(); - if (line) console.error(`[settings] ${line}`); + if (line) log.error(`[settings] ${line}`); }); child.on("exit", (code, signal) => { if (this.settingsProcess === child) { - console.warn(`[gateway] Settings UI exited (code=${code}, signal=${signal})`); + log.warn(`Settings UI exited (code=${code}, signal=${signal})`); this.settingsProcess = null; } }); this.settingsProcess = child; - console.log(`[gateway] Settings UI starting on port ${port}`); + log.info(`Settings UI starting on port ${port}`); } /** Stop the Settings UI child process. */ private stopSettingsServer(): void { if (this.settingsProcess) { - console.log("[gateway] Stopping Settings UI..."); + log.info("Stopping Settings UI..."); this.settingsProcess.kill("SIGTERM"); this.settingsProcess = null; } @@ -675,19 +678,19 @@ export class Gateway { /** Verify LLM API access works before starting services. */ private async checkLlmAccess(): Promise { - console.log("[gateway] Checking LLM access..."); + log.info("Checking LLM access..."); const apiKey = process.env.ANTHROPIC_API_KEY; const baseUrl = process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com"; const useVertex = process.env.CLAUDE_CODE_USE_VERTEX === "1"; if (useVertex) { - console.log("[gateway] Using Vertex AI — skipping API key check"); + log.info("Using Vertex AI — skipping API key check"); return; } if (!apiKey) { - console.warn("[gateway] ⚠ No ANTHROPIC_API_KEY set — LLM calls will fail"); + log.warn("⚠ No ANTHROPIC_API_KEY set — LLM calls will fail"); return; } @@ -707,17 +710,17 @@ export class Gateway { }); if (res.ok) { - console.log("[gateway] LLM access verified"); + log.info("LLM access verified"); } else { const body = await res.text(); - console.error(`[gateway] LLM access check failed (${res.status}): ${body}`); - console.error("[gateway] Verify ANTHROPIC_API_KEY and model configuration in .env"); - console.warn("[gateway] ⚠ Daemon starting without verified LLM access"); + log.error(`LLM access check failed (${res.status}): ${body}`); + log.error("Verify ANTHROPIC_API_KEY and model configuration in .env"); + log.warn("⚠ Daemon starting without verified LLM access"); } } catch (err) { const message = err instanceof Error ? err.message : String(err); - console.error(`[gateway] LLM access check failed: ${message}`); - console.warn("[gateway] ⚠ Daemon starting without verified LLM access"); + log.error({ err: message }, `LLM access check failed`); + log.warn("⚠ Daemon starting without verified LLM access"); } } @@ -756,13 +759,13 @@ export class Gateway { // Fire-and-forget: index conversation turn into vector memory indexConversationTurn(msg, result).catch((err) => - console.error("[gateway] Memory indexing failed:", err), + log.error({ err }, "Memory indexing failed"), ); }) .catch(async (err) => { // Update placeholder with error if possible await responder?.finalize("Sorry, an error occurred."); - console.error(`[gateway] Failed to process message from ${msg.platform}:`, err); + log.error({ err }, `Failed to process message from ${msg.platform}`); }); } @@ -797,9 +800,7 @@ export class Gateway { // Observe mode: index without agent response if (adapter?.mode === "observe") { - observeMessage(msg).catch((err) => - console.error("[gateway] Observe indexing failed:", err), - ); + observeMessage(msg).catch((err) => log.error({ err }, "Observe indexing failed")); return; } @@ -812,7 +813,7 @@ export class Gateway { const consent = await getConsentMode(msg.platform); if (consent === "notify_only") { this.postNotifyOnlyToDefaultChannel(msg).catch((err) => - console.error("[gateway] Notify-only notification failed:", err), + log.error({ err }, "Notify-only notification failed"), ); return; // don't process through agent } @@ -831,7 +832,7 @@ export class Gateway { batcher.add(msg); }) .catch((err) => { - console.error(`[gateway] Incoming hook transform failed:`, err); + log.error({ err }, `Incoming hook transform failed`); }); }; @@ -849,7 +850,7 @@ export class Gateway { try { await syncSlackConfigToFile(); } catch (err) { - console.warn("[gateway] Failed to sync Slack config to file:", err); + log.warn({ err }, "Failed to sync Slack config to file"); } } @@ -894,9 +895,7 @@ export class Gateway { // when an xapp- token is available. Polling for everything else. if (isDefaultWorkspace && slackAppToken) { usingSocketMode = true; - console.log( - `[gateway] Using Socket Mode for workspace ${ws.team_id} (default channel)`, - ); + log.info(`Using Socket Mode for workspace ${ws.team_id} (default channel)`); const adapter = new SlackUserAdapter({ userToken: ws.access_token, appToken: slackAppToken, @@ -1109,12 +1108,12 @@ export class Gateway { entryScript = path.resolve(srcDir, "../index.js"); } if (!fs.existsSync(entryScript)) { - console.warn(`[gateway] Cannot find CLI entry script for ingestion subprocess`); + log.warn(`Cannot find CLI entry script for ingestion subprocess`); resolve(); return; } - console.log(`[gateway] Starting ingestion for ${label} (subprocess)...`); + log.info(`Starting ingestion for ${label} (subprocess)...`); this.broadcast({ type: "system", subtype: "ingest_start", @@ -1137,20 +1136,20 @@ export class Gateway { child.stdout?.on("data", (data: Buffer) => { for (const line of data.toString().split("\n")) { const trimmed = line.trim(); - if (trimmed) console.log(`[ingest:${subcommand}] ${trimmed}`); + if (trimmed) log.info(`[ingest:${subcommand}] ${trimmed}`); } }); child.stderr?.on("data", (data: Buffer) => { for (const line of data.toString().split("\n")) { const trimmed = line.trim(); - if (trimmed) console.error(`[ingest:${subcommand}] ${trimmed}`); + if (trimmed) log.error(`[ingest:${subcommand}] ${trimmed}`); } }); child.on("exit", (code) => { if (code === 0) { - console.log(`[gateway] Ingestion complete for ${label}`); + log.info(`Ingestion complete for ${label}`); this.broadcast({ type: "system", subtype: "ingest_complete", @@ -1160,7 +1159,7 @@ export class Gateway { // Re-register delta sync cron jobs in case a new full ingest completed registerDeltaSyncJobs().catch(() => {}); } else { - console.error(`[gateway] Ingestion for ${label} exited with code ${code}`); + log.error(`Ingestion for ${label} exited with code ${code}`); this.broadcast({ type: "system", subtype: "ingest_complete", @@ -1172,7 +1171,7 @@ export class Gateway { }); child.on("error", (err) => { - console.error(`[gateway] Failed to spawn ingestion for ${label}:`, err.message); + log.error({ err: err.message }, `Failed to spawn ingestion for ${label}`); this.broadcast({ type: "system", subtype: "ingest_complete", diff --git a/src/daemon/grpc-server.ts b/src/daemon/grpc-server.ts index 45e5ea1..995d5b7 100644 --- a/src/daemon/grpc-server.ts +++ b/src/daemon/grpc-server.ts @@ -16,6 +16,9 @@ import type { MessageQueue } from "./message-queue.ts"; import type { DraftManager } from "./draft-manager.ts"; import type { AgentEvent, IncomingMessage } from "./types.ts"; import { indexConversationTurn } from "./memory-indexer.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("grpc-server"); const __dirname = dirname(fileURLToPath(import.meta.url)); // In dev (tsx): __dirname = src/daemon/ → ../../proto works @@ -84,11 +87,11 @@ export class GrpcServer { grpc.ServerCredentials.createInsecure(), (err, boundPort) => { if (err) { - console.error("[grpc-server] Failed to bind:", err); + log.error({ err }, "Failed to bind"); reject(err); return; } - console.log(`[grpc-server] Listening on 0.0.0.0:${boundPort}`); + log.info(`Listening on 0.0.0.0:${boundPort}`); resolve(); }, ); @@ -131,7 +134,7 @@ export class GrpcServer { try { stream.call.write(payload); } catch (err) { - console.error(`[grpc-server] Failed to broadcast to stream ${stream.id}:`, err); + log.error({ err }, `Failed to broadcast to stream ${stream.id}`); } } } @@ -190,7 +193,7 @@ export class GrpcServer { .then((result) => { // Fire-and-forget: index conversation turn into vector memory indexConversationTurn(incoming, result).catch((err) => - console.error("[grpc-server] Memory indexing failed:", err), + log.error({ err }, "Memory indexing failed"), ); call.end(); this.activeStreams.delete(streamId); diff --git a/src/daemon/lifecycle.ts b/src/daemon/lifecycle.ts index 9ff9041..b1fbb53 100644 --- a/src/daemon/lifecycle.ts +++ b/src/daemon/lifecycle.ts @@ -5,6 +5,9 @@ import fs from "node:fs"; import path from "node:path"; import os from "node:os"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("lifecycle"); const PID_DIR = path.join(os.homedir(), ".nomos"); const PID_FILE = path.join(PID_DIR, "daemon.pid"); @@ -80,11 +83,11 @@ export function installSignalHandlers(onShutdown: () => Promise): void { if (shuttingDown) return; shuttingDown = true; - console.log(`[daemon] Received ${signal}, shutting down...`); + log.info(`Received ${signal}, shutting down...`); try { await onShutdown(); } catch (err) { - console.error("[daemon] Error during shutdown:", err); + log.error({ err }, "Error during shutdown"); } removePidFile(); process.exit(0); @@ -96,7 +99,7 @@ export function installSignalHandlers(onShutdown: () => Promise): void { // Handle uncaught errors process.on("uncaughtException", (err) => { - console.error("[daemon] Uncaught exception:", err); + log.error({ err }, "Uncaught exception"); removePidFile(); process.exit(1); }); @@ -107,6 +110,6 @@ export function installSignalHandlers(onShutdown: () => Promise): void { if (reason instanceof Error && reason.message.includes("ProcessTransport is not ready")) { return; } - console.error("[daemon] Unhandled rejection:", reason); + log.error({ err: reason }, "Unhandled rejection"); }); } diff --git a/src/daemon/memory-indexer.ts b/src/daemon/memory-indexer.ts index 2f7c42f..5e7d883 100644 --- a/src/daemon/memory-indexer.ts +++ b/src/daemon/memory-indexer.ts @@ -15,6 +15,9 @@ import { generateEmbeddings, isEmbeddingAvailable } from "../memory/embeddings.t import { storeMemoryChunk } from "../db/memory.ts"; import { loadEnvConfig } from "../config/env.ts"; import type { IncomingMessage, OutgoingMessage } from "./types.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("memory-indexer"); /** * Index a conversation turn (user message + agent response) into vector memory. @@ -51,7 +54,7 @@ export async function indexConversationTurn( try { embeddings = await generateEmbeddings(chunks.map((c) => c.text)); } catch (err) { - console.debug("[memory-indexer] Embedding generation failed, storing text-only:", err); + log.debug({ err }, "Embedding generation failed, storing text-only"); } } @@ -76,18 +79,18 @@ export async function indexConversationTurn( }); } - console.debug(`[memory-indexer] Indexed ${chunks.length} chunk(s) from ${sessionKey}`); + log.debug(`Indexed ${chunks.length} chunk(s) from ${sessionKey}`); // Adaptive memory: extract structured knowledge and score exemplars (fire-and-forget) const config = loadEnvConfig(); if (config.adaptiveMemory) { extractAndStoreKnowledgeFromTurn(incoming, outgoing, sessionKey).catch((err) => { - console.debug("[memory-indexer] Knowledge extraction failed:", err); + log.debug({ err }, "Knowledge extraction failed"); }); // Score user message as a potential exemplar for few-shot personality priming scoreExemplarFromTurn(incoming, sessionKey).catch((err) => { - console.debug("[memory-indexer] Exemplar scoring failed:", err); + log.debug({ err }, "Exemplar scoring failed"); }); } } diff --git a/src/daemon/message-batcher.ts b/src/daemon/message-batcher.ts index efb34a6..1a65de1 100644 --- a/src/daemon/message-batcher.ts +++ b/src/daemon/message-batcher.ts @@ -12,6 +12,9 @@ import type { IncomingMessage } from "./types.ts"; import { randomUUID } from "node:crypto"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("message-batcher"); /** Default debounce window: 8 seconds of silence before processing. */ const DEFAULT_DEBOUNCE_MS = 8_000; @@ -75,7 +78,10 @@ export class MessageBatcher { /** Immediately flush all pending buffers (e.g., on shutdown). */ flushAll(): void { - for (const key of [...this.buffers.keys()]) { + // Snapshot keys: flush() deletes from this.buffers, so iterating the live + // map would skip entries. + const keys = Array.from(this.buffers.keys()); + for (const key of keys) { this.flush(key); } } @@ -118,9 +124,7 @@ export class MessageBatcher { }, }; - console.log( - `[message-batcher] Combined ${messages.length} messages from ${first.userId} in ${first.channelId}`, - ); + log.info(`Combined ${messages.length} messages from ${first.userId} in ${first.channelId}`); this.onReady(combined); } diff --git a/src/daemon/message-hooks.ts b/src/daemon/message-hooks.ts index 1570293..4c34312 100644 --- a/src/daemon/message-hooks.ts +++ b/src/daemon/message-hooks.ts @@ -18,6 +18,9 @@ import fs from "node:fs"; import path from "node:path"; import os from "node:os"; import type { IncomingMessage, OutgoingMessage } from "./types.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("message-hooks"); export interface MessageHook { /** Unique hook name for logging and debugging. */ @@ -83,16 +86,14 @@ export class MessageHookPipeline { }; if (!hook.transformIncoming && !hook.transformOutgoing) { - console.warn( - `[hooks] Skipping ${entry}: no transformIncoming or transformOutgoing exported`, - ); + log.warn(`Skipping ${entry}: no transformIncoming or transformOutgoing exported`); continue; } this.hooks.push(hook); - console.log(`[hooks] Loaded: ${hook.name} (priority: ${hook.priority})`); + log.info(`Loaded: ${hook.name} (priority: ${hook.priority})`); } catch (err) { - console.error(`[hooks] Failed to load ${entry}:`, err); + log.error({ err }, `Failed to load ${entry}`); } } @@ -113,7 +114,7 @@ export class MessageHookPipeline { try { result = await hook.transformIncoming(result); } catch (err) { - console.error(`[hooks] ${hook.name}.transformIncoming failed:`, err); + log.error({ err }, `${hook.name}.transformIncoming failed`); // Continue with the unmodified message on error } } @@ -135,7 +136,7 @@ export class MessageHookPipeline { try { result = await hook.transformOutgoing(result); } catch (err) { - console.error(`[hooks] ${hook.name}.transformOutgoing failed:`, err); + log.error({ err }, `${hook.name}.transformOutgoing failed`); } } diff --git a/src/daemon/observer.ts b/src/daemon/observer.ts index ec161a2..dc2732c 100644 --- a/src/daemon/observer.ts +++ b/src/daemon/observer.ts @@ -9,6 +9,9 @@ import { indexConversationTurn } from "./memory-indexer.ts"; import type { IncomingMessage, OutgoingMessage } from "./types.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("observer"); /** * Process an observed message — store in memory without agent response. @@ -29,9 +32,9 @@ export async function observeMessage(message: IncomingMessage): Promise { try { await indexConversationTurn(message, syntheticOutgoing); } catch (err) { - console.warn( - `[observer] Failed to index observed message from ${message.platform}:`, - err instanceof Error ? err.message : err, + log.warn( + { err: err instanceof Error ? err.message : err }, + `Failed to index observed message from ${message.platform}`, ); } } diff --git a/src/daemon/proactive-sender.ts b/src/daemon/proactive-sender.ts index 5fa2115..bf7362d 100644 --- a/src/daemon/proactive-sender.ts +++ b/src/daemon/proactive-sender.ts @@ -7,6 +7,9 @@ */ import type { ChannelManager } from "./channel-manager.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("proactive-sender"); export interface ProactiveMessage { /** Target platform (e.g., "slack-user:T074HACEZ2L") */ @@ -29,7 +32,7 @@ export async function sendProactiveMessage( ): Promise { const adapter = channelManager.getAdapter(message.platform); if (!adapter) { - console.warn(`[proactive] No adapter for platform: ${message.platform}`); + log.warn(`No adapter for platform: ${message.platform}`); return false; } @@ -45,12 +48,12 @@ export async function sendProactiveMessage( content: message.content, }); } - console.log( - `[proactive] Message sent to ${message.platform}/${message.channelId} (${message.content.length} chars)`, + log.info( + `Message sent to ${message.platform}/${message.channelId} (${message.content.length} chars)`, ); return true; } catch (err) { - console.error("[proactive] Failed to send message:", err); + log.error({ err }, "Failed to send message"); return false; } } diff --git a/src/daemon/team-runtime.ts b/src/daemon/team-runtime.ts index 3531b1f..7e7e07c 100644 --- a/src/daemon/team-runtime.ts +++ b/src/daemon/team-runtime.ts @@ -24,6 +24,9 @@ import path from "node:path"; import { runSession, type McpServerConfig, type SdkPluginConfig } from "../sdk/session.ts"; import { getTeamMailbox } from "./team-mailbox.ts"; import { getTaskManager } from "./task-manager.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("team-runtime"); const execFileAsync = promisify(execFile); @@ -330,11 +333,9 @@ export class TeamRuntime { maxTurns: 2, plugins: task.plugins, }); - console.log( - `[team-runtime] Coordinator output (${output.length} chars): ${output.slice(0, 300)}`, - ); + log.info(`Coordinator output (${output.length} chars): ${output.slice(0, 300)}`); } catch (err) { - console.error("[team-runtime] Coordinator decomposition failed:", err); + log.error({ err }, "Coordinator decomposition failed"); // Fallback: treat entire task as single subtask return [{ id: randomUUID(), description: task.prompt }]; } @@ -343,13 +344,13 @@ export class TeamRuntime { // The coordinator may wrap it in ```json ... ``` fences or add text after. const descriptions = this.extractJsonArray(output); if (!descriptions) { - console.error( - `[team-runtime] Coordinator did not return valid JSON array. Output (${output.length} chars): ${output.slice(0, 500)}`, + log.error( + `Coordinator did not return valid JSON array. Output (${output.length} chars): ${output.slice(0, 500)}`, ); return [{ id: randomUUID(), description: task.prompt }]; } - console.log(`[team-runtime] Coordinator decomposed into ${descriptions.length} subtask(s)`); + log.info(`Coordinator decomposed into ${descriptions.length} subtask(s)`); return descriptions .slice(0, this.config.maxWorkers) .map((desc) => ({ id: randomUUID(), description: desc })); @@ -406,8 +407,9 @@ export class TeamRuntime { return parsed; } } catch (err) { - console.error( - `[team-runtime] Balanced bracket extraction failed: ${err instanceof Error ? err.message : err}`, + log.error( + { err: err instanceof Error ? err.message : err }, + `Balanced bracket extraction failed`, ); } break; @@ -643,9 +645,7 @@ Verify the workers' changes are correct. Run builds, tests, linters, and adversa plugins: task.plugins, }); - console.log( - `[team-runtime] Verification output (${output.length} chars): ${output.slice(0, 500)}`, - ); + log.info(`Verification output (${output.length} chars): ${output.slice(0, 500)}`); // Parse verdict — if verification didn't complete (no VERDICT line), treat as PASS // rather than penalizing with PARTIAL (the agent ran out of turns, not a real failure) @@ -669,7 +669,7 @@ Verify the workers' changes are correct. Run builds, tests, linters, and adversa checks, }; } catch (err) { - console.error("[team-runtime] Verification agent failed:", err); + log.error({ err }, "Verification agent failed"); return { verdict: "PARTIAL", summary: "Verification agent failed to complete", @@ -716,7 +716,7 @@ Verify the workers' changes are correct. Run builds, tests, linters, and adversa plugins: task.plugins, }); } catch (err) { - console.error("[team-runtime] Synthesis agent failed, returning raw results:", err); + log.error({ err }, "Synthesis agent failed, returning raw results"); // Fallback: return worker outputs directly return results .map((r, i) => { @@ -744,8 +744,8 @@ Verify the workers' changes are correct. Run builds, tests, linters, and adversa }, emit?: (event: { type: string; message: string }) => void, ): Promise { - console.log( - `[team-runtime] Running agent (model: ${options.model ?? "default"}${options.cwd ? `, cwd: ${options.cwd}` : ""})...`, + log.info( + `Running agent (model: ${options.model ?? "default"}${options.cwd ? `, cwd: ${options.cwd}` : ""})...`, ); const stderrChunks: string[] = []; @@ -766,7 +766,7 @@ Verify the workers' changes are correct. Run builds, tests, linters, and adversa if (trimmed) { stderrChunks.push(trimmed); if (stderrChunks.length > 50) stderrChunks.shift(); - console.error(`[team-runtime:stderr] ${trimmed}`); + log.error(`[stderr] ${trimmed}`); // Also write to file for debugging try { fs.appendFileSync(stderrLogFile, trimmed + "\n"); @@ -804,25 +804,23 @@ Verify the workers' changes are correct. Run builds, tests, linters, and adversa } } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); - console.error(`[team-runtime] Agent failed: ${errMsg}`); + log.error({ err: errMsg }, `Agent failed`); // If we captured text output before the process crashed, it likely // contains the real error message (e.g. "model not available"). // Return it instead of throwing a generic "exited with code 1". if (fullText.trim()) { - console.error( - `[team-runtime] Returning captured output (${fullText.length} chars) despite exit error`, - ); + log.error(`Returning captured output (${fullText.length} chars) despite exit error`); return fullText; } if (stderrChunks.length > 0) { - console.error(`[team-runtime] stderr output:\n${stderrChunks.join("\n")}`); + log.error(`stderr output:\n${stderrChunks.join("\n")}`); } throw err; } - console.log(`[team-runtime] Agent finished (${fullText.length} chars)`); + log.info(`Agent finished (${fullText.length} chars)`); return fullText; } } diff --git a/src/daemon/websocket-server.ts b/src/daemon/websocket-server.ts index 3e1b417..f0e9f7b 100644 --- a/src/daemon/websocket-server.ts +++ b/src/daemon/websocket-server.ts @@ -11,6 +11,9 @@ import type { IncomingMessage as HttpRequest } from "node:http"; import type { MessageQueue } from "./message-queue.ts"; import type { DraftManager } from "./draft-manager.ts"; import type { ClientMessage, AgentEvent, IncomingMessage } from "./types.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("websocket-server"); const HEARTBEAT_INTERVAL_MS = 30_000; @@ -40,12 +43,12 @@ export class DaemonWebSocketServer { this.wss = new WebSocketServer({ port: this.port }); this.wss.on("listening", () => { - console.log(`[ws-server] Listening on ws://localhost:${this.port}`); + log.info(`Listening on ws://localhost:${this.port}`); resolve(); }); this.wss.on("error", (err) => { - console.error("[ws-server] Server error:", err); + log.error({ err }, "Server error"); reject(err); }); @@ -57,7 +60,7 @@ export class DaemonWebSocketServer { this.heartbeatTimer = setInterval(() => { for (const client of this.clients.values()) { if (!client.alive) { - console.log(`[ws-server] Client ${client.id} timed out`); + log.info(`Client ${client.id} timed out`); client.ws.terminate(); this.clients.delete(client.id); continue; @@ -107,7 +110,7 @@ export class DaemonWebSocketServer { try { client.ws.send(data); } catch (err) { - console.error(`[ws-server] Failed to broadcast to client ${client.id}:`, err); + log.error({ err }, `Failed to broadcast to client ${client.id}`); } } } @@ -121,7 +124,7 @@ export class DaemonWebSocketServer { try { client.ws.send(JSON.stringify(event)); } catch (err) { - console.error(`[ws-server] Failed to send to client ${clientId}:`, err); + log.error({ err }, `Failed to send to client ${clientId}`); } } @@ -130,7 +133,7 @@ export class DaemonWebSocketServer { const client: ConnectedClient = { id: clientId, ws, alive: true }; this.clients.set(clientId, client); - console.log(`[ws-server] Client connected: ${clientId}`); + log.info(`Client connected: ${clientId}`); ws.on("pong", () => { client.alive = true; @@ -150,12 +153,12 @@ export class DaemonWebSocketServer { }); ws.on("close", () => { - console.log(`[ws-server] Client disconnected: ${clientId}`); + log.info(`Client disconnected: ${clientId}`); this.clients.delete(clientId); }); ws.on("error", (err) => { - console.error(`[ws-server] Client ${clientId} error:`, err); + log.error({ err }, `Client ${clientId} error`); this.clients.delete(clientId); }); diff --git a/src/db/encryption.ts b/src/db/encryption.ts index 201ea26..9dd5fe1 100644 --- a/src/db/encryption.ts +++ b/src/db/encryption.ts @@ -5,7 +5,7 @@ * Format: "iv_hex.ciphertext_hex.tag_hex" * * If ENCRYPTION_KEY is not set, secrets are stored as plaintext - * with a console.warn (dev convenience). + * with a warning (dev convenience). */ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"; @@ -13,6 +13,10 @@ import fs from "node:fs"; import path from "node:path"; import os from "node:os"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("encryption"); + const ALGORITHM = "aes-256-gcm"; const IV_LENGTH = 12; // 96-bit IV for GCM @@ -41,7 +45,7 @@ export function encrypt(plaintext: string): string { const key = getKey(); if (!key) { if (!warnedOnce) { - console.warn("[encryption] ENCRYPTION_KEY not set — secrets stored as plaintext"); + log.warn("ENCRYPTION_KEY not set, secrets stored as plaintext"); warnedOnce = true; } return plaintext; diff --git a/src/identity/auto-linker.ts b/src/identity/auto-linker.ts index 3b8e5e4..0c110fc 100644 --- a/src/identity/auto-linker.ts +++ b/src/identity/auto-linker.ts @@ -9,6 +9,9 @@ import { getDb } from "../db/client.ts"; import { mergeContacts } from "./contacts.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("auto-linker"); interface LinkCandidate { contact_id_a: string; @@ -107,8 +110,13 @@ export async function findLinkCandidates(autoMergeThreshold = 0.9): Promise { if (reason instanceof Error && reason.message.includes("ProcessTransport is not ready")) { return; } - console.error("Unhandled rejection:", reason); + log.error({ err: reason }, "Unhandled rejection"); process.exit(1); }); diff --git a/src/ingest/delta-sync.ts b/src/ingest/delta-sync.ts index 2d44f2c..f91b7b1 100644 --- a/src/ingest/delta-sync.ts +++ b/src/ingest/delta-sync.ts @@ -10,6 +10,9 @@ import { getKysely } from "../db/client.ts"; import { CronStore } from "../cron/store.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("delta-sync"); /** * Register delta sync cron jobs for all platforms with completed full ingests. @@ -41,7 +44,7 @@ export async function registerDeltaSyncJobs(): Promise { const schedule = job.delta_schedule || "6h"; if (existing.schedule !== schedule) { await store.updateJob(existing.id, { schedule }); - console.log(`[delta-sync] Updated schedule for ${cronName}: ${schedule}`); + log.info({ cronName, schedule }, "Updated schedule"); } continue; } @@ -57,9 +60,7 @@ export async function registerDeltaSyncJobs(): Promise { errorCount: 0, }); - console.log( - `[delta-sync] Registered cron job: ${cronName} every ${job.delta_schedule || "6h"}`, - ); + log.info({ cronName, schedule: job.delta_schedule || "6h" }, "Registered cron job"); } // Signal the cron engine to reload jobs diff --git a/src/ingest/scheduler.ts b/src/ingest/scheduler.ts index fae5404..2aea04f 100644 --- a/src/ingest/scheduler.ts +++ b/src/ingest/scheduler.ts @@ -10,6 +10,9 @@ */ import { getIngestJobByPlatform, getLastCompletedJob } from "./pipeline.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("ingest-scheduler"); /** Default lookback for auto-triggered full ingests (30 days). */ const DEFAULT_LOOKBACK_DAYS = 30; @@ -58,9 +61,7 @@ export class IngestScheduler { const since = new Date(); since.setDate(since.getDate() - DEFAULT_LOOKBACK_DAYS); - console.log( - `[ingest-scheduler] Starting full ingest for ${platform} (last ${DEFAULT_LOOKBACK_DAYS} days)`, - ); + log.info({ platform, lookbackDays: DEFAULT_LOOKBACK_DAYS }, "Starting full ingest"); await this.spawnFn( subcommand, ["--run-type", "full", "--since", since.toISOString()], @@ -104,7 +105,7 @@ export class IngestScheduler { extraArgs.push("--since", new Date(lastCompleted.last_successful_at).toISOString()); } - console.log(`[ingest-scheduler] Starting delta sync for ${platform}`); + log.info({ platform }, "Starting delta sync"); await this.spawnFn(subcommand, extraArgs, `${platform} (delta)`); // Clear dedupe after completion so cron-triggered deltas still work later @@ -162,7 +163,7 @@ export class IngestScheduler { private enqueue(task: () => Promise): void { this.ingestQueue = this.ingestQueue.then(task).catch((err) => { - console.error("[ingest-scheduler] Task failed:", err); + log.error({ err }, "Task failed"); }); } } diff --git a/src/ingest/sources/gmail.ts b/src/ingest/sources/gmail.ts index 1140ee5..177c899 100644 --- a/src/ingest/sources/gmail.ts +++ b/src/ingest/sources/gmail.ts @@ -13,6 +13,9 @@ import { GoogleAuth, OAuth2Client } from "google-auth-library"; import { execFile } from "node:child_process"; import type { IngestSource, IngestMessage, IngestOptions } from "../types.ts"; +import { createLogger } from "../../lib/logger.ts"; + +const log = createLogger("ingest-gmail"); const PAGE_DELAY_MS = 200; // Gmail: 250 quota units/sec const MAX_RESULTS = 100; @@ -43,30 +46,51 @@ export class GmailIngestSource implements IngestSource { /** * Get an access token for Gmail API. - * Priority: gws CLI tokens > Application Default Credentials (gcloud/service account). + * + * Priority: + * 1. gws CLI tokens (the common Google Workspace OAuth path). + * 2. Application Default Credentials — ONLY when gws is not set up + * at all. ADC tokens from `gcloud auth application-default login` + * do not include Gmail scope, so falling back to ADC after a gws + * failure just hides the real problem (you get a 403 + * ACCESS_TOKEN_SCOPE_INSUFFICIENT on the first Gmail call). */ private async getAccessToken(): Promise { - // 1. Try gws CLI (Google Workspace auth) - try { - const creds = await exportGwsCredentials(); - if (creds) { - console.log("[ingest:gmail] Using gws CLI credentials for Gmail access"); + const creds = await exportGwsCredentials(); + + if (creds) { + // gws is configured — its tokens are the only path that has Gmail + // scope. If refresh fails here, do NOT fall back to ADC; surface + // the real error so the user can re-authorize. + log.info("Using gws CLI credentials for Gmail access"); + try { const oauth2 = new OAuth2Client(creds.client_id, creds.client_secret); oauth2.setCredentials({ refresh_token: creds.refresh_token }); const { token } = await oauth2.getAccessToken(); if (token) return token; - console.warn("[ingest:gmail] gws token refresh returned no token"); - } else { - console.warn("[ingest:gmail] gws auth export returned no credentials"); + throw new Error("gws token refresh returned no token"); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const isInvalidClient = /invalid_client/i.test(message); + const isInvalidGrant = /invalid_grant/i.test(message); + const hint = isInvalidClient + ? "the gws keyring's refresh token was issued against a different OAuth client than the one currently in client_secret.json (usually after the client credentials were rotated or rewritten)" + : isInvalidGrant + ? "the refresh token is expired or has been revoked" + : "the gws OAuth refresh failed"; + throw new Error( + `Gmail auth failed: ${message}. Cause: ${hint}. ` + + 'Fix: in Settings UI → Integrations → Google, click "Remove" on the authorized account then "Authorize Account". ' + + "Or from a shell: `npx @googleworkspace/cli auth logout` then re-run the OAuth flow.", + ); } - } catch (err) { - console.warn( - "[ingest:gmail] gws auth failed, falling back to ADC:", - err instanceof Error ? err.message : err, - ); } - // 2. Fall back to Application Default Credentials (Vertex AI / service account) + // gws is not set up — try Application Default Credentials. This path + // only works if the user has explicitly granted Gmail scope to ADC + // (uncommon — `gcloud auth application-default login` gives only + // cloud-platform scope by default). If you see a 403 + // ACCESS_TOKEN_SCOPE_INSUFFICIENT after this, use gws instead. const auth = new GoogleAuth({ scopes: ["https://www.googleapis.com/auth/gmail.readonly"], }); @@ -74,8 +98,8 @@ export class GmailIngestSource implements IngestSource { const tokenResponse = await client.getAccessToken(); if (!tokenResponse.token) { throw new Error( - "Failed to obtain Gmail access token. Either authorize via Settings > Google Workspace, " + - "or run: gcloud auth application-default login", + "No Gmail access token available. Authorize via Settings UI → Integrations → Google " + + "(this uses the gws CLI under the hood and is the recommended path).", ); } return tokenResponse.token; @@ -249,7 +273,7 @@ function exportGwsCredentials(): Promise { { timeout: 15_000 }, (err, stdout, stderr) => { if (err) { - console.warn("[ingest:gmail] gws auth export failed:", err.message, stderr?.trim()); + log.warn({ err: err.message, stderr: stderr?.trim() }, "gws auth export failed"); resolve(null); return; } diff --git a/src/ingest/sources/slack.ts b/src/ingest/sources/slack.ts index fe14aac..f64f1e9 100644 --- a/src/ingest/sources/slack.ts +++ b/src/ingest/sources/slack.ts @@ -14,6 +14,9 @@ import { WebClient } from "@slack/web-api"; import { listWorkspaces } from "../../db/slack-workspaces.ts"; import type { IngestSource, IngestMessage, IngestOptions } from "../types.ts"; +import { createLogger } from "../../lib/logger.ts"; + +const log = createLogger("ingest-slack"); /** Delay between search pages (Tier 2: 20 req/min, we target ~6 req/min). */ const PAGE_DELAY_MS = 10_000; @@ -47,7 +50,7 @@ export class SlackIngestSource implements IngestSource { query += ` after:${dateStr}`; } - console.log(`[slack-ingest] Searching: ${query}`); + log.info({ query }, "Searching"); let page = 1; let totalPages = 1; @@ -65,7 +68,7 @@ export class SlackIngestSource implements IngestSource { if (!matches || matches.length === 0) break; totalPages = result.messages?.paging?.pages ?? 1; - console.log(`[slack-ingest] Page ${page}/${totalPages} -- ${matches.length} messages`); + log.info({ page, totalPages, messages: matches.length }, "Page"); for (const match of matches) { if (!match.text) continue; diff --git a/src/lib/logger.ts b/src/lib/logger.ts new file mode 100644 index 0000000..d9e5ef5 --- /dev/null +++ b/src/lib/logger.ts @@ -0,0 +1,102 @@ +/** + * Centralized logging utility using Pino + * + * Features: + * - Pretty output in development, JSON in production + * - Log level controlled via LOG_LEVEL env var + * - Child loggers with context (module, orgId, userId) + * + * Usage: + * import { logger } from "../lib/logger.ts"; + * logger.info("Hello world"); + * logger.error({ err }, "Something went wrong"); + * + * // With context + * import { createLogger } from "../lib/logger.ts"; + * const log = createLogger("cron-engine"); + * log.info({ jobId: "job_123" }, "Triggering job"); + */ + +import pino, { type Logger, type LoggerOptions } from "pino"; + +type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal" | "silent"; + +function getLogLevel(): LogLevel { + const envLevel = process.env.LOG_LEVEL?.toLowerCase() as LogLevel | undefined; + const validLevels: LogLevel[] = ["trace", "debug", "info", "warn", "error", "fatal", "silent"]; + + if (envLevel && validLevels.includes(envLevel)) { + return envLevel; + } + + return process.env.NODE_ENV === "production" ? "info" : "debug"; +} + +const isDevelopment = process.env.NODE_ENV !== "production"; + +const baseOptions: LoggerOptions = { + level: getLogLevel(), + timestamp: pino.stdTimeFunctions.isoTime, + formatters: { + level: (label) => ({ level: label }), + bindings: (bindings) => ({ + pid: bindings.pid, + hostname: bindings.hostname, + }), + }, + serializers: { + err: pino.stdSerializers.err, + error: pino.stdSerializers.err, + }, +}; + +function createBaseLogger(): Logger { + if (isDevelopment) { + return pino({ + ...baseOptions, + transport: { + target: "pino-pretty", + options: { + colorize: true, + translateTime: "HH:MM:ss", + ignore: "pid,hostname", + singleLine: false, + }, + }, + }); + } + + return pino(baseOptions); +} + +export const logger = createBaseLogger(); + +/** + * Create a child logger with a module name. + * @param module - The module/component name (e.g., "cron-engine", "channel-manager") + */ +export function createLogger(module: string): Logger { + return logger.child({ module }); +} + +/** + * Create a request-scoped logger with user context. + */ +export function createRequestLogger( + module: string, + context: { + userId?: string; + orgId?: string; + requestId?: string; + [key: string]: unknown; + }, +): Logger { + return logger.child({ + module, + ...context, + }); +} + +export type { Logger, LogLevel }; + +export default logger; diff --git a/src/memory/auto-dream.ts b/src/memory/auto-dream.ts index 8b0f27f..354e175 100644 --- a/src/memory/auto-dream.ts +++ b/src/memory/auto-dream.ts @@ -18,6 +18,9 @@ import { readFile, writeFile, unlink, mkdir } from "node:fs/promises"; import { join } from "node:path"; import { homedir } from "node:os"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("auto-dream"); /** Minimum time between consolidation runs (1 hour). */ const MIN_INTERVAL_MS = 60 * 60 * 1000; @@ -214,12 +217,12 @@ export async function autoDream( // Acquire lock const locked = await acquireLock(); if (!locked) { - console.log("[auto-dream] Another consolidation is in progress, skipping"); + log.info("Another consolidation is in progress, skipping"); return null; } try { - console.log("[auto-dream] Starting background consolidation..."); + log.info("Starting background consolidation..."); const start = Date.now(); const result = await runConsolidation(); @@ -232,13 +235,19 @@ export async function autoDream( state.totalRuns += 1; await saveState(state); - console.log( - `[auto-dream] Consolidation complete: ${result.newChunks} new, ${result.merged} merged, ${result.pruned} pruned (${result.durationMs}ms)`, + log.info( + { + newChunks: result.newChunks, + merged: result.merged, + pruned: result.pruned, + durationMs: result.durationMs, + }, + "Consolidation complete", ); return result; } catch (err) { - console.error("[auto-dream] Consolidation failed:", err); + log.error({ err }, "Consolidation failed"); return null; } finally { await releaseLock(); @@ -355,8 +364,14 @@ export async function reRankValues(reflection: { } if (result.boosted + result.decreased + result.added + result.refined > 0) { - console.log( - `[auto-dream] Value re-ranking: ${result.boosted} boosted, ${result.decreased} decreased, ${result.added} new, ${result.refined} refined`, + log.info( + { + boosted: result.boosted, + decreased: result.decreased, + added: result.added, + refined: result.refined, + }, + "Value re-ranking", ); } diff --git a/src/memory/consolidator.ts b/src/memory/consolidator.ts index 3f33252..38c5456 100644 --- a/src/memory/consolidator.ts +++ b/src/memory/consolidator.ts @@ -13,6 +13,9 @@ import { sql, type SqlBool } from "kysely"; import { getKysely } from "../db/client.ts"; import { loadEnvConfig } from "../config/env.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("consolidator"); export interface ConsolidationResult { merged: number; @@ -322,14 +325,12 @@ async function llmConsolidate(): Promise { } if (changeCount > 0) { - console.log( - `[consolidator] LLM review: ${changeCount} changes from ${decisions.length} decisions`, - ); + log.info({ changeCount, decisionsCount: decisions.length }, "LLM review"); } return changeCount; } catch (err) { - console.debug("[consolidator] LLM consolidation failed:", err); + log.debug({ err }, "LLM consolidation failed"); return 0; } } diff --git a/src/memory/exemplars.ts b/src/memory/exemplars.ts index a22a99f..3d861df 100644 --- a/src/memory/exemplars.ts +++ b/src/memory/exemplars.ts @@ -11,6 +11,9 @@ import { storeMemoryChunk, searchMemoryByVector, searchMemoryByText } from "../d import { generateEmbedding, isEmbeddingAvailable } from "./embeddings.ts"; import { runSession } from "../sdk/session.ts"; import { loadEnvConfig } from "../config/env.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("exemplars"); /** Context tags for exemplar classification. */ export type ExemplarContext = @@ -186,9 +189,7 @@ export async function scoreAndStoreExemplar( if (scored) { await storeExemplar(scored, platform, sessionKey); if (scored.score >= 0.6) { - console.debug( - `[exemplars] Stored exemplar (score: ${scored.score}, context: ${scored.context})`, - ); + log.debug({ score: scored.score, context: scored.context }, "Stored exemplar"); } } } @@ -234,7 +235,7 @@ export async function retrieveExemplars( platform: ((r.metadata as Record)?.platform as string) ?? "unknown", })); } catch (err) { - console.debug("[exemplars] Retrieval failed:", err); + log.debug({ err }, "Retrieval failed"); return []; } } diff --git a/src/memory/extractor.ts b/src/memory/extractor.ts index 9ec5e07..646266a 100644 --- a/src/memory/extractor.ts +++ b/src/memory/extractor.ts @@ -11,6 +11,9 @@ import { runSession } from "../sdk/session.ts"; import { storeMemoryChunk } from "../db/memory.ts"; import { generateEmbedding, isEmbeddingAvailable } from "./embeddings.ts"; import { loadEnvConfig } from "../config/env.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("extractor"); export interface ExtractedFact { text: string; @@ -150,7 +153,7 @@ export async function extractKnowledge( values: Array.isArray(parsed.values) ? parsed.values : [], }; } catch (err) { - console.debug("[extractor] Knowledge extraction failed:", err); + log.debug({ err }, "Knowledge extraction failed"); return empty; } } @@ -328,7 +331,7 @@ export async function extractAndStoreKnowledge( } if (chunkIds.length > 0) { - console.debug(`[extractor] Stored ${chunkIds.length} knowledge chunk(s) from ${sessionKey}`); + log.debug({ count: chunkIds.length, sessionKey }, "Stored knowledge chunks"); } return { knowledge, chunkIds }; diff --git a/src/memory/knowledge-compiler.ts b/src/memory/knowledge-compiler.ts index bb4dcff..4d89eb7 100644 --- a/src/memory/knowledge-compiler.ts +++ b/src/memory/knowledge-compiler.ts @@ -24,6 +24,9 @@ import { runForkedAgent } from "../sdk/forked-agent.ts"; import { upsertArticle, getArticle, listArticles } from "../db/wiki.ts"; import { syncToDisk } from "./wiki-sync.ts"; import { syncFileToDb } from "../config/file-sync.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("knowledge-compiler"); const LOCK_FILE = path.join(homedir(), ".nomos", "wiki-compiler.lock"); const WIKI_DIR = path.join(homedir(), ".nomos", "wiki"); @@ -102,11 +105,7 @@ export async function compileKnowledge(options?: { force?: boolean }): Promise a.path), - ); + const knowledgeSummary = buildKnowledgeSummary(entries, contactRows); const planResult = await runForkedAgent({ prompt: `You are a knowledge wiki curator. Based on the user's accumulated knowledge, decide which wiki articles to create or update. @@ -144,11 +143,11 @@ Maximum ${MAX_ARTICLES_PER_RUN} articles. Return [] if nothing is worth compilin } if (plans.length === 0) { - console.log("[knowledge-compiler] No articles to compile"); + log.info("No articles to compile"); return result; } - console.log(`[knowledge-compiler] Planning ${plans.length} article(s)`); + log.info({ count: plans.length }, "Planning articles"); // 3. Compile each article for (const plan of plans) { @@ -199,9 +198,7 @@ Maximum ${MAX_ARTICLES_PER_RUN} articles. Return [] if nothing is worth compilin // 6. Backup wiki articles to managed_files table await backupWikiToDb(); - console.log( - `[knowledge-compiler] Done: ${result.articlesCreated} created, ${result.articlesUpdated} updated`, - ); + log.info({ created: result.articlesCreated, updated: result.articlesUpdated }, "Done"); } finally { if (fs.existsSync(LOCK_FILE)) { fs.writeFileSync(LOCK_FILE, new Date().toISOString()); @@ -309,7 +306,6 @@ async function backupWikiToDb(): Promise { function buildKnowledgeSummary( entries: Array<{ category: string; key: string; value: string; confidence: number }>, contacts: Array<{ id: string; name: string; identities: unknown[] }>, - existingPaths: string[], ): string { const lines: string[] = []; diff --git a/src/memory/theory-of-mind.ts b/src/memory/theory-of-mind.ts index 683d2fb..d94e8d9 100644 --- a/src/memory/theory-of-mind.ts +++ b/src/memory/theory-of-mind.ts @@ -16,6 +16,9 @@ */ import type { ForkedAgentResult } from "../sdk/forked-agent.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("theory-of-mind"); // ── Types ── @@ -217,10 +220,7 @@ export class TheoryOfMindTracker { } }) .catch((err) => { - console.warn( - "[theory-of-mind] LLM assessment failed:", - err instanceof Error ? err.message : err, - ); + log.warn({ err: err instanceof Error ? err.message : err }, "LLM assessment failed"); }) .finally(() => { this.llmInFlight = false; diff --git a/src/memory/wiki-sync.ts b/src/memory/wiki-sync.ts index f15ee6e..1d3ad7c 100644 --- a/src/memory/wiki-sync.ts +++ b/src/memory/wiki-sync.ts @@ -9,6 +9,9 @@ import fs from "node:fs"; import path from "node:path"; import { homedir } from "node:os"; import { listArticles, upsertArticle } from "../db/wiki.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("wiki-sync"); const WIKI_DIR = path.join(homedir(), ".nomos", "wiki"); @@ -27,7 +30,7 @@ export async function syncToDisk(): Promise { count++; } - console.log(`[wiki-sync] Synced ${count} articles to disk`); + log.info({ count }, "Synced articles to disk"); return count; } @@ -51,7 +54,7 @@ export async function syncToDb(): Promise { } if (count > 0) { - console.log(`[wiki-sync] Synced ${count} files from disk to DB`); + log.info({ count }, "Synced files from disk to DB"); } return count; } @@ -63,7 +66,7 @@ export async function reconcileOnStartup(): Promise { // No articles in DB — try loading from disk const diskCount = await syncToDb(); if (diskCount > 0) { - console.log("[wiki-sync] Loaded wiki from disk into DB"); + log.info("Loaded wiki from disk into DB"); } return; } diff --git a/src/plugins/installer.ts b/src/plugins/installer.ts index ba05b85..6e9ebef 100644 --- a/src/plugins/installer.ts +++ b/src/plugins/installer.ts @@ -13,7 +13,6 @@ import { readInstalledManifest } from "./loader.ts"; const PLUGINS_DIR = join(homedir(), ".nomos", "plugins"); const MANIFEST_PATH = join(PLUGINS_DIR, "installed.json"); -const MARKETPLACE_BASE = join(homedir(), ".claude", "plugins", "marketplaces"); const KNOWN_MARKETPLACES_PATH = join(homedir(), ".claude", "plugins", "known_marketplaces.json"); interface KnownMarketplaces { diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index c1d9641..34a956e 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -1,5 +1,5 @@ import { existsSync } from "node:fs"; -import { readdir, readFile } from "node:fs/promises"; +import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { homedir } from "node:os"; import type { LoadedPlugin, PluginManifest, InstalledManifest, SdkPluginConfig } from "./types.ts"; diff --git a/src/proactive/scheduler.ts b/src/proactive/scheduler.ts index 26ae0f8..02f4b72 100644 --- a/src/proactive/scheduler.ts +++ b/src/proactive/scheduler.ts @@ -23,6 +23,9 @@ import { calendarScanJobSpec } from "./calendar-watcher.ts"; import { morningBriefingJobSpec, DEFAULT_BRIEFING_CRON } from "./morning-briefing.ts"; import { loadEnvConfigAsync } from "../config/env.ts"; import { getNotificationDefault } from "../db/notification-defaults.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("proactive-scheduler"); const INBOX_JOB_NAME = "proactive:inbox-watcher"; const CALENDAR_JOB_NAME = "proactive:calendar-watcher"; @@ -51,7 +54,7 @@ export async function registerProactiveJobs(): Promise { if (existing?.enabled) { await store.updateJob(existing.id, { enabled: false }); changed = true; - console.log(`[proactive] Disabled ${name} (inbox autonomy is off)`); + log.info({ name }, "Disabled job (inbox autonomy is off)"); } } if (changed) process.emit("cron:refresh" as never); @@ -59,8 +62,8 @@ export async function registerProactiveJobs(): Promise { } if (!target) { - console.warn( - "[proactive] No default notification channel configured — proactive jobs not registered. Set one via the Settings UI.", + log.warn( + "No default notification channel configured — proactive jobs not registered. Set one via the Settings UI.", ); return; } @@ -95,7 +98,7 @@ async function upsertJob( enabled: true, errorCount: 0, }); - console.log(`[proactive] Registered ${spec.name} (${spec.schedule})`); + log.info({ name: spec.name, schedule: spec.schedule }, "Registered job"); return true; } @@ -112,7 +115,7 @@ async function upsertJob( if (Object.keys(updates).length === 0) return false; await store.updateJob(existing.id, updates); - console.log(`[proactive] Updated ${spec.name} (${Object.keys(updates).join(", ")})`); + log.info({ name: spec.name, updates: Object.keys(updates) }, "Updated job"); return true; } @@ -134,7 +137,7 @@ export async function runCommitmentReminders(): Promise<{ }) .join("\n"); - console.log(`[proactive] Commitment reminders:\n${reminders}`); + log.info(`Commitment reminders:\n${reminders}`); await markReminded(due.map((c) => c.id)); } @@ -179,6 +182,6 @@ export async function runTriageDigest(): Promise { } const summary = lines.join("\n"); - console.log(`[proactive] ${summary}`); + log.info(summary); return summary; } diff --git a/src/sdk/cache-break-detection.ts b/src/sdk/cache-break-detection.ts index 623a53b..7f7f31f 100644 --- a/src/sdk/cache-break-detection.ts +++ b/src/sdk/cache-break-detection.ts @@ -10,6 +10,9 @@ */ import { createHash } from "node:crypto"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("cache-break-detection"); /** * Components tracked for cache break detection. @@ -86,7 +89,7 @@ export class PromptCacheTracker { this.breakCount += 1; const summary = `Cache break #${this.breakCount}: ${changes.join(", ")} changed`; - console.warn(`[cache-tracker] ${summary}`); + log.warn({ breakCount: this.breakCount, changes }, summary); return { broken: true, changes, summary }; } diff --git a/src/sdk/forked-agent.ts b/src/sdk/forked-agent.ts index 844b573..02b4537 100644 --- a/src/sdk/forked-agent.ts +++ b/src/sdk/forked-agent.ts @@ -11,6 +11,9 @@ import { runSession, type RunSessionParams } from "./session.ts"; import { getCostTracker } from "./cost-tracker.ts"; +import { createLogger } from "../lib/logger.ts"; + +const log = createLogger("forked-agent"); /** Default model for background subagents (cheap + fast). */ const DEFAULT_FORK_MODEL = "claude-haiku-4-5"; @@ -60,7 +63,7 @@ export async function runForkedAgent(options: ForkedAgentOptions): Promise { const trimmed = data.trim(); - if (trimmed) console.error(`[forked-agent:stderr:${label}] ${trimmed}`); + if (trimmed) log.error({ label, stream: "stderr" }, trimmed); }, }; @@ -119,8 +122,9 @@ export async function runForkedAgent(options: ForkedAgentOptions): Promise 0) { skillsWithMissingDeps.push({ skill, missing }); - console.log(`❌ ${skill.name}: missing ${missing.join(", ")}`); + log.info({ skill: skill.name, missing }, `❌ ${skill.name}: missing ${missing.join(", ")}`); if (available.length > 0) { - console.log(` ✅ available: ${available.join(", ")}`); + log.info({ available }, ` ✅ available: ${available.join(", ")}`); } if (skill.install) { - console.log(` 📦 install: ${skill.install.join(" && ")}`); + log.info({ install: skill.install }, ` 📦 install: ${skill.install.join(" && ")}`); } - console.log(); + log.info(""); } } if (skillsWithMissingDeps.length === 0) { - console.log("✅ All skill dependencies are satisfied!\n"); + log.info("✅ All skill dependencies are satisfied!\n"); return; } - console.log(`Found ${skillsWithMissingDeps.length} skill(s) with missing dependencies.\n`); + log.info( + { count: skillsWithMissingDeps.length }, + `Found ${skillsWithMissingDeps.length} skill(s) with missing dependencies.\n`, + ); if (checkOnly) { - console.log("Check complete (use --install to install).\n"); + log.info("Check complete (use --install to install).\n"); return; } if (!doInstall) { - console.log("Run with --install to install missing dependencies.\n"); + log.info("Run with --install to install missing dependencies.\n"); return; } - console.log("Installing missing dependencies...\n"); + log.info("Installing missing dependencies...\n"); for (const { skill, missing } of skillsWithMissingDeps) { - console.log(`Installing ${skill.name} (${missing.join(", ")})...`); + log.info({ skill: skill.name, missing }, `Installing ${skill.name} (${missing.join(", ")})...`); const result = await installer.install(skill); if (result.success) { - console.log(`✅ Success\n`); + log.info(`✅ Success\n`); } else { - console.log(`❌ Failed:\n${result.output}\n`); + log.info({ output: result.output }, `❌ Failed:\n${result.output}\n`); } } - console.log("Installation complete.\n"); + log.info("Installation complete.\n"); } // Run CLI if executed directly if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { - console.error("Error:", error); + log.error({ err: error }, "Error"); process.exit(1); }); }