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

@schlessera/brain-module-travel

v0.40.0

Published

brain-kit module: journeys, day trips and visited places

Readme

@schlessera/brain-module-travel

Journeys, repeated day trips and visited places for brain-kit. Requires Bun ≥ 1.3.5. This package joins the next lockstep release.

Enable it under modules in brain.config.ts or brain.config.json:

modules: {
  "@schlessera/brain-module-travel": {
    travelParty: [
      { name: "Odysseus" },
      { name: "Penelope", role: "partner", requirementsDoc: "people/penelope.md" },
    ],
  },
}

travelParty defaults to []. A member has name: string, optional role: string and optional requirementsDoc: string. Requirements documents are relative to the brain root. The strict schema preserves the existing speaking configuration's values and rejects unknown configuration keys. User taxonomy overrides can retain existing custom directories.

Ownership

| Type | Default directory | Canonical record | | --- | --- | --- | | travel | travel/ | An overnight journey; existing itineraries keep their paths and fields. | | trip | trips/ | One document per repeatable route or place, containing its visit history. | | place | places/ | One country, city, town or notable spot, linked to canonical visits. |

Travel retains the legacy status.md, itinerary.md and outline.md directory anchors and planning skill plan-travel. Speaking retains talks, conferences and the same shared directory anchors. Travel alone introduces no slide-deck exclusions. Both modules can be enabled together.

Canonical formats

All three use the ordinary core frontmatter (title, type, dates, tags, status and relevance); brain validate checks that common metadata and wiki links. brain travel validate checks the domain formats and their references. Custom frontmatter is preserved. These readers never rewrite content.

An existing travel document needs no new fields. It may add places and visits using the same references and visit records as a trip.

type: trip
title: Ithaca headland
trip_status: done
places: [places/ithaca.md]
routes:
  - label: north-path
    source: own plan
    kind: planned
    gpx: routes/north.gpx
    primary: true
visits:
  - id: homecoming
    date: null
    party: [Odysseus, Penelope]
    route: north-path
    track: recordings/homecoming.gpx
    actual: {distance_km: null, ascent_m: null, duration_s: null}
    verdict: A route to revisit
    photos: [photos/headland.jpg]
cover: photos/headland.jpg

The example's referenced assets must exist beside that trip document in the named subdirectories. Asset/track paths are relative to their document; ../ can reach a shared asset within the brain. Resolution follows symlinks and refuses escapes from the root.

  • trip_status is proposed, done or dismissed. done requires at least one visit; its date may remain unknown. Changing state never deletes visits. The core status remains the independent active/archived/draft lifecycle.
  • visits[].id is a stable lowercase slug, unique within the document. Repeated visits, including two on an unknown or identical date, have separate IDs. Never derive identity from the date or party.
  • A visit's date is a real YYYY-MM-DD, null, or omitted. Its party, route, actual and other descriptive fields may be omitted when unknown. actual holds optional nonnegative distance_km, ascent_m, duration_s; each can remain null. It describes the recording associated with track.
  • Route labels are unique within the trip. Visits reference those exact labels. A nonempty routes list has exactly one primary: true. kind is planned, recorded or reference; source is free descriptive provenance. Optional url is HTTP(S), gpx is a relative asset, and distance_km/ascent_m are optional nonnegative derived metrics or null.
  • places on a journey/trip applies to its visits; a visit can also name individual places. References use complete root-relative .md paths.
type: place
title: Ithaca
place_kind: spot
coordinates: null
visits:
  - document: trips/ithaca-headland.md
    visit: homecoming

place_kind is country, city, town or spot. Optional parent_place uses a root-relative place document path. Countries are roots; cities/towns may belong to a country, and spots to a country or city/town. Missing parents and cycles are errors. A standalone place can omit its parent.

Coordinates are omitted, null, or {lat, lon} in latitude −90…90 and longitude −180…180. (0, 0) is valid. Unknown coordinates stay unknown. No geocoding or network request occurs.

The document path identifies a place. A visit reference is the pair of its owning journey/trip's path and visit ID. Record an event once in its owning document and reference it elsewhere. summarizePlaceVisits unions explicit place references with journey/trip place links and deduplicates that pair; titles, route labels and dates are not deduplication keys.

visit_count, first_visit, last_visit are derived display fields. Their stored values are ignored when computing a summary. Counts come from canonical events. First/last are null when any included visit's date is unknown, or when there are no visits; the individual known dates remain available. Later registry generation consumes these records.

Photo copies

brain travel photo originals/ithaca.jpg originals/scheria.png --to trips/ithaca/photos --json

Source paths resolve from the brain root; an explicit absolute path may read a camera file elsewhere. Originals are read only and never stored by this command. The output directory must remain inside the brain root. Symlinked output directories and filenames are refused, including links whose targets are inside the root.

Each supported raster becomes a JPEG with a maximum 1600-pixel long edge, preserved aspect ratio and no upscaling. EXIF orientation is applied to actual pixels. Transparent areas become white. The encoder converts to sRGB and uses mozjpeg at quality 80. The fresh copy contains no input EXIF, GPS, XMP, ICC, IPTC or comments; ordinary JPEG structural headers remain. Animated and multi-page images, vector documents and unsupported codecs are refused. Available codecs depend on the installed Sharp/libvips build; HEIC support is not guaranteed.

Copies use the source basename with .jpg. An occupied name receives -2, -3 and so on, including when the source already sits in the output directory. A completed temporary sibling is published with an exclusive hard link, so an arriving destination cannot be overwritten and readers never see partial bytes. The output filesystem must support hard links; errors are reported rather than falling back to an overwrite-prone writer. New files have mode 0600 where the filesystem supports it. Temporary siblings are normally removed; if cleanup fails after publication, the completed copy is still reported as successful and a hidden sibling may remain.

--json returns {photo: {files, errors}}. Each success contains:

| Field | Meaning | | --- | --- | | source | Input argument, unchanged. | | output | Brain-root-relative JPEG path, using forward slashes. | | width, height, bytes | Actual encoded dimensions and byte length. | | captured_at | Original EXIF camera time as ISO text, or null. | | location | Original EXIF {lat, lon} in decimal degrees, or null. |

Capture time retains a written fractional second and UTC offset. Without an offset it stays a local time such as 2026-07-15T12:34:56; the process timezone never supplies one. Missing or impossible dates remain null. Coordinates require a finite latitude/longitude pair within their normal ranges; zero is valid. These values are returned separately, never embedded in the copy or written to a sidecar.

Errors contain {source, message}, with the original input argument and a prose diagnostic. Inputs are processed in argument order; a failed input does not prevent other copies. Exit 0 means all succeeded, 2 means one or more input jobs failed, and 1 means invalid arguments or an unusable output directory before processing (stderr diagnostic, no success envelope). --human shows paths and dimensions, with errors on stderr. Put flags before -- when passing a filename that begins with a dash.

Sharp and exifr load only when processing photos. Sharp ships native codec binaries for its supported platforms; see its installation requirements and mozjpeg options.

Upgrade from speaking

This release moves travel taxonomy, planning and configuration out of speaking. Before indexing an upgraded speaking-only brain:

  1. Install the matching travel package when this release is available.
  2. Add "@schlessera/brain-module-travel": {} to modules, keeping speaking enabled if you use its workflows.
  3. Run brain travel migrate --dry-run --json, then brain travel migrate --json. Review and commit the changed config.
  4. Start a fresh process/session and run brain module lint travel, brain validate, brain travel validate, and brain skills sync.

Migration moves the complete legacy travelParty value, including roles and requirements-document paths. It leaves the speaking entry and all content bytes untouched. Rerunning is a no-op. Equal values in both module blocks remove only the legacy field; conflicting values change nothing and require review. Speaking accepts the deprecated field during this transition and warns when a nonempty value is still present.

The deterministic writer supports JSON and directly exported TypeScript object literals, including defineConfig({...}). It edits only the owned field and target module entry. Duplicate keys, computed/spread/shorthand targets and dynamic party expressions are refused with manual migration instructions. Preserve the complete value when migrating those configurations by hand. Symlinked configs are refused; replacement is atomic and retains the file's permissions.

Module settings #528 owns per-module JSON settings and their shared CLI/API writer. Until that path ships, this migration operates on the existing canonical config. If settings/speaking.json or settings/travel.json already exists, it refuses the migration so their precedence can be reviewed explicitly; it never overwrites those files.

CLI and library

  • brain travel route <url|file> --to <dir> [--trim-start-m N] [--trim-end-m N] [--json]: imports GPX or public Komoot geometry and writes a new normalized GPX. See route import below.
  • brain travel validate [--json]: {validation: {valid, files, issues}}, with issues {file, level: "error", message}. Exit 0 means valid, 1 means domain errors. files counts successfully parsed domain documents.
  • brain travel migrate [--dry-run] [--json]: {migration: {path, changed, dry_run}}. changed reports whether the migration has an edit; dry run leaves the file untouched. Refusals exit 1 with an actionable stderr message.
  • brain travel photo <files> --to <dir> [--json]: {photo: {files, errors}}; see Photo copies for fields, failure behavior and output rules.
  • The root export supplies the manifest/config schema, content schemas, parseTravelDocument, readTravelCorpus and summarizePlaceVisits with their corresponding types. ./module supplies the manifest for the loader.

Day-trip/place workflows and generated registries are covered by #569.

Route import

brain travel route recordings/odysseus.gpx --to routes --json
brain travel route recordings/odysseus.gpx --to routes --trim-start-m 100 --trim-end-m 100 --json

Input and output paths are relative to the brain root (absolute contained paths also work). The original stays untouched. An occupied output name gets -2, -3, and so on; an existing file is never replaced. Output paths in the response are relative to the brain root. Importing a recording does not attach it to a trip or change a Markdown document.

Supported inputs are local GPX 1.0/1.1 files, direct HTTP(S) GPX URLs, and public komoot.com/tour/<id> or komoot.com/smarttour/<id> pages, including www and locale prefixes. Public pages use anonymous requests and discard query/share tokens. Private, deleted, login-only, malformed and unsupported pages refuse with an error. Komoot page geometry can have different sampling from its exported GPX; metrics describe the imported points. The package does not request account credentials or use Komoot's authenticated export.

HTTP uses the shared scrape client's User-Agent, robots.txt enforcement, redirect checks and pacing. Embedded credentials, unsupported schemes and hosts resolving to private/reserved addresses are refused before dispatch; redirect targets receive the same checks. Files and responses are capped at 20 MiB and geometry at 200,000 points. XML doctypes/entities and unsupported encodings are refused. These address checks do not pin DNS answers to the HTTP connection. Robots lookup failures retain the shared client's documented permissive policy.

GPX track segments take precedence over route elements. Gaps between segments add no distance, ascent or duration; segments with fewer than two points are omitted with a warning. Missing/invalid elevations and timestamps stay unknown. Invalid latitude/longitude rejects the input. Source metadata, waypoints, links and extensions are discarded. The output contains only retained track points, optional elevations/timestamps and recomputed bounds.

Metrics derive from the points written to GPX:

  • Distance is the sum of spherical great-circle edges (mean Earth radius 6,371,008.8 m), reported in kilometres to six decimal places. Coordinates are written to nine decimal places, elevations to three.
  • Ascent uses a three-point median for interior elevations, retaining each segment's endpoints, then a 3 m hysteresis: changes smaller than 3 m from the last accepted elevation are ignored; accepted upward changes add to ascent. This resets at every segment. Ascent and altitude extrema are null if any retained point lacks a valid elevation. Altitude extrema use retained unsmoothed values, in metres.
  • A continuous single segment is a loop when its endpoints are within both 30 m and 5% of its travelled distance; otherwise it is one_way. Multiple segments have unknown shape.
  • Recorded duration sums each segment's last minus first timestamp, in seconds to three decimals. Every retained timestamp must be valid and nondecreasing within its segment. Gaps are excluded. Missing or reversed timestamps yield null. Komoot's relative t values are not absolute recording timestamps and are discarded, so its duration stays null.

Trimming measures metres along those same edges, excluding segment gaps. Boundaries interpolate on the great circle; known elevations and ordered timestamps interpolate linearly. Unknown endpoint data stays unknown. Nonzero cuts must be at least 1 mm. Trimming away the entire route, a zero-distance input, an ambiguous antipodal cut or a retained distance below 1 mm reporting precision refuses without writing. For the documented sphere, each cut's position is within 1 cm after serialization and reported distance rounding; this is a computational tolerance, not a GPS accuracy claim. Bounds and all metrics are rebuilt after trimming. Removing departure/end points does not hide a location that the retained track visits again.

Source access evidence

Checked 2026-10-01: normal anonymous HTTP requests to Komoot tour and smarttour pages expose page._embedded.tour._embedded.coordinates.items in the JSON string passed to kmtBoot.setProps. The parser decodes that JSON without running JavaScript. Deterministic CLI fixtures use the observed structure with invented Odysseus geometry. Komoot documents that its official GPX export requires an unlocked region. The GPX schema specifies WGS84 positions, metric elevations and continuous track segments.

Outdooractive import remains an unmet requirement of #568. Its public page offers a login-gated GPX export; robots.txt disallows GPX download paths and /api/*, including the geometry API its public map uses. Its documented Data API requires a project/API key. The robots-wins decision requires written site permission before using a disallowed path. Rendering the public map to observe its response shape grants no importing permission. Outdooractive URLs therefore give an actionable refusal. A user-exported local GPX can be processed, but that does not complete the Outdooractive criterion. #568 records the exact permission and completion evidence needed.