chore: update dependencies and improve code formatting

- Added `vendor/discord-video-stream` to pnpm workspace.
- Refactored `llmModerationClient.ts` for better readability and consistency.
- Adjusted imports in `recordingsRoutes.ts` for clarity.
- Updated `webserver.ts` to correctly import `createRecordingsRoutes`.
- Enhanced test cases in `llmModerationClient.test.ts` for improved readability.
- Updated submodule references for `better-sqlite3`, `discord-video-stream`, `discord.js-selfbot-v13`, `drizzle-orm`, and `node-datachannel`.
- Created documentation for deprecated dependency removal plan and design.
This commit is contained in:
MythEclipse
2026-05-19 02:49:55 +07:00
parent e85967ae51
commit 7f5db953fa
12 changed files with 1384 additions and 23 deletions
@@ -0,0 +1,597 @@
# Deprecated Dependency Removal Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Remove deprecated packages from `pnpm-lock.yaml` where maintained replacements exist, and vendor/submodule only when no clean replacement exists.
**Architecture:** Treat dependency cleanup as package graph surgery: identify owner, change one dependency source at a time, reinstall, verify lockfile, then run project checks. Existing vendored workspaces stay source of truth for selfbot and discord-video-stream patches.
**Tech Stack:** Node.js, pnpm workspaces, TypeScript, Vitest, Biome, git submodules.
---
## File Structure
- Modify: `package.json` — root dependency versions, workspace references, `pnpm.onlyBuiltDependencies`, optional `pnpm.overrides` if needed.
- Modify: `pnpm-workspace.yaml` — workspace package list for any new submodule/vendor package.
- Modify: `pnpm-lock.yaml` — regenerated by `pnpm install` only.
- Modify: `vendor/discord-video-stream/package.json` — dev dependency on `discord.js-selfbot-v13` should use workspace package instead of registry.
- Modify: `vendor/discord.js-selfbot-v13/package.json` — replace deprecated `otplib@12` chain if compatible.
- Possibly create: `vendor/<package-name>` — only if package has no maintained replacement and strict cleanup still needs local patching.
## Deprecated Package Owners
Known current owners:
```text
drizzle-kit -> @esbuild-kit/esm-loader -> @esbuild-kit/core-utils
discord.js-selfbot-v13 -> otplib@12 -> @otplib/plugin-crypto, @otplib/plugin-thirty-two, @otplib/preset-default
@discordjs/opus -> @discordjs/node-pre-gyp -> npmlog, are-we-there-yet, gauge, rimraf@3, glob@7, inflight
better-sqlite3 -> prebuild-install
@lng2004/node-datachannel -> prebuild-install
```
## Task 1: Establish Baseline
**Files:**
- Read: `package.json`
- Read: `pnpm-workspace.yaml`
- Read: `vendor/discord-video-stream/package.json`
- Read: `vendor/discord.js-selfbot-v13/package.json`
- [ ] **Step 1: Capture current git state**
Run:
```bash
git status --short
```
Expected: includes existing intended changes for `package.json`, `pnpm-workspace.yaml`, `pnpm-lock.yaml`, `.gitmodules`, and `vendor/discord-video-stream`. Do not revert user changes.
- [ ] **Step 2: Capture dependency owners**
Run:
```bash
pnpm why discord.js-selfbot-v13 @esbuild-kit/core-utils @esbuild-kit/esm-loader @otplib/plugin-crypto @otplib/plugin-thirty-two @otplib/preset-default are-we-there-yet fs-then-native gauge inflight lodash.pick npmlog prebuild-install stream-connect test-value
```
Expected: output maps deprecated packages to direct owners. Save relevant owner names in notes for next tasks.
- [ ] **Step 3: Capture install warning baseline**
Run:
```bash
pnpm install
```
Expected: install completes. Warnings may mention deprecated transitive packages.
- [ ] **Step 4: Capture baseline checks**
Run:
```bash
pnpm run typecheck
pnpm run test
```
Expected: both pass before dependency changes. If failing, stop and report exact failures before continuing.
## Task 2: Patch `discord-video-stream` Workspace Selfbot Reference
**Files:**
- Modify: `vendor/discord-video-stream/package.json`
- Modify: `pnpm-lock.yaml`
- [ ] **Step 1: Inspect current devDependency**
Open `vendor/discord-video-stream/package.json` and find:
```json
"discord.js-selfbot-v13": "^3.7.1"
```
Expected: exists under `devDependencies`.
- [ ] **Step 2: Replace devDependency with workspace reference**
Change that entry to:
```json
"discord.js-selfbot-v13": "workspace:*"
```
Keep peer dependency unchanged:
```json
"peerDependencies": {
"discord.js-selfbot-v13": "^3.6.0"
}
```
- [ ] **Step 3: Reinstall**
Run:
```bash
pnpm install
```
Expected: lockfile uses local workspace for `discord.js-selfbot-v13` in `vendor/discord-video-stream` importer.
- [ ] **Step 4: Verify no registry selfbot fetch from discord-video-stream**
Run:
```bash
pnpm why discord.js-selfbot-v13
```
Expected: root and discord-video-stream both point to `link:vendor/discord.js-selfbot-v13` or workspace link.
- [ ] **Step 5: Run checks**
Run:
```bash
pnpm run typecheck
pnpm run test
```
Expected: both pass.
## Task 3: Upgrade or Patch `otplib` Chain in Vendored Selfbot
**Files:**
- Modify: `vendor/discord.js-selfbot-v13/package.json`
- Modify: `pnpm-lock.yaml`
- [ ] **Step 1: Locate selfbot otplib dependency**
Run:
```bash
node -e "const p=require('./vendor/discord.js-selfbot-v13/package.json'); console.log(p.dependencies?.otplib || p.devDependencies?.otplib)"
```
Expected: prints `^12.x` or `12.x`.
- [ ] **Step 2: Check latest otplib version**
Run:
```bash
npm view otplib version deprecated --json
```
Expected: latest version is not deprecated.
- [ ] **Step 3: Change selfbot dependency to otplib latest major**
In `vendor/discord.js-selfbot-v13/package.json`, replace existing `otplib` dependency value with latest non-deprecated major range. Example if latest is 13.x:
```json
"otplib": "^13.0.0"
```
Do not change package name or selfbot exports.
- [ ] **Step 4: Reinstall**
Run:
```bash
pnpm install
```
Expected: install completes. `@otplib/plugin-crypto`, `@otplib/plugin-thirty-two`, and `@otplib/preset-default` should disappear if otplib v13 no longer pulls them.
- [ ] **Step 5: Verify otplib deprecated plugins gone**
Run:
```bash
pnpm why @otplib/plugin-crypto @otplib/plugin-thirty-two @otplib/preset-default
```
Expected: no dependency path for those packages. If they remain under `otplib`, inspect latest otplib metadata and stop before vendoring otplib.
- [ ] **Step 6: Run checks**
Run:
```bash
pnpm run typecheck
pnpm run test
```
Expected: both pass. If selfbot code breaks due otplib API changes, revert only the otplib version change and report API mismatch.
## Task 4: Upgrade `drizzle-kit` to Remove `@esbuild-kit/*`
**Files:**
- Modify: `package.json`
- Modify: `pnpm-lock.yaml`
- [ ] **Step 1: Check current and latest drizzle-kit**
Run:
```bash
node -e "const p=require('./package.json'); console.log(p.devDependencies['drizzle-kit'])"
npm view drizzle-kit version deprecated --json
```
Expected: latest version is not deprecated.
- [ ] **Step 2: Update root devDependency if newer version exists**
If latest version is newer than `0.31.10`, change `package.json` devDependency:
```json
"drizzle-kit": "^<latest-version>"
```
Example:
```json
"drizzle-kit": "^0.32.0"
```
Use actual latest version from npm output.
- [ ] **Step 3: Reinstall**
Run:
```bash
pnpm install
```
Expected: install completes.
- [ ] **Step 4: Verify esbuild-kit packages gone**
Run:
```bash
pnpm why @esbuild-kit/core-utils @esbuild-kit/esm-loader
```
Expected: no dependency path. If latest `drizzle-kit` still pulls them, keep latest only if project checks pass; do not vendor `drizzle-kit` unless user confirms dev-only strictness.
- [ ] **Step 5: Run drizzle commands**
Run:
```bash
pnpm run db:generate -- --help
pnpm run db:migrate -- --help
```
Expected: commands print help or usage without crashing. Do not run actual migrations.
- [ ] **Step 6: Run checks**
Run:
```bash
pnpm run typecheck
pnpm run test
```
Expected: both pass.
## Task 5: Remove or Replace Direct `@discordjs/opus`
**Files:**
- Modify: `package.json`
- Modify: `pnpm-lock.yaml`
- Inspect: `src/recorder/decoder.ts`
- Inspect: `tests/decoder.test.ts`
- [ ] **Step 1: Find project usage of `@discordjs/opus`**
Run:
```bash
grep -R "@discordjs/opus\|OpusEncoder\|OpusDecoder" -n src tests package.json
```
Expected: usage locations show whether direct package is imported by project code or only required indirectly by `prism-media`.
- [ ] **Step 2: Check `@discordjs/voice` encryption/audio requirements**
Run:
```bash
pnpm why @discordjs/opus prism-media
```
Expected: shows `@discordjs/voice` and root dependency relationships.
- [ ] **Step 3: Test removal in package manifest**
Remove root dependency line from `package.json`:
```json
"@discordjs/opus": "^0.10.0",
```
Do not edit code yet.
- [ ] **Step 4: Reinstall**
Run:
```bash
pnpm install
```
Expected: install completes. If install or peer resolution fails, restore `@discordjs/opus` and continue to Step 8.
- [ ] **Step 5: Run decoder-specific tests**
Run:
```bash
pnpm run test -- tests/decoder.test.ts
```
Expected: tests pass or skip native opus gracefully. If tests fail because native opus is required by project behavior, restore `@discordjs/opus` and continue to Step 8.
- [ ] **Step 6: Run full checks**
Run:
```bash
pnpm run typecheck
pnpm run test
```
Expected: both pass.
- [ ] **Step 7: Verify deprecated node-pre-gyp chain gone**
Run:
```bash
pnpm why @discordjs/node-pre-gyp npmlog are-we-there-yet gauge rimraf glob inflight
```
Expected: no dependency path through `@discordjs/opus`. If gone, task complete.
- [ ] **Step 8: If `@discordjs/opus` is required, try maintained alternatives**
Run:
```bash
npm view opusscript version deprecated --json
npm view @evan/opus version deprecated --json
```
Expected: identify non-deprecated candidate. Do not switch unless package supports same runtime path used by `prism-media` or direct project imports.
- [ ] **Step 9: If no compatible maintained alternative exists, vendor decision checkpoint**
Stop and report:
```text
@discordjs/opus still required. No compatible maintained replacement verified. Next action requires cloning/vendoring smallest package owner or accepting deprecated native install chain.
```
Do not clone without user confirmation of target repository.
## Task 6: Evaluate `prebuild-install` Owners
**Files:**
- Modify: `package.json` only if safe upgrade exists
- Modify: `vendor/discord-video-stream/package.json` only if safe upgrade exists
- Modify: `pnpm-lock.yaml`
- [ ] **Step 1: Check owner versions**
Run:
```bash
npm view better-sqlite3 version deprecated --json
npm view @lng2004/node-datachannel version deprecated --json
node -e "const root=require('./package.json'); const dvs=require('./vendor/discord-video-stream/package.json'); console.log({betterSqlite3: root.dependencies['better-sqlite3'], nodeDatachannel: dvs.dependencies['@lng2004/node-datachannel']})"
```
Expected: latest versions known.
- [ ] **Step 2: Upgrade `better-sqlite3` if newer version exists**
If latest is newer than current, change `package.json`:
```json
"better-sqlite3": "^<latest-version>"
```
Use actual latest version.
- [ ] **Step 3: Upgrade `@lng2004/node-datachannel` if newer version exists**
If latest is newer than current and package name still matches discord-video-stream requirements, change `vendor/discord-video-stream/package.json`:
```json
"@lng2004/node-datachannel": "<latest-version>"
```
Use exact latest version only if upstream uses exact published builds.
- [ ] **Step 4: Reinstall**
Run:
```bash
pnpm install
```
Expected: native dependencies install or reuse existing builds successfully.
- [ ] **Step 5: Verify `prebuild-install` status**
Run:
```bash
pnpm why prebuild-install
```
Expected: either no dependency path, or only native owners remain.
- [ ] **Step 6: Run checks**
Run:
```bash
pnpm run typecheck
pnpm run test
```
Expected: both pass.
- [ ] **Step 7: If `prebuild-install` remains**
Stop and report owner paths. Do not vendor native packages unless no maintained version removes it and user explicitly wants native submodule maintenance.
## Task 7: Final Deprecation Audit
**Files:**
- Modify: `pnpm-lock.yaml` through install only
- [ ] **Step 1: Run clean install audit**
Run:
```bash
pnpm install
```
Expected: no deprecated package warnings where maintained replacements were applied.
- [ ] **Step 2: Run owner query for all known deprecated names**
Run:
```bash
pnpm why discord.js-selfbot-v13 @esbuild-kit/core-utils @esbuild-kit/esm-loader @otplib/plugin-crypto @otplib/plugin-thirty-two @otplib/preset-default are-we-there-yet fs-then-native gauge inflight lodash.pick npmlog prebuild-install stream-connect test-value @discordjs/node-pre-gyp
```
Expected: no paths for packages removed by prior tasks. Remaining paths must be only approved unavoidable native/package-owner cases.
- [ ] **Step 3: Check npm deprecation metadata for remaining suspect packages**
Run:
```bash
node - <<'NODE'
const {execFileSync}=require('child_process');
const pkgs=['discord.js-selfbot-v13','@esbuild-kit/core-utils','@esbuild-kit/esm-loader','@otplib/plugin-crypto','@otplib/plugin-thirty-two','@otplib/preset-default','are-we-there-yet','fs-then-native','gauge','inflight','lodash.pick','npmlog','prebuild-install','stream-connect','test-value','@discordjs/node-pre-gyp'];
for (const p of pkgs) {
try {
const out=execFileSync('npm',['view',p,'deprecated','--json'],{encoding:'utf8'}).trim();
if (out && out !== 'null') console.log(`${p}: ${JSON.parse(out)}`);
} catch {
console.log(`${p}: npm view failed`);
}
}
NODE
```
Expected: command prints metadata only. Compare printed package names with `pnpm why` output.
- [ ] **Step 4: Run final checks**
Run:
```bash
pnpm run typecheck
pnpm run test
pnpm run lint
```
Expected: all pass.
## Task 8: Document Remaining Unavoidable Deprecated Packages
**Files:**
- Modify: `docs/superpowers/specs/2026-05-19-deprecated-dependency-removal-design.md`
- [ ] **Step 1: If no deprecated packages remain, append success note**
Append this section:
```markdown
## Final Audit Result
All known deprecated packages from the initial audit were removed from the active pnpm dependency graph.
```
- [ ] **Step 2: If deprecated packages remain, append owner note**
Append this section with actual owner paths from `pnpm why`:
```markdown
## Final Audit Result
Remaining deprecated packages after maintained upgrade attempts:
- `<package>` remains via `<owner path>`. Reason: `<no maintained replacement found | native install chain still used by latest upstream | user-approved vendored package>`.
These are candidates for future vendoring/submodule patching if strict lockfile cleanup remains required.
```
- [ ] **Step 3: Run final git diff review**
Run:
```bash
git diff -- package.json pnpm-workspace.yaml pnpm-lock.yaml vendor/discord-video-stream/package.json vendor/discord.js-selfbot-v13/package.json docs/superpowers/specs/2026-05-19-deprecated-dependency-removal-design.md
```
Expected: diff contains only dependency migration changes and audit note.
## Task 9: Commit Checkpoint Only If User Requests Commit
**Files:**
- Stage only changed package/spec/submodule files relevant to dependency cleanup.
- [ ] **Step 1: Show status**
Run:
```bash
git status --short
```
Expected: changed files match work done.
- [ ] **Step 2: Ask before committing**
Ask user whether to commit. Do not commit unless explicitly requested.
- [ ] **Step 3: If user asks to commit, create commit**
Use exact changed file list, not `git add -A`. Commit message:
```bash
git commit -m "$(cat <<'EOF'
chore: remove deprecated dependency graph entries
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```
Expected: commit succeeds without bypassing hooks.
## Self-Review
- Spec coverage: plan covers replace-first, workspace patches, submodule/vendor checkpoint, verification, and final audit documentation.
- Placeholder scan: no TBD/TODO placeholders; steps include exact commands and expected outcomes.
- Type consistency: paths and package names match current workspace files and dependency owners.
@@ -0,0 +1,652 @@
# Winston Logging Refactor Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace Pino logging with Winston, normalize log levels, and write readable console plus JSON file logs without changing application behavior.
**Architecture:** Keep `src/logger.ts` as the single logging entry point. Rebuild it around one Winston logger with child context support, centralized metadata/error serialization, colorized console output, and JSON file transports. Keep call-site changes minimal and only adjust imports/usages that fail after the backend swap.
**Tech Stack:** TypeScript, Node.js ESM, Winston, Zod config validation, pnpm, Vitest, Biome.
---
## File Structure
- Modify: `package.json` — replace Pino dependencies with Winston.
- Modify: `pnpm-lock.yaml` — update via pnpm after dependency changes.
- Modify: `src/config.ts` — expand `LOG_LEVEL` enum to Winston npm levels.
- Modify: `src/logger.ts` — replace Pino implementation with Winston implementation.
- Modify: `.gitignore` — ignore runtime `logs/` directory.
- Test: `tests/logger.test.ts` — add focused logger behavior tests for level validation, error serialization, and file output format helpers.
- Inspect and modify only if needed: files importing `createChildLogger` or `logger`.
## Important Git Note
The prior design-spec commit unexpectedly included vendor submodule entries that were already staged in the working tree. Before making implementation commits, run `git status --short` and do not stage unrelated vendor or existing user changes. Stage only files touched by this plan.
---
### Task 1: Add Logger Tests Before Migration
**Files:**
- Create: `tests/logger.test.ts`
- Modify: none
- [ ] **Step 1: Write tests for Winston-compatible logger behavior**
Create `tests/logger.test.ts` with this content:
```ts
import { describe, expect, it } from "vitest";
import { formatLogMetadataForTest, serializeLogValueForTest } from "../src/logger";
class TestError extends Error {
public code = "TEST_CODE";
public statusCode = 418;
constructor() {
super("test failure");
this.name = "TestError";
}
}
describe("logger serialization", () => {
it("serializes Error values with stable fields", () => {
const serialized = serializeLogValueForTest(new TestError());
expect(serialized).toMatchObject({
name: "TestError",
message: "test failure",
code: "TEST_CODE",
statusCode: 418,
});
expect(serialized).toHaveProperty("stack");
});
it("serializes nested error metadata keys", () => {
const error = new TestError();
expect(formatLogMetadataForTest({ error, err: error, reason: error })).toMatchObject({
error: {
name: "TestError",
message: "test failure",
code: "TEST_CODE",
statusCode: 418,
},
err: {
name: "TestError",
message: "test failure",
code: "TEST_CODE",
statusCode: 418,
},
reason: {
name: "TestError",
message: "test failure",
code: "TEST_CODE",
statusCode: 418,
},
});
});
it("preserves plain metadata", () => {
expect(
formatLogMetadataForTest({ context: "bot", signal: "SIGINT", count: 2 }),
).toEqual({ context: "bot", signal: "SIGINT", count: 2 });
});
});
```
- [ ] **Step 2: Run test to verify current implementation fails**
Run:
```bash
pnpm vitest run tests/logger.test.ts
```
Expected: FAIL because `formatLogMetadataForTest` and `serializeLogValueForTest` are not exported from `src/logger.ts` yet.
- [ ] **Step 3: Commit failing tests only**
```bash
git add tests/logger.test.ts
git commit -m "test: cover logger metadata serialization"
```
Expected: commit contains only `tests/logger.test.ts`.
---
### Task 2: Update Dependencies and Config Schema
**Files:**
- Modify: `package.json`
- Modify: `pnpm-lock.yaml`
- Modify: `src/config.ts`
- [ ] **Step 1: Update package dependencies**
Run:
```bash
pnpm remove pino pino-http pino-pretty
pnpm add winston
```
Expected:
- `package.json` dependencies no longer include `pino` or `pino-http`.
- `package.json` devDependencies no longer include `pino-pretty`.
- `package.json` dependencies include `winston`.
- `pnpm-lock.yaml` updates accordingly.
- [ ] **Step 2: Expand `LOG_LEVEL` validation**
In `src/config.ts`, replace the current `LOG_LEVEL` line:
```ts
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
```
with:
```ts
LOG_LEVEL: z
.enum(["error", "warn", "info", "http", "verbose", "debug", "silly"])
.default("info"),
```
- [ ] **Step 3: Run targeted config tests**
Run:
```bash
pnpm vitest run tests/config.test.ts
```
Expected: PASS.
- [ ] **Step 4: Commit dependency and config changes**
```bash
git add package.json pnpm-lock.yaml src/config.ts
git commit -m "chore: switch logging dependency to winston"
```
Expected: commit contains only dependency files and `src/config.ts`.
---
### Task 3: Replace `src/logger.ts` With Winston Implementation
**Files:**
- Modify: `src/logger.ts`
- [ ] **Step 1: Replace logger implementation**
Replace entire `src/logger.ts` with:
```ts
import fs from "node:fs";
import path from "node:path";
import winston from "winston";
const isDev = process.env.NODE_ENV !== "production";
const logLevel = process.env.LOG_LEVEL || (isDev ? "debug" : "info");
const logsDir = path.resolve(process.cwd(), "logs");
fs.mkdirSync(logsDir, { recursive: true });
type LogMetadata = Record<string, unknown>;
type SerializedError = {
name: string;
message: string;
stack?: string;
code?: unknown;
statusCode?: unknown;
} & Record<string, unknown>;
const serializeError = (error: Error): SerializedError => {
const serialized: SerializedError = {
name: error.name,
message: error.message,
};
if (error.stack) {
serialized.stack = error.stack;
}
const errorWithFields = error as Error & {
code?: unknown;
statusCode?: unknown;
[key: string]: unknown;
};
if (errorWithFields.code !== undefined) {
serialized.code = errorWithFields.code;
}
if (errorWithFields.statusCode !== undefined) {
serialized.statusCode = errorWithFields.statusCode;
}
for (const [key, value] of Object.entries(errorWithFields)) {
if (serialized[key] === undefined) {
serialized[key] = value;
}
}
return serialized;
};
const serializeLogValue = (value: unknown): unknown => {
if (value instanceof Error) {
return serializeError(value);
}
if (Array.isArray(value)) {
return value.map(serializeLogValue);
}
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, nestedValue]) => [
key,
serializeLogValue(nestedValue),
]),
);
}
return value;
};
const formatLogMetadata = (metadata: LogMetadata): LogMetadata => {
return Object.fromEntries(
Object.entries(metadata).map(([key, value]) => [key, serializeLogValue(value)]),
);
};
const metadataFormat = winston.format((info) => {
const { level, message, timestamp, ...metadata } = info;
return {
level,
message,
timestamp,
...formatLogMetadata(metadata),
};
});
const consoleFormat = winston.format.printf((info) => {
const { level, message, timestamp, context, ...metadata } = info;
const contextLabel = context ? ` [${String(context)}]` : "";
const metadataText = Object.keys(metadata).length
? ` ${JSON.stringify(metadata)}`
: "";
return `${timestamp} ${level}${contextLabel}: ${message}${metadataText}`;
});
export const logger = winston.createLogger({
level: logLevel,
levels: winston.config.npm.levels,
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
metadataFormat(),
),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
metadataFormat(),
consoleFormat,
),
}),
new winston.transports.File({
filename: path.join(logsDir, "app.log"),
format: winston.format.json(),
}),
new winston.transports.File({
filename: path.join(logsDir, "error.log"),
level: "error",
format: winston.format.json(),
}),
],
});
export const createChildLogger = (context: string) => {
return logger.child({ context });
};
export const serializeLogValueForTest = serializeLogValue;
export const formatLogMetadataForTest = formatLogMetadata;
```
- [ ] **Step 2: Run logger tests**
Run:
```bash
pnpm vitest run tests/logger.test.ts
```
Expected: PASS.
- [ ] **Step 3: Run TypeScript check**
Run:
```bash
pnpm run typecheck
```
Expected: either PASS or type errors only from logger call sites needing Winston-compatible adjustments.
- [ ] **Step 4: Commit Winston logger implementation**
If typecheck passes:
```bash
git add src/logger.ts
git commit -m "refactor: replace pino logger with winston"
```
Expected: commit contains only `src/logger.ts`.
If typecheck fails in logger call sites, do not commit yet. Continue to Task 4, then commit `src/logger.ts` together with required call-site fixes.
---
### Task 4: Fix Logger Call Sites Only If Needed
**Files:**
- Inspect: `src/**/*.ts`
- Modify only files with TypeScript errors from Task 3.
- [ ] **Step 1: Find logger imports and usages**
Run:
```bash
grep -rn "createChildLogger\|from \"./logger\"\|from \"../logger\"\|logger\.\|log\." src --include="*.ts"
```
Expected: list of logger imports and method calls.
- [ ] **Step 2: Fix pino-style calls if TypeScript requires it**
Use this mapping only where needed:
Before:
```ts
logger.info({ signal }, "Graceful shutdown initiated");
```
After:
```ts
logger.info("Graceful shutdown initiated", { signal });
```
Before:
```ts
logger.error({ error }, "Failed to initialize app");
```
After:
```ts
logger.error("Failed to initialize app", { error });
```
Before:
```ts
logger.warn({ error }, "Backlog sync failed");
```
After:
```ts
logger.warn("Backlog sync failed", { error });
```
Plain string calls remain unchanged:
```ts
logger.info("Creating Discord client");
logger.debug("Queue empty, no playback started");
```
- [ ] **Step 3: Re-run TypeScript check**
Run:
```bash
pnpm run typecheck
```
Expected: PASS.
- [ ] **Step 4: Commit call-site fixes**
```bash
git add src/logger.ts src/index.ts src/webserver.ts src/middleware.ts src/voiceController.ts src/media/mediaController.ts src/media/screenShareController.ts src/moderation/broadcaster.ts src/moderation/messageCapture.ts src/streaming/transcoder.ts
git commit -m "refactor: normalize logger call sites for winston"
```
Expected: commit stages only files actually changed. If some listed files are unchanged, `git add` is harmless.
---
### Task 5: Ignore Runtime Log Files
**Files:**
- Modify: `.gitignore`
- [ ] **Step 1: Check whether `logs/` is already ignored**
Run:
```bash
grep -n "^logs/$" .gitignore || true
```
Expected: either one matching line or no output.
- [ ] **Step 2: Add `logs/` if missing**
If no output from Step 1, append:
```gitignore
logs/
```
- [ ] **Step 3: Verify ignore rule**
Run:
```bash
git check-ignore logs/app.log
```
Expected output:
```text
logs/app.log
```
- [ ] **Step 4: Commit ignore rule**
```bash
git add .gitignore
git commit -m "chore: ignore runtime log files"
```
Expected: commit contains only `.gitignore`. If `.gitignore` already ignored `logs/`, skip this commit.
---
### Task 6: Verify No Pino Usage Remains
**Files:**
- Inspect: repository source and package files
- [ ] **Step 1: Search for Pino imports/usages**
Run:
```bash
grep -rn "pino\|pino-http\|pino-pretty" src package.json pnpm-lock.yaml --exclude-dir=node_modules || true
```
Expected: no output. If output appears only in old comments/docs outside runtime files, remove or update those comments/docs if they are part of the changed scope.
- [ ] **Step 2: Search for Winston dependency**
Run:
```bash
grep -n '"winston"' package.json
```
Expected output contains one `winston` dependency line.
- [ ] **Step 3: Run focused tests**
Run:
```bash
pnpm vitest run tests/logger.test.ts tests/config.test.ts
```
Expected: PASS.
- [ ] **Step 4: Commit cleanup if any files changed**
```bash
git status --short
git add <only-files-changed-by-this-task>
git commit -m "chore: remove remaining pino references"
```
Expected: commit only if Step 1 required changes.
---
### Task 7: Runtime Log Output Verification
**Files:**
- No source changes expected
- [ ] **Step 1: Run one-shot logger command**
Run:
```bash
node --import tsx -e 'import { logger } from "./src/logger.ts"; logger.info("logger smoke info", { context: "smoke", value: 1 }); logger.error("logger smoke error", { error: new Error("smoke failure") }); await new Promise((resolve) => setTimeout(resolve, 250));'
```
Expected:
- Console shows readable timestamped logs.
- Info line includes `smoke` context metadata.
- Error line includes serialized error metadata.
- [ ] **Step 2: Verify app log file exists and contains JSON**
Run:
```bash
test -s logs/app.log && node -e 'const fs = require("fs"); const line = fs.readFileSync("logs/app.log", "utf8").trim().split("\n").at(-1); const parsed = JSON.parse(line); if (!parsed.level || !parsed.message) process.exit(1); console.log(parsed.level + ":" + parsed.message);'
```
Expected output includes:
```text
error:logger smoke error
```
- [ ] **Step 3: Verify error log file exists and contains error JSON**
Run:
```bash
test -s logs/error.log && node -e 'const fs = require("fs"); const line = fs.readFileSync("logs/error.log", "utf8").trim().split("\n").at(-1); const parsed = JSON.parse(line); if (parsed.level !== "error") process.exit(1); console.log(parsed.level + ":" + parsed.message);'
```
Expected output:
```text
error:logger smoke error
```
---
### Task 8: Full Validation
**Files:**
- No source changes expected unless validation reveals issues.
- [ ] **Step 1: Run typecheck**
Run:
```bash
pnpm run typecheck
```
Expected: PASS.
- [ ] **Step 2: Run test suite**
Run:
```bash
pnpm run test
```
Expected: PASS.
- [ ] **Step 3: Run lint**
Run:
```bash
pnpm run lint
```
Expected: PASS.
- [ ] **Step 4: Inspect final git status**
Run:
```bash
git status --short
```
Expected: no uncommitted changes from this plan except ignored `logs/` files. Existing unrelated user changes may still appear; do not stage them.
- [ ] **Step 5: Final commit if validation fixes were needed**
If validation required fixes:
```bash
git add <only-files-changed-by-validation-fixes>
git commit -m "fix: stabilize winston logging migration"
```
Expected: commit contains only validation fixes.
---
## Self-Review Notes
- Spec coverage: dependency swap, `LOG_LEVEL`, centralized logger, console/file output, error serialization, Pino removal, and validation all have tasks.
- Placeholder scan: no TBD/TODO/fill-in placeholders remain; `<only-files-changed-by-this-task>` and similar are explicit safety instructions to avoid staging unrelated user changes.
- Type consistency: test helper names match `src/logger.ts` exports; Winston npm levels match `src/config.ts`; file names match spec.
@@ -0,0 +1,91 @@
# Deprecated Dependency Removal Design
## Goal
Remove deprecated packages from the pnpm lockfile where practical. Prefer maintained replacements or upgrades. If no maintained replacement exists, vendor upstream code as a submodule/workspace and patch dependency metadata there.
## Scope
Current deprecated sources:
- `drizzle-kit` pulls `@esbuild-kit/esm-loader` and `@esbuild-kit/core-utils`.
- `discord.js-selfbot-v13` pulls `otplib@12` plugins.
- `@discordjs/opus` pulls `@discordjs/node-pre-gyp`, which pulls `npmlog`, `are-we-there-yet`, `gauge`, `rimraf@3`, `glob@7`, and `inflight`.
- `@lng2004/node-datachannel` and `better-sqlite3` pull `prebuild-install`.
Existing workspace packages:
- `vendor/discord.js-selfbot-v13`
- `vendor/discord-video-stream`
## Approach
1. Upgrade direct dependencies first and re-check `pnpm why` plus npm deprecation metadata.
2. Patch vendored workspace dependencies when project already owns package source.
3. Replace direct packages only when runtime compatibility is clear.
4. Add submodules only for packages that cannot be replaced or upgraded without keeping deprecated transitive packages.
## Package Plan
### `drizzle-kit`
Try latest compatible `drizzle-kit`. If latest still depends on `@esbuild-kit/*`, keep current version unless project commands fail, because vendoring `drizzle-kit` only to remove dev-only install warnings has high maintenance cost.
### `discord.js-selfbot-v13`
Patch `vendor/discord.js-selfbot-v13` dependency graph to remove `otplib@12` if code is compatible with `otplib@13`. Verify by installing and running typecheck/tests. Keep peer/package name unchanged.
### `@discordjs/opus`
Find maintained Opus alternative compatible with current recorder and `@discordjs/voice`. Prefer removing direct `@discordjs/opus` only if code and tests still pass. If native Opus remains needed and every maintained option drags deprecated install tooling, vendor the smallest dependency owner.
### `prebuild-install` sources
Do not patch native package install chains blindly. For `better-sqlite3`, keep upstream unless latest removes `prebuild-install`. For `@lng2004/node-datachannel`, try latest first through `discord-video-stream`; vendor only if strict lockfile cleanup remains blocked and build still works.
### `discord-video-stream`
Keep as workspace submodule. Patch devDependency `discord.js-selfbot-v13` to use workspace reference so installs do not fetch deprecated registry selfbot.
## Verification
After each dependency change:
1. Run `pnpm install`.
2. Run `pnpm why` for known deprecated package names.
3. Check npm deprecation metadata for remaining lockfile packages.
4. Run `pnpm run typecheck`.
5. Run `pnpm run test`.
## Success Criteria
- Root `package.json` uses workspace paths for vendored packages.
- `pnpm-lock.yaml` has no deprecated packages where maintained replacements exist.
- Any remaining deprecated packages are documented as no-maintained-replacement and owned by a vendored submodule or unavoidable native upstream.
- Typecheck and tests pass.
## Final Audit Result
Deprecated packages removed from active dependency graph:
- `@otplib/plugin-crypto`, `@otplib/plugin-thirty-two`, `@otplib/preset-default` — removed by patching `vendor/discord.js-selfbot-v13` to `otplib@13`.
- `@discordjs/opus` direct dependency — removed from root dependencies.
- `@discordjs/node-pre-gyp`, `npmlog`, `are-we-there-yet`, `gauge`, `rimraf@3`, `glob@7`, `inflight` — removed by eliminating `@discordjs/opus` auto-installed peer path.
Remaining unavoidable deprecated packages:
- `@esbuild-kit/core-utils@3.3.2` via `drizzle-kit@0.31.10`.
- `@esbuild-kit/esm-loader@2.6.5` via `drizzle-kit@0.31.10`.
- `prebuild-install@7.1.3` via `better-sqlite3@12.10.0` and `@lng2004/node-datachannel@0.32.0-20260202`.
Reason these remain:
- `drizzle-kit@0.31.10` is latest stable and still depends on `@esbuild-kit/*`.
- `better-sqlite3@12.10.0` is latest stable and still uses `prebuild-install` for native binary install.
- `@lng2004/node-datachannel@0.32.0-20260202` is latest available and still uses `prebuild-install` for native binary install.
The upstream repositories are vendored as submodules for future patching if strict zero-deprecated lockfile becomes worth maintaining as forks:
- `vendor/drizzle-orm`
- `vendor/better-sqlite3`
- `vendor/node-datachannel`
+1
View File
@@ -1,5 +1,6 @@
packages:
- .
- vendor/discord-video-stream
- vendor/discord.js-selfbot-v13
onlyBuiltDependencies:
+6 -4
View File
@@ -91,15 +91,16 @@ export function parseModerationResponse(
message_id: msgId,
status: (parsed as any).status || "clean",
flags: (parsed as any).flags || [],
score: (parsed as any).score !== undefined ? (parsed as any).score : 0.1,
score:
(parsed as any).score !== undefined ? (parsed as any).score : 0.1,
analysis: (parsed as any).analysis || "",
},
],
};
} else {
// Look for any array property (result, data, messages, moderation, etc.)
const arrayKey = Object.keys(parsed).find(
(key) => Array.isArray((parsed as any)[key]),
const arrayKey = Object.keys(parsed).find((key) =>
Array.isArray((parsed as any)[key]),
);
if (arrayKey) {
parsed.results = (parsed as any)[arrayKey];
@@ -537,7 +538,8 @@ Return ONLY valid JSON, no other text.`;
try {
parsed = parseModerationResponse(content, targetIds);
} catch (parseError) {
const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
const errorMsg =
parseError instanceof Error ? parseError.message : String(parseError);
log.error(
{
error: errorMsg,
+2 -2
View File
@@ -1,8 +1,8 @@
import {
Router,
type NextFunction,
type Request,
type Response,
type NextFunction,
Router,
} from "express";
import { listVoiceRecordings } from "../database/voiceRecordingRepo";
import { AppError } from "../errors";
+1 -1
View File
@@ -25,10 +25,10 @@ import { discordPlayer } from "./player";
import { createAnalysisRoutes } from "./routes/analysisRoutes";
import { createMediaRoutes } from "./routes/mediaRoutes";
import { createMessageRoutes } from "./routes/messageRoutes";
import { createRecordingsRoutes } from "./routes/recordingsRoutes";
import { createSyncRoutes } from "./routes/syncRoutes";
import { createUIStateRoutes } from "./routes/uiStateRoutes";
import { createVoiceRoutes } from "./routes/voiceRoutes";
import { createRecordingsRoutes } from "./routes/recordingsRoutes";
import { Streamer } from "./streaming";
import type { VoiceController } from "./voiceController";
+30 -12
View File
@@ -415,7 +415,9 @@ describe("runModerationAnalysis", () => {
expect(requestBody.temperature).toBe(0);
expect(requestBody.response_format).toEqual({ type: "json_object" });
expect(requestBody.reasoning_budget).toBeUndefined();
expect(requestBody.chat_template_kwargs).toEqual({ enable_thinking: false });
expect(requestBody.chat_template_kwargs).toEqual({
enable_thinking: false,
});
});
it("throws on non-ok HTTP response", async () => {
@@ -655,7 +657,9 @@ describe("runModerationAnalysis", () => {
expect(downloadedUrls).toContain("https://httpbin.org/image/png?source=t2");
expect(downloadedUrls).toContain("https://httpbin.org/image/png?source=t1");
expect(downloadedUrls).toContain("https://httpbin.org/image/png?source=c7");
expect(downloadedUrls).not.toContain("https://httpbin.org/image/png?source=c1");
expect(downloadedUrls).not.toContain(
"https://httpbin.org/image/png?source=c1",
);
});
it("sends verified real PNG and JPEG attachments with realistic Indonesian text", async () => {
@@ -761,7 +765,9 @@ describe("runModerationAnalysis", () => {
const requestBody = JSON.parse(fetchCalls[2][1].body);
const contentParts = requestBody.messages[0].content;
expect(contentParts.filter((part: any) => part.type === "image_url")).toHaveLength(2);
expect(
contentParts.filter((part: any) => part.type === "image_url"),
).toHaveLength(2);
expect(contentParts[0].image_url.url).toContain("data:image/jpeg;base64,");
expect(contentParts[2].image_url.url).toContain("data:image/png;base64,");
expect(contentParts.at(-1).text).toContain("https://example.invalid/login");
@@ -795,7 +801,10 @@ describe("runModerationAnalysis", () => {
return Promise.resolve({
ok: true,
arrayBuffer: async () =>
buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength,
),
});
}
@@ -807,7 +816,12 @@ describe("runModerationAnalysis", () => {
});
await runModerationAnalysis({
targets: [createMessageRecord({ id: "m1", content: "gambar belum selesai upload ke picser" })],
targets: [
createMessageRecord({
id: "m1",
content: "gambar belum selesai upload ke picser",
}),
],
contextText: "test context",
attachments: [
{
@@ -830,7 +844,9 @@ describe("runModerationAnalysis", () => {
],
});
expect((global.fetch as any).mock.calls[0][0]).toBe("https://httpbin.org/image/png");
expect((global.fetch as any).mock.calls[0][0]).toBe(
"https://httpbin.org/image/png",
);
});
it("keeps analyzing text when an image URL returns non-OK", async () => {
@@ -845,7 +861,8 @@ describe("runModerationAnalysis", () => {
status: "warn",
flags: ["suspicious_link"],
score: 0.6,
analysis: "Image fetch failed, text still indicates suspicious link.",
analysis:
"Image fetch failed, text still indicates suspicious link.",
},
],
}),
@@ -873,7 +890,8 @@ describe("runModerationAnalysis", () => {
content: "cek bonus gratis di https://example.invalid/claim sekarang",
}),
],
contextText: "Pesan ini dikirim berulang setelah user lain menolak klik link.",
contextText:
"Pesan ini dikirim berulang setelah user lain menolak klik link.",
attachments: [
{
id: "bad-image",
@@ -899,7 +917,9 @@ describe("runModerationAnalysis", () => {
const requestBody = JSON.parse((global.fetch as any).mock.calls[1][1].body);
expect(requestBody.messages[0].content).toHaveLength(1);
expect(requestBody.messages[0].content[0].text).toContain("https://example.invalid/claim");
expect(requestBody.messages[0].content[0].text).toContain(
"https://example.invalid/claim",
);
});
describe("Edge Cases & Real-World Scenarios", () => {
@@ -1349,9 +1369,7 @@ describe("runModerationAnalysis", () => {
]
}`;
expect(() =>
parseModerationResponse(content, ["m1"]),
).toThrow(/finite/i);
expect(() => parseModerationResponse(content, ["m1"])).toThrow(/finite/i);
});
});
});