@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
Maintainers
Readme
@bro-code/scribe
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/scribeQuick 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 highThe [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
consoleoption 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.jsoncorrected to emit real CommonJS output. The previousmodule: Node16setting combined with"type": "module"caused TypeScript to emit ESM syntax (export {) intodist/cjs/, which brokerequire()in Node.js environments (causingSyntaxError: Unexpected token 'export'in production).
[2026.1.0] - 2026-04-22
Added
setServicemethod 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.
BroScribeclass 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 compactJSON.stringifyof 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 —limitis passed so the sink can enforce the same cap in its data store. - Independent
serviceLevelfilter andonServiceErrorhook for the service transport. - JSON-config support for the service via
{ module, export? }descriptor. - Independent
levelfor the scribe (separate from the logger's level). - Optional in-memory record buffer (
buffer: true) accessible viascribe.records. reload()to pick upbro.config.*changes at runtime.
