
Research
Supply Chain Attack on Axios Pulls Malicious Dependency from npm
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.
@echoes-io/tracker
Advanced tools
Type-safe SQLite database library for managing hierarchical story content.
Type-safe SQLite database library for managing hierarchical story content.
npm install @echoes-io/tracker
import { Tracker } from '@echoes-io/tracker';
// Create tracker instance
const tracker = new Tracker('./echoes.db');
// Initialize database (runs migrations automatically)
await tracker.init();
// Create content
await tracker.createTimeline({
name: 'my-story',
description: 'A fantastic adventure'
});
await tracker.createArc({
timelineName: 'my-story',
name: 'arc-1',
number: 1,
description: 'The beginning'
});
// Query content
const arcs = await tracker.getArcs('my-story');
// Clean up
await tracker.close();
The tracker manages a hierarchical content structure:
Timeline (story universe)
└─ Arc (story phase)
└─ Episode (story event)
└─ Part (optional subdivision)
└─ Chapter (individual content file)
Example:
my-fantasy-world
the-beginning (number: 1)
awakening (number: 1)
morning (number: 1)
new Tracker(dbPath?: string)
Creates a new Tracker instance.
dbPath: Path to SQLite database file (default: ./tracker.db). Use :memory: for in-memory database.init(): Promise<void>Initializes the database schema and runs pending migrations. Must be called before any other operations.
close(): Promise<void>Closes the database connection. Should be called when done to free resources.
createTimeline(data: Timeline): Promise<Timeline>Creates a new timeline.
await tracker.createTimeline({
name: 'my-story',
description: 'A fantastic adventure'
});
getTimelines(): Promise<Timeline[]>Retrieves all timelines.
getTimeline(name: string): Promise<Timeline | undefined>Retrieves a specific timeline by name.
updateTimeline(name: string, data: Partial<Timeline>): Promise<Timeline>Updates a timeline.
deleteTimeline(name: string): Promise<void>Deletes a timeline and all its related content (cascades).
createArc(data: Arc): Promise<Arc>Creates a new arc within a timeline.
await tracker.createArc({
timelineName: 'my-story',
name: 'arc-1',
number: 1,
description: 'First arc'
});
getArcs(timelineName: string): Promise<Arc[]>Retrieves all arcs in a timeline, ordered by number.
getArc(timelineName: string, arcName: string): Promise<Arc | undefined>Retrieves a specific arc.
updateArc(timelineName: string, arcName: string, data: Partial<Arc>): Promise<Arc>Updates an arc.
deleteArc(timelineName: string, arcName: string): Promise<void>Deletes an arc and all its related content (cascades).
createEpisode(data: Episode): Promise<Episode>Creates a new episode within an arc.
await tracker.createEpisode({
timelineName: 'my-story',
arcName: 'arc-1',
number: 1,
slug: 'awakening',
title: 'The Awakening',
description: 'Hero discovers their power'
});
getEpisodes(timelineName: string, arcName: string): Promise<Episode[]>Retrieves all episodes in an arc, ordered by number.
getEpisode(timelineName: string, arcName: string, episodeNumber: number): Promise<Episode | undefined>Retrieves a specific episode.
updateEpisode(timelineName: string, arcName: string, episodeNumber: number, data: Partial<Episode>): Promise<Episode>Updates an episode.
deleteEpisode(timelineName: string, arcName: string, episodeNumber: number): Promise<void>Deletes an episode and all its related content (cascades).
createPart(data: Part): Promise<Part>Creates a new part within an episode.
await tracker.createPart({
timelineName: 'my-story',
arcName: 'arc-1',
episodeNumber: 1,
number: 1,
slug: 'morning',
title: 'Morning',
description: 'The day begins'
});
getParts(timelineName: string, arcName: string, episodeNumber: number): Promise<Part[]>Retrieves all parts in an episode, ordered by number.
getPart(timelineName: string, arcName: string, episodeNumber: number, partNumber: number): Promise<Part | undefined>Retrieves a specific part.
updatePart(timelineName: string, arcName: string, episodeNumber: number, partNumber: number, data: Partial<Part>): Promise<Part>Updates a part.
deletePart(timelineName: string, arcName: string, episodeNumber: number, partNumber: number): Promise<void>Deletes a part and all its related content (cascades).
createChapter(data: Chapter): Promise<Chapter>Creates a new chapter within an episode (and optionally a part).
await tracker.createChapter({
timelineName: 'my-story',
arcName: 'arc-1',
episodeNumber: 1,
partNumber: 1,
number: 1,
pov: 'Alice',
title: 'A Strange Dream',
date: new Date('2024-01-01'),
excerpt: 'Alice woke up in a strange place...',
location: 'Enchanted Forest',
outfit: 'Blue dress', // optional
kink: 'fantasy', // optional
words: 1500,
characters: 7500,
charactersNoSpaces: 6000,
paragraphs: 15,
sentences: 75,
readingTimeMinutes: 8
});
getChapters(timelineName: string, arcName: string, episodeNumber: number, partNumber?: number): Promise<Chapter[]>Retrieves chapters in an episode, optionally filtered by part.
getChapter(timelineName: string, arcName: string, episodeNumber: number, chapterNumber: number): Promise<Chapter | undefined>Retrieves a specific chapter.
updateChapter(timelineName: string, arcName: string, episodeNumber: number, chapterNumber: number, data: Partial<Chapter>): Promise<Chapter>Updates a chapter.
deleteChapter(timelineName: string, arcName: string, episodeNumber: number, chapterNumber: number): Promise<void>Deletes a chapter.
Migrations run automatically when you call tracker.init().
migrations/ directoryXXX_description.ts (e.g., 001_initial.ts)_migrations tableCreate a file in migrations/ with the next number:
// migrations/002_add_tags.ts
import type { Kysely } from 'kysely';
import type { Database } from '../lib/database.js';
export async function up(db: Kysely<Database>): Promise<void> {
await db.schema
.createTable('tags')
.addColumn('name', 'text', (col) => col.primaryKey())
.addColumn('description', 'text')
.execute();
}
export async function down(db: Kysely<Database>): Promise<void> {
await db.schema.dropTable('tags').execute();
}
The migration will run automatically the next time tracker.init() is called.
All types are imported from @echoes-io/models:
import type {
Timeline,
Arc,
Episode,
Part,
Chapter
} from '@echoes-io/tracker';
The library throws errors in the following cases:
Always wrap operations in try/catch blocks:
try {
await tracker.createTimeline({
name: 'my-story',
description: 'Test'
});
} catch (error) {
if (error instanceof ZodError) {
console.error('Validation error:', error.errors);
} else {
console.error('Database error:', error.message);
}
}
# Install dependencies
npm install
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Build
npm run build
# Lint
npm run lint
The library includes comprehensive tests covering all CRUD operations, cascade deletes, error cases, and migrations.
Test structure:
test/
├── connection.test.ts # Database connection tests
├── index.test.ts # Module exports tests
├── migrations.test.ts # Migration system tests
└── tracker.test.ts # Tracker CRUD operations tests
Current coverage: ~99% statements, 100% functions.
MIT
Part of the Echoes project - a multi-POV digital storytelling platform.
FAQs
Type-safe SQLite database library for managing hierarchical story content.
We found that @echoes-io/tracker demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.