npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bro-code/scribe

v2026.1.1

Published

A persistent log scribe that wraps @bro-code/logger — mirrors all log methods to a rotating file and/or a custom service callback (e.g. database, HTTP), fully configurable via bro.config.json.

Downloads

334

Readme

@bro-code/scribe

npm version License: MIT

A persistent log recorder that wraps @bro-code/logger. Scribe captures exactly what the logger writes to the console and persists it — to a rotating file, a developer-supplied service callback (e.g. database / HTTP), or both.

Recording vs. logging

Scribe is a recorder, not a logger. The two packages have strict, non-overlapping jobs:

| Concern | Owner | | --- | --- | | Console output: labels ([INFO]), colors, borders, timestamps, indentation | @bro-code/logger | | Persisting that output verbatim to a file or service | @bro-code/scribe |

Scribe never generates or reformats log text. When you call scribe.info('hello'), scribe delegates to logger.info('hello'), captures the exact bytes the logger writes to stdout/stderr (with ANSI escapes stripped), and persists that captured string. Whatever shows up in your terminal is what shows up in the file.

Install

npm install @bro-code/scribe

Quick usage

import scribe from '@bro-code/scribe';

scribe.info('Server started');
scribe.warn('Disk usage high', 0, null, { usage: '92%' });
scribe.error('Boom', 0, { bold: true });

By default, scribe will:

  • Delegate every call to the underlying console logger (terminal output is unchanged).
  • Append each entry to .logs/logs.txt (created on first write).
  • Keep the latest 30 entries in the file (oldest are dropped).
  • Prefix every line with a scribe-generated ISO-8601 timestamp.

File format

Each entry is written as a single line:

[2026-04-22T22:30:01.123Z] [INFO] Server started
[2026-04-22T22:30:02.456Z] [WARN] Disk usage high

The [ISO timestamp] is always prepended by scribe regardless of whether the logger is configured to emit its own timestamps. Multi-line output (e.g. code(), bordered blocks) is flattened — embedded newlines become .

Configuration

Add a scribe section to bro.config.json in your project root:

{
  "logger": {
    "level": "info",
    "defaults": { "timestamp": true }
  },
  "scribe": {
    "level": "info",
    "limit": 30,
    "path": ".logs/logs.txt",
    "file": true,
    "service": { "module": "./logs/db-sink.js" },
    "serviceLevel": "warn"
  }
}

| Option | Type | Default | Description | | --- | --- | --- | --- | | level | LogLevel | 'info' | Minimum level recorded (independent of the logger). | | limit | number | 30 | Max records retained in the file (oldest trimmed). Also passed to the service callback. | | path | string | '.logs/logs.txt' | Destination log file. | | file | boolean | true | Persist records to the file transport. | | service | function \| { module, export? } | — | Custom sink (DB, HTTP, queue, …). | | serviceLevel | LogLevel | level | Minimum level forwarded to the service. | | buffer | boolean | false | Keep an in-memory copy of records (scribe.records). |

Console behavior is always controlled by the logger. There is no console option on scribe — to silence the terminal, set the logger's level (e.g. silent).

Record shape

Every captured entry is a ScribeRecord:

interface ScribeRecord {
  timestamp: string;    // ISO-8601 — when scribe captured the call
  method: ScribeMethod; // 'log' | 'info' | 'warn' | 'debug' | 'error' | 'json' | 'code'
  output: string;       // the raw text the logger wrote (ANSI stripped)
}

output is the only content field. Scribe does not store the original message or arguments separately — that would require interpreting the call, which is the logger's responsibility.

Special methods

json(data)

The logger pretty-prints JSON to the terminal with colors and indentation. Scribe records the compact serialization of the original data instead:

scribe.json({ event: 'purchase', total: 99.99 });
// file line: [2026-04-22T22:30:01.123Z] {"event":"purchase","total":99.99}

code(source)

The logger renders source code with line numbers and a border. Scribe records the raw source string:

scribe.code('const x = 1;');
// file line: [2026-04-22T22:30:01.123Z] const x = 1;

Service callback

The service callback lets you stream records to a database (or anywhere else) alongside the file transport.

Callback signature

type ScribeServiceFn = (record: ScribeRecord, limit: number) => void | Promise<void>;

The limit argument is the configured record limit (default 30). Use it to enforce the same cap in your data store:

const scribe = new BroScribe({
  limit: 30,
  service: async (record, limit) => {
    await db.logs.insert(record);
    // trim DB to the same cap as the file
    await db.logs.deleteOldestOver(limit);
  },
});

From bro.config.json

JSON cannot store functions, so reference a module that default-exports a ScribeServiceFn:

{
  "scribe": {
    "limit": 50,
    "service": { "module": "./logs/db-sink.cjs" }
  }
}
// logs/db-sink.cjs
module.exports = async function (record, limit) {
  await db.collection('logs').insertOne(record);
  await db.collection('logs').deleteOldestOver(limit);
};

Use the optional export field to select a non-default export:

{ "scribe": { "service": { "module": "./logs/sinks.cjs", "export": "primary" } } }

Service errors are caught and routed to the onServiceError hook — they will never crash the host process.

Custom instance

import { BroScribe } from '@bro-code/scribe';

const scribe = new BroScribe({
  level: 'debug',
  limit: 200,
  path: 'var/log/app.log',
  service: async (record, limit) => {
    await fetch('/logs', { method: 'POST', body: JSON.stringify(record) });
  },
  onServiceError: (err) => console.error('sink failed', err),
  onFileError: (err) => console.error('disk failed', err),
  loggerConfig: { level: 'verbose', defaults: { timestamp: true } },
});

API

class BroScribe {
  log(message, tab?, options?, ...args): this
  info(message, tab?, options?, ...args): this
  warn(message, tab?, options?, ...args): this
  debug(message, tab?, options?, ...args): this
  error(message, tab?, options?, ...args): this
  json(data, tab?, options?, ...args): this   // records compact JSON, not terminal rendering
  code(source, tab?, options?, ...args): this // records raw source, not terminal rendering

  reload(cwd?): this
  flush(): this

  readonly logger: BroLogger
  level: LogLevel
  readonly filePath: string | null
  readonly records: ScribeRecord[]
}

The method signatures mirror @bro-code/logger exactly, so scribe is a drop-in replacement at the call site.

License

MIT — see LICENSE.

Changelog

[2026.1.1] - 2026-04-23

Fixed

  • CJS build: tsconfig.cjs.json corrected to emit real CommonJS output. The previous module: Node16 setting combined with "type": "module" caused TypeScript to emit ESM syntax (export {) into dist/cjs/, which broke require() in Node.js environments (causing SyntaxError: Unexpected token 'export' in production).

[2026.1.0] - 2026-04-22

Added

  • setService method so user can attach the sink at startup using the normal TypeScript import chain (tsx-compatible, no build needed)

[2026.0.0] - 2026-04-22

Added

  • Initial release.
  • BroScribe class with chainable API mirroring @bro-code/logger (log, info, warn, debug, error, json, code).
  • Pure recording architecture: scribe captures exactly what the logger writes to stdout/stderr — it never generates or reformats output.
  • Every file line is prefixed with a scribe-generated ISO-8601 timestamp regardless of logger configuration.
  • File transport with rotating size limit (limit, default 30; oldest dropped).
  • Configurable file path (default .logs/logs.txt).
  • json() records the compact JSON.stringify of the data, not the logger's colorized rendering.
  • code() records the raw source string, not the logger's line-numbered bordered rendering.
  • Service callback transport with (record, limit) signature — limit is passed so the sink can enforce the same cap in its data store.
  • Independent serviceLevel filter and onServiceError hook for the service transport.
  • JSON-config support for the service via { module, export? } descriptor.
  • Independent level for the scribe (separate from the logger's level).
  • Optional in-memory record buffer (buffer: true) accessible via scribe.records.
  • reload() to pick up bro.config.* changes at runtime.