From 22fb4b26b55b5f4f2e78b036fbd3af90e010e8d6 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 19 May 2026 02:12:10 -0400 Subject: [PATCH 1/7] feat: persist logs across deploys, prune dangling Docker images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each deploy recreates the vault-mcp container, destroying the previous container's log stream. Add a file sink extension to the logger that writes date-stamped log files (vault-mcp-YYYY-MM-DD.log) to /data/logs on the existing persistent volume. Files are pruned after 30 days (configurable via LOG_RETENTION_DAYS). Also add `docker image prune -f` to both deploy paths to prevent dangling images from accumulating (was consuming ~10 GB on the Lightsail instance). - Logger file sink uses the existing extensions mechanism — child loggers inherit it automatically - Luxon DateTime replaces all native Date usage in logger.ts - Add Luxon convention to AGENTS.md code style section - 9 new tests for file sink, date rollover, and retention pruning Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 4 + .github/workflows/deploy.yml | 2 +- AGENTS.md | 6 + deploy/local/.env.example | 3 + deploy/local/docker-compose.yml | 2 + deploy/remote/.env.example | 3 + deploy/remote/docker-compose.yml | 2 + docker-compose.yml | 2 + scripts/dev.ts | 2 +- src/__tests__/logger.test.ts | 185 +++++++++++++++++++++++++++++++ src/logger.ts | 76 ++++++++++++- 11 files changed, 281 insertions(+), 6 deletions(-) create mode 100644 src/__tests__/logger.test.ts diff --git a/.env.example b/.env.example index 46f9f50..57bba1a 100644 --- a/.env.example +++ b/.env.example @@ -42,6 +42,10 @@ DEVICE_NAME=vault-cortex-lightsail CONFLICT_STRATEGY=merge SYNC_MODE=bidirectional LOG_LEVEL=info +# Directory for persistent log files (survives container restarts). Default: unset (stdout only). +# LOG_DIR=/data/logs +# Days to retain log files before cleanup (default: 30). +# LOG_RETENTION_DAYS=30 TZ=UTC # AWS region — only used by SST deploys (sst.config.ts). Ignored by Docker containers. diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0e4d78e..7f24b78 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -172,7 +172,7 @@ jobs: IP: ${{ steps.ssh.outputs.host }} run: | ssh -o StrictHostKeyChecking=accept-new ubuntu@"$IP" \ - 'cd /opt/vault-cortex && docker compose pull && docker compose up -d --wait --wait-timeout 180' + 'cd /opt/vault-cortex && docker compose pull && docker compose up -d --wait --wait-timeout 180 && docker image prune -f' - name: Healthcheck env: diff --git a/AGENTS.md b/AGENTS.md index 4df55a0..a26799f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,6 +145,12 @@ search.fullTextSearch({ query, filters }, reqLogger) - TypeScript strict mode. `node:` prefix for built-ins. - Explicit return types on exports. Zod for MCP tool schemas. - No `any`. Prefer `async/await` over `.then()`/`.catch()`. +- Luxon `DateTime` over the native `Date` API. Luxon is declarative + (`DateTime.now().minus({ days: 7 }).toISODate()`), immutable, and + avoids manual arithmetic (`Date.now() - 7 * 86_400_000`) and + mutation (`date.setDate()`). Use `DateTime.now()` for current time, + `.toISO()` for timestamps, `.toISODate()` for date-only strings, + `.toUnixInteger()` for epoch seconds. - Immutable by default. Avoid `let` — carry state in reduce accumulators, use early returns, or destructure conditional results. A bit of duplication is acceptable to keep code immutable and clear. diff --git a/deploy/local/.env.example b/deploy/local/.env.example index 6caabf5..8095f9c 100644 --- a/deploy/local/.env.example +++ b/deploy/local/.env.example @@ -24,3 +24,6 @@ VAULT_PATH= # Log verbosity: debug | info | warn | error (default: info). # LOG_LEVEL=info + +# Days to retain persistent log files before cleanup (default: 30). +# LOG_RETENTION_DAYS=30 diff --git a/deploy/local/docker-compose.yml b/deploy/local/docker-compose.yml index 6a2d90e..985843f 100644 --- a/deploy/local/docker-compose.yml +++ b/deploy/local/docker-compose.yml @@ -25,6 +25,8 @@ services: PUBLIC_URL: ${PUBLIC_URL:-http://localhost:8000} MEMORY_DIR: ${MEMORY_DIR:-About Me} LOG_LEVEL: ${LOG_LEVEL:-info} + LOG_DIR: /data/logs + LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-} TZ: ${TZ:-UTC} # Uncomment to override smart defaults (see Configuration in the main README): # PROTECTED_PATHS: ${PROTECTED_PATHS:-} diff --git a/deploy/remote/.env.example b/deploy/remote/.env.example index 3352d3f..5d1df55 100644 --- a/deploy/remote/.env.example +++ b/deploy/remote/.env.example @@ -37,6 +37,9 @@ VAULT_NAME= # Log verbosity: debug | info | warn | error (default: info). # LOG_LEVEL=info +# Days to retain persistent log files before cleanup (default: 30). +# LOG_RETENTION_DAYS=30 + # User/group IDs for obsidian-sync (default: 1000). # PUID=1000 # PGID=1000 diff --git a/deploy/remote/docker-compose.yml b/deploy/remote/docker-compose.yml index 491d353..6500c95 100644 --- a/deploy/remote/docker-compose.yml +++ b/deploy/remote/docker-compose.yml @@ -69,6 +69,8 @@ services: PUBLIC_URL: "${PUBLIC_URL:?Set PUBLIC_URL to your server's public URL}" MEMORY_DIR: ${MEMORY_DIR:-About Me} LOG_LEVEL: ${LOG_LEVEL:-info} + LOG_DIR: /data/logs + LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-} TZ: ${TZ:-UTC} # Uncomment to override smart defaults (see Configuration in the main README): # PROTECTED_PATHS: ${PROTECTED_PATHS:-} diff --git a/docker-compose.yml b/docker-compose.yml index 917e16a..5dd3ded 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,6 +70,8 @@ services: MCP_AUTH_TOKEN: ${MCP_AUTH_TOKEN:?MCP_AUTH_TOKEN required for in-process auth} PUBLIC_URL: ${PUBLIC_URL:?PUBLIC_URL required for OAuth metadata} LOG_LEVEL: ${LOG_LEVEL:-info} + LOG_DIR: /data/logs + LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-} TZ: ${TZ:-UTC} MEMORY_DIR: ${MEMORY_DIR:-About Me} PROTECTED_PATHS: ${PROTECTED_PATHS:-} diff --git a/scripts/dev.ts b/scripts/dev.ts index 548090b..c9a294b 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -161,7 +161,7 @@ switch (sub) { ) run(`scp ${id} ${sshOpts} ${ENV_PATH} ubuntu@${ip}:/opt/vault-cortex/.env`) run( - `ssh ${id} ${sshOpts} ubuntu@${ip} 'cd /opt/vault-cortex && docker compose pull && docker compose up -d'`, + `ssh ${id} ${sshOpts} ubuntu@${ip} 'cd /opt/vault-cortex && docker compose pull && docker compose up -d && docker image prune -f'`, ) console.log(`✓ vault-mcp deployed to ${ip}:8000`) break diff --git a/src/__tests__/logger.test.ts b/src/__tests__/logger.test.ts new file mode 100644 index 0000000..9086a51 --- /dev/null +++ b/src/__tests__/logger.test.ts @@ -0,0 +1,185 @@ +import { describe, it, expect, onTestFinished } from "vitest" +import { + mkdtempSync, + rmSync, + readFileSync, + writeFileSync, + readdirSync, + existsSync, +} from "node:fs" +import { join } from "node:path" +import { tmpdir } from "node:os" +import { Settings } from "luxon" +import { createFileSinkExtension, pruneOldLogFiles } from "../logger.js" + +const createTempDir = (): string => { + const dir = mkdtempSync(join(tmpdir(), "logger-test-")) + onTestFinished(() => { + try { + rmSync(dir, { recursive: true, force: true }) + } catch { + // ignore — OS tmp dir handles stragglers + } + }) + return dir +} + +const sampleLine = (message: string): string => + JSON.stringify({ + timestamp: new Date().toISOString(), + level: "info", + name: "test", + message, + }) + "\n" + +const sampleEntry = (message: string) => ({ + timestamp: new Date().toISOString(), + level: "info" as const, + name: "test", + message, + data: {}, +}) + +describe("createFileSinkExtension", () => { + it("writes log lines to a date-stamped file", () => { + const logDir = createTempDir() + const extension = createFileSinkExtension(logDir) + + extension(sampleEntry("hello"), sampleLine("hello")) + extension(sampleEntry("world"), sampleLine("world")) + + const today = new Date().toISOString().slice(0, 10) + const content = readFileSync(join(logDir, `vault-mcp-${today}.log`), "utf8") + expect(content).toContain('"message":"hello"') + expect(content).toContain('"message":"world"') + }) + + it("creates the log directory if it does not exist", () => { + const parentDir = createTempDir() + const nestedLogDir = join(parentDir, "nested", "logs") + + createFileSinkExtension(nestedLogDir) + + expect(existsSync(nestedLogDir)).toBe(true) + }) + + it("appends to an existing log file", () => { + const logDir = createTempDir() + const today = new Date().toISOString().slice(0, 10) + const logFile = join(logDir, `vault-mcp-${today}.log`) + + writeFileSync(logFile, '{"message":"existing"}\n') + + const extension = createFileSinkExtension(logDir) + extension(sampleEntry("appended"), sampleLine("appended")) + + const content = readFileSync(logFile, "utf8") + expect(content).toContain('"message":"existing"') + expect(content).toContain('"message":"appended"') + }) + + it("rolls to a new file when the date changes", () => { + const logDir = createTempDir() + onTestFinished(() => { + Settings.now = () => Date.now() + }) + + // Day 1 + Settings.now = () => Date.parse("2026-01-15T12:00:00Z") + const extension = createFileSinkExtension(logDir, 30) + extension(sampleEntry("day1"), '{"message":"day1"}\n') + + // Day 2 + Settings.now = () => Date.parse("2026-01-16T12:00:00Z") + extension(sampleEntry("day2"), '{"message":"day2"}\n') + + const files = readdirSync(logDir) + .filter((filename) => filename.endsWith(".log")) + .sort() + expect(files).toEqual([ + "vault-mcp-2026-01-15.log", + "vault-mcp-2026-01-16.log", + ]) + + const day1Content = readFileSync( + join(logDir, "vault-mcp-2026-01-15.log"), + "utf8", + ) + const day2Content = readFileSync( + join(logDir, "vault-mcp-2026-01-16.log"), + "utf8", + ) + expect(day1Content).toContain('"message":"day1"') + expect(day2Content).toContain('"message":"day2"') + }) + + it("prunes old files on creation", () => { + const logDir = createTempDir() + writeFileSync(join(logDir, "vault-mcp-2020-01-01.log"), "ancient") + + createFileSinkExtension(logDir, 7) + + const remaining = readdirSync(logDir) + expect(remaining).toHaveLength(0) + }) +}) + +describe("pruneOldLogFiles", () => { + it("deletes log files older than retention period", () => { + const logDir = createTempDir() + + writeFileSync(join(logDir, "vault-mcp-2020-01-01.log"), "old") + writeFileSync(join(logDir, "vault-mcp-2020-06-15.log"), "also old") + const today = new Date().toISOString().slice(0, 10) + writeFileSync(join(logDir, `vault-mcp-${today}.log`), "current") + + pruneOldLogFiles(logDir, 30) + + const remaining = readdirSync(logDir) + expect(remaining).toHaveLength(1) + expect(remaining[0]).toBe(`vault-mcp-${today}.log`) + }) + + it("ignores non-matching files", () => { + const logDir = createTempDir() + + writeFileSync(join(logDir, "vault-mcp-2020-01-01.log"), "old") + writeFileSync(join(logDir, "other-file.txt"), "keep me") + writeFileSync(join(logDir, "README.md"), "keep me too") + + pruneOldLogFiles(logDir, 30) + + const remaining = readdirSync(logDir).sort() + expect(remaining).toEqual(["README.md", "other-file.txt"]) + }) + + it("keeps files within retention window", () => { + const logDir = createTempDir() + + const today = new Date().toISOString().slice(0, 10) + const yesterday = new Date(Date.now() - 86_400_000) + .toISOString() + .slice(0, 10) + writeFileSync(join(logDir, `vault-mcp-${today}.log`), "today") + writeFileSync(join(logDir, `vault-mcp-${yesterday}.log`), "yesterday") + + pruneOldLogFiles(logDir, 30) + + const remaining = readdirSync(logDir) + expect(remaining).toHaveLength(2) + }) + + it("respects custom retention days", () => { + const logDir = createTempDir() + + const twoDaysAgo = new Date(Date.now() - 2 * 86_400_000) + .toISOString() + .slice(0, 10) + writeFileSync(join(logDir, `vault-mcp-${twoDaysAgo}.log`), "old-ish") + + pruneOldLogFiles(logDir, 1) + + const remaining = readdirSync(logDir) + expect(remaining).toHaveLength(0) + }) +}) diff --git a/src/logger.ts b/src/logger.ts index 086fe24..346aa80 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,4 +1,7 @@ import { env } from "node:process" +import { mkdirSync, readdirSync, unlinkSync, appendFileSync } from "node:fs" +import { join } from "node:path" +import { DateTime } from "luxon" type LogLevel = "debug" | "info" | "warn" | "error" @@ -10,7 +13,7 @@ type LogEntry = { data: Record } -type LogExtension = (entry: LogEntry) => void +type LogExtension = (entry: LogEntry, line: string) => void export type Logger = { debug: (message: string, data?: Record) => void @@ -43,6 +46,69 @@ const getCallerSource = (): string => { return `${file}:${frame.getLineNumber()}` } +// ── File sink extension ───────────────────────────────────── + +const LOG_FILE_PREFIX = "vault-mcp-" +const LOG_FILE_SUFFIX = ".log" +/** Matches date-stamped log files: vault-mcp-YYYY-MM-DD.log */ +const LOG_FILE_PATTERN = /^vault-mcp-(\d{4}-\d{2}-\d{2})\.log$/ + +const DEFAULT_RETENTION_DAYS = 30 + +const todayDateString = (): string => DateTime.now().toISODate() + +/** Deletes log files older than retentionDays. */ +export const pruneOldLogFiles = ( + logDir: string, + retentionDays: number, +): void => { + const cutoffDate = DateTime.now().minus({ days: retentionDays }).toISODate() + + for (const filename of readdirSync(logDir)) { + const match = LOG_FILE_PATTERN.exec(filename) + const [, fileDate] = match ?? [] + if (fileDate && fileDate < cutoffDate) { + unlinkSync(join(logDir, filename)) + } + } +} + +/** Creates a LogExtension that appends each line to a date-stamped file. + * Rolls to a new file at midnight. Prunes files older than retentionDays on creation. */ +export const createFileSinkExtension = ( + logDir: string, + retentionDays: number = DEFAULT_RETENTION_DAYS, +): LogExtension => { + mkdirSync(logDir, { recursive: true }) + pruneOldLogFiles(logDir, retentionDays) + + const logPath = (): string => + join(logDir, `${LOG_FILE_PREFIX}${todayDateString()}${LOG_FILE_SUFFIX}`) + + return (_entry: LogEntry, line: string): void => { + appendFileSync(logPath(), line) + } +} + +// ── Logger ────────────────────────────────────────────────── + +const parseRetentionDays = (raw: string | undefined): number | undefined => { + if (!raw) return undefined + const parsed = parseInt(raw, 10) + return Number.isNaN(parsed) ? undefined : parsed +} + +const fileSinkExtension: LogExtension | undefined = env.LOG_DIR + ? createFileSinkExtension( + env.LOG_DIR, + parseRetentionDays(env.LOG_RETENTION_DAYS), + ) + : undefined + +const defaultExtensions: LogExtension[] = fileSinkExtension + ? [fileSinkExtension] + : [] + const createLogger = ( name: string, options?: { @@ -65,7 +131,7 @@ const createLogger = ( const mergedData = { ...baseProps, ...data } const entry: LogEntry = { - timestamp: new Date().toISOString(), + timestamp: DateTime.now().toISO(), level, name, message, @@ -85,7 +151,7 @@ const createLogger = ( else process.stdout.write(line) for (const ext of extensions) { - ext(entry) + ext(entry, line) } } @@ -102,4 +168,6 @@ const createLogger = ( } } -export const logger = createLogger("vault-cortex") +export const logger = createLogger("vault-cortex", { + extensions: defaultExtensions, +}) From 0e3448ecd57f924bef371527031e4c8383b6e176 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 19 May 2026 02:20:23 -0400 Subject: [PATCH 2/7] docs: add LOG_DIR and LOG_RETENTION_DAYS to README config table Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9800708..11384fe 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,8 @@ All settings are environment variables with sensible defaults. | `TZ` | — | `UTC` | IANA timezone for timestamps and daily note resolution | | `SERVICE_DOCUMENTATION_URL` | — | GitHub repo URL | URL returned in OAuth discovery metadata | | `LOG_LEVEL` | — | `info` | Logging verbosity: `debug`, `info`, `warn`, `error` | +| `LOG_DIR` | — | `/data/logs` (Docker) | Directory for persistent log files. Logs survive container restarts. | +| `LOG_RETENTION_DAYS` | — | `30` | Days to keep log files before automatic cleanup on startup | **Smart defaults:** Setting `MEMORY_DIR` automatically updates the defaults for `PROTECTED_PATHS` and `ORPHAN_EXCLUDE_FOLDERS`. You only set those explicitly for a fully custom list. From 4808487e089390153fa0c3405b970606579be253 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 19 May 2026 02:27:37 -0400 Subject: [PATCH 3/7] chore: clarify file sink extension comment Co-Authored-By: Claude Opus 4.6 (1M context) --- src/logger.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/logger.ts b/src/logger.ts index 346aa80..1483feb 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -85,6 +85,7 @@ export const createFileSinkExtension = ( const logPath = (): string => join(logDir, `${LOG_FILE_PREFIX}${todayDateString()}${LOG_FILE_SUFFIX}`) + // `line` is the same JSON string already written to stdout/stderr by emit() return (_entry: LogEntry, line: string): void => { appendFileSync(logPath(), line) } From 74a2bdb4cc4444850fe5545997fee7008572d438 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 19 May 2026 02:28:24 -0400 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20readability=20=E2=80=94=20expli?= =?UTF-8?q?cit=20names=20in=20logger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename `raw` → `envValue`, `parsed` → `retentionDays`, `match` → `logFileMatch`, `ext` → `extension` per naming convention. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/logger.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/logger.ts b/src/logger.ts index 1483feb..e326bbf 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -65,8 +65,8 @@ export const pruneOldLogFiles = ( const cutoffDate = DateTime.now().minus({ days: retentionDays }).toISODate() for (const filename of readdirSync(logDir)) { - const match = LOG_FILE_PATTERN.exec(filename) - const [, fileDate] = match ?? [] + const logFileMatch = LOG_FILE_PATTERN.exec(filename) + const [, fileDate] = logFileMatch ?? [] if (fileDate && fileDate < cutoffDate) { unlinkSync(join(logDir, filename)) } @@ -93,10 +93,12 @@ export const createFileSinkExtension = ( // ── Logger ────────────────────────────────────────────────── -const parseRetentionDays = (raw: string | undefined): number | undefined => { - if (!raw) return undefined - const parsed = parseInt(raw, 10) - return Number.isNaN(parsed) ? undefined : parsed +const parseRetentionDays = ( + envValue: string | undefined, +): number | undefined => { + if (!envValue) return undefined + const retentionDays = parseInt(envValue, 10) + return Number.isNaN(retentionDays) ? undefined : retentionDays } const fileSinkExtension: LogExtension | undefined = env.LOG_DIR @@ -151,8 +153,8 @@ const createLogger = ( if (level === "error") process.stderr.write(line) else process.stdout.write(line) - for (const ext of extensions) { - ext(entry, line) + for (const extension of extensions) { + extension(entry, line) } } From c76394c15cd6cc741e00bf54f17d7ecfb80b3c7b Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 19 May 2026 02:48:59 -0400 Subject: [PATCH 5/7] chore: show LOG_RETENTION_DAYS default (30) in compose files Co-Authored-By: Claude Opus 4.6 (1M context) --- deploy/local/docker-compose.yml | 2 +- deploy/remote/docker-compose.yml | 2 +- docker-compose.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/local/docker-compose.yml b/deploy/local/docker-compose.yml index 985843f..9dd3c34 100644 --- a/deploy/local/docker-compose.yml +++ b/deploy/local/docker-compose.yml @@ -26,7 +26,7 @@ services: MEMORY_DIR: ${MEMORY_DIR:-About Me} LOG_LEVEL: ${LOG_LEVEL:-info} LOG_DIR: /data/logs - LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-} + LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-30} TZ: ${TZ:-UTC} # Uncomment to override smart defaults (see Configuration in the main README): # PROTECTED_PATHS: ${PROTECTED_PATHS:-} diff --git a/deploy/remote/docker-compose.yml b/deploy/remote/docker-compose.yml index 6500c95..015dd91 100644 --- a/deploy/remote/docker-compose.yml +++ b/deploy/remote/docker-compose.yml @@ -70,7 +70,7 @@ services: MEMORY_DIR: ${MEMORY_DIR:-About Me} LOG_LEVEL: ${LOG_LEVEL:-info} LOG_DIR: /data/logs - LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-} + LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-30} TZ: ${TZ:-UTC} # Uncomment to override smart defaults (see Configuration in the main README): # PROTECTED_PATHS: ${PROTECTED_PATHS:-} diff --git a/docker-compose.yml b/docker-compose.yml index 5dd3ded..7e4b29e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -71,7 +71,7 @@ services: PUBLIC_URL: ${PUBLIC_URL:?PUBLIC_URL required for OAuth metadata} LOG_LEVEL: ${LOG_LEVEL:-info} LOG_DIR: /data/logs - LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-} + LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-30} TZ: ${TZ:-UTC} MEMORY_DIR: ${MEMORY_DIR:-About Me} PROTECTED_PATHS: ${PROTECTED_PATHS:-} From 76043eec5c23a9c0056e804d403bc04f198dfffc Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 19 May 2026 02:50:41 -0400 Subject: [PATCH 6/7] docs: fix stale LOG_DIR comment in .env.example LOG_DIR is hardcoded in docker-compose.yml, not user-configurable via .env. Remove the misleading "Default: unset" comment. Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 57bba1a..04f8c94 100644 --- a/.env.example +++ b/.env.example @@ -42,9 +42,8 @@ DEVICE_NAME=vault-cortex-lightsail CONFLICT_STRATEGY=merge SYNC_MODE=bidirectional LOG_LEVEL=info -# Directory for persistent log files (survives container restarts). Default: unset (stdout only). -# LOG_DIR=/data/logs -# Days to retain log files before cleanup (default: 30). +# Days to retain persistent log files before cleanup (default: 30). +# LOG_DIR is set in docker-compose.yml (/data/logs) — no need to set it here. # LOG_RETENTION_DAYS=30 TZ=UTC From d5a7c67e5f744856f769ea8b3bba41397d53c334 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Tue, 19 May 2026 02:52:30 -0400 Subject: [PATCH 7/7] refactor: use Luxon DateTime in logger tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all `new Date()` arithmetic with Luxon — `DateTime.now().toISODate()`, `.minus({ days: N })`, `.toISO()`. Consistent with the convention. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/__tests__/logger.test.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/__tests__/logger.test.ts b/src/__tests__/logger.test.ts index 9086a51..839fee2 100644 --- a/src/__tests__/logger.test.ts +++ b/src/__tests__/logger.test.ts @@ -9,7 +9,7 @@ import { } from "node:fs" import { join } from "node:path" import { tmpdir } from "node:os" -import { Settings } from "luxon" +import { DateTime, Settings } from "luxon" import { createFileSinkExtension, pruneOldLogFiles } from "../logger.js" const createTempDir = (): string => { @@ -26,14 +26,14 @@ const createTempDir = (): string => { const sampleLine = (message: string): string => JSON.stringify({ - timestamp: new Date().toISOString(), + timestamp: DateTime.now().toISO(), level: "info", name: "test", message, }) + "\n" const sampleEntry = (message: string) => ({ - timestamp: new Date().toISOString(), + timestamp: DateTime.now().toISO(), level: "info" as const, name: "test", message, @@ -48,7 +48,7 @@ describe("createFileSinkExtension", () => { extension(sampleEntry("hello"), sampleLine("hello")) extension(sampleEntry("world"), sampleLine("world")) - const today = new Date().toISOString().slice(0, 10) + const today = DateTime.now().toISODate() const content = readFileSync(join(logDir, `vault-mcp-${today}.log`), "utf8") expect(content).toContain('"message":"hello"') expect(content).toContain('"message":"world"') @@ -65,7 +65,7 @@ describe("createFileSinkExtension", () => { it("appends to an existing log file", () => { const logDir = createTempDir() - const today = new Date().toISOString().slice(0, 10) + const today = DateTime.now().toISODate() const logFile = join(logDir, `vault-mcp-${today}.log`) writeFileSync(logFile, '{"message":"existing"}\n') @@ -130,7 +130,7 @@ describe("pruneOldLogFiles", () => { writeFileSync(join(logDir, "vault-mcp-2020-01-01.log"), "old") writeFileSync(join(logDir, "vault-mcp-2020-06-15.log"), "also old") - const today = new Date().toISOString().slice(0, 10) + const today = DateTime.now().toISODate() writeFileSync(join(logDir, `vault-mcp-${today}.log`), "current") pruneOldLogFiles(logDir, 30) @@ -156,10 +156,8 @@ describe("pruneOldLogFiles", () => { it("keeps files within retention window", () => { const logDir = createTempDir() - const today = new Date().toISOString().slice(0, 10) - const yesterday = new Date(Date.now() - 86_400_000) - .toISOString() - .slice(0, 10) + const today = DateTime.now().toISODate() + const yesterday = DateTime.now().minus({ days: 1 }).toISODate() writeFileSync(join(logDir, `vault-mcp-${today}.log`), "today") writeFileSync(join(logDir, `vault-mcp-${yesterday}.log`), "yesterday") @@ -172,9 +170,7 @@ describe("pruneOldLogFiles", () => { it("respects custom retention days", () => { const logDir = createTempDir() - const twoDaysAgo = new Date(Date.now() - 2 * 86_400_000) - .toISOString() - .slice(0, 10) + const twoDaysAgo = DateTime.now().minus({ days: 2 }).toISODate() writeFileSync(join(logDir, `vault-mcp-${twoDaysAgo}.log`), "old-ish") pruneOldLogFiles(logDir, 1)