{"id":48486430,"url":"https://github.com/azat-io/pinbook","last_synced_at":"2026-04-07T10:01:49.616Z","repository":{"id":347427195,"uuid":"1194028477","full_name":"azat-io/pinbook","owner":"azat-io","description":"📌 YAML-first CLI for building Google My Maps-ready KML","archived":false,"fork":false,"pushed_at":"2026-04-05T12:40:24.000Z","size":6951,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-05T14:27:14.083Z","etag":null,"topics":["cli","google-my-maps","kml","maps","travel","yaml"],"latest_commit_sha":null,"homepage":"https://google.com/maps/d/u/0/viewer?mid=1am89OiTz6iQ7sreXEYzyjCbvvY6DR9I","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/azat-io.png","metadata":{"files":{"readme":"readme.md","changelog":"changelog.config.ts","contributing":"contributing.md","funding":null,"license":"license.md","code_of_conduct":".github/code_of_conduct.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":".github/security.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-27T20:54:58.000Z","updated_at":"2026-04-05T12:40:28.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/azat-io/pinbook","commit_stats":null,"previous_names":["azat-io/pinbook"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/azat-io/pinbook","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azat-io%2Fpinbook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azat-io%2Fpinbook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azat-io%2Fpinbook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azat-io%2Fpinbook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/azat-io","download_url":"https://codeload.github.com/azat-io/pinbook/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azat-io%2Fpinbook/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31508282,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T03:10:19.677Z","status":"ssl_error","status_checked_at":"2026-04-07T03:10:13.982Z","response_time":105,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","google-my-maps","kml","maps","travel","yaml"],"created_at":"2026-04-07T10:01:48.398Z","updated_at":"2026-04-07T10:01:49.607Z","avatar_url":"https://github.com/azat-io.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Pinbook\n\n\u003cimg\n  src=\"https://raw.githubusercontent.com/azat-io/pinbook/main/assets/logo.svg\"\n  alt=\"Pinbook logo\"\n  width=\"160\"\n  height=\"160\"\n  align=\"right\"\n/\u003e\n\n[![Version](https://img.shields.io/npm/v/pinbook.svg?color=fff\u0026labelColor=c83636)](https://npmjs.com/package/pinbook)\n[![Code Coverage](https://img.shields.io/codecov/c/github/azat-io/pinbook.svg?color=fff\u0026labelColor=c83636)](https://codecov.io/gh/azat-io/pinbook)\n[![GitHub License](https://img.shields.io/badge/license-MIT-232428.svg?color=fff\u0026labelColor=c83636)](https://github.com/azat-io/pinbook/blob/main/license.md)\n\nPinbook is a YAML-first CLI for building Google My Maps-ready KML.\n\nIt is designed for travel planning workflows where you want a map format that is\nreadable by humans, easy to generate with AI, and safe to keep in git.\n\nPlan the trip in plain YAML first, then turn it into a visual map.\n\n## Example\n\nPinbook can describe a real multi-city trip, not just a tiny demo config.\n\nThe canonical example in this repository covers Tokyo, Kyoto, Osaka, Nara, and\nHiroshima as a multi-file travel map:\n\n- Source config: [example/index.yaml](./example/index.yaml)\n- Live Google My Maps example:\n  [Japan Example Map](https://www.google.com/maps/d/viewer?mid=1am89OiTz6iQ7sreXEYzyjCbvvY6DR9I)\n\n## What It Does\n\n- Scaffolds a new map project with `pinbook create`\n- Reads an `index.yaml` map config\n- Geocodes addresses during build when coordinates are missing\n- Writes a `.pinbook/map.kml` file you can import into Google My Maps\n- Offers an optional Pinbook skill install for AI agents\n\n## Quick Start\n\n```bash\nnpx pinbook create my-map\ncd my-map\n\n# Edit index.yaml and add at least one pin\n\n# Optional: authorize Google Drive uploads\n# when `photo` points to local files\n# pnpm exec pinbook drive-auth\n\npnpm install\npnpm build\n```\n\nThen import `.pinbook/map.kml` into Google My Maps.\n\nOptional AI skill:\n\n```bash\nnpx skills add azat-io/pinbook\n```\n\n## Workflow\n\n1. Create a new project with `pinbook create`.\n2. Edit `index.yaml`.\n3. Run `pnpm build`.\n4. Import `.pinbook/map.kml` into Google My Maps.\n5. Repeat as the trip plan changes.\n\n## Multi-File Maps\n\nPinbook also supports root-level `imports` so one map can be split across\nmultiple YAML files.\n\nThis works well for trips organized by city:\n\n```text\nindex.yaml\ncities/\n  tokyo/\n    food.yaml\n    sights.yaml\n  kyoto.yaml\n  osaka.yaml\n```\n\nTwo common conventions:\n\n- **layers for categories** — group by `food`, `sights`, `entertainment` (shown\n  below)\n- **layers for geography** — group by city such as `tokyo`, `kyoto`, `osaka`\n  (used in the [canonical example](./example/index.yaml))\n\nBoth work well. Pick whichever makes the map easier to read at a glance.\n\nExample root config with category-based layers:\n\n```yaml\nmap:\n  title: Japan Trip\n\nlayers:\n  - id: food\n    title: Food\n\n  - id: sights\n    title: Sights\n\nimports:\n  - ./cities/tokyo/*.yaml\n  - ./cities/kyoto.yaml\n  - ./cities/osaka.yaml\n\npins: []\n```\n\nImported files may contain only `pins`:\n\n```yaml\npins:\n  - id: onibus-coffee-nakameguro\n    title: Onibus Coffee Nakameguro\n    address: Onibus Coffee Nakameguro, Tokyo\n    layer: food\n```\n\n## Minimal Example\n\n```yaml\nmap:\n  title: Tokyo First Week\n  description: \u003e\n    A first-pass map for a week in Tokyo.\n\nlayers:\n  - id: sights\n    title: Sights\n\npins:\n  - id: senso-ji\n    title: Senso-ji\n    address: Senso-ji Temple, Asakusa, Tokyo\n    layer: sights\n    color: red-500\n    icon: places-viewpoint\n    description: \u003e\n      Good early-morning stop before the street gets crowded.\n    photo: https://example.com/photos/senso-ji.jpg\n```\n\n## Config Shape\n\nPinbook maps use four top-level keys:\n\n- `map` for the map title and optional description\n- `layers` for optional logical groups such as `food`, `stay`, or `sights`\n- `imports` for optional relative YAML paths or glob patterns\n- `pins` for the actual places that should appear on the map\n\nEach pin should describe a real place using either:\n\n- `address` when a clear human-readable address exists\n- `coords` when no reliable address is available\n\nPins can also define `color`, `icon`, `description`, `layer`, and `photo`.\n\n`imports` is root-only. Imported files may contain only `pins`.\n\n## Schema Stability\n\nPinbook treats the current YAML shape as the public map format.\n\nFor the future `1.x` line:\n\n- all current root keys and documented fields are considered stable\n- new fields may be added in minor releases\n- removing, renaming, or changing the meaning of an existing field requires a\n  major release\n\n## Build Behavior\n\n- If a pin includes `coords`, Pinbook uses them as the final coordinates and\n  does not geocode the pin.\n- If a pin includes `address` and does not include `coords`, Pinbook may call\n  the Google Geocoding API during build.\n- If a pin includes both `address` and `coords`, `coords` are authoritative and\n  are used as the final coordinates.\n- If `imports` is present, Pinbook expands the imported YAML files before final\n  validation and build.\n- Resolved addresses are stored in the local resolution cache so later builds\n  stay faster and more repeatable.\n\n## Photos\n\n`photo` is supported as either:\n\n- a single full public `http://` or `https://` image URL\n- a single local image path such as `./photos/senso-ji.jpg`\n- a list that mixes public URLs and local paths\n\nDuring build, Pinbook includes those images in the generated placemark\ndescription so they can appear in Google My Maps after import.\n\nWhen `photo` points to a local file, Pinbook uploads it to Google Drive during\nbuild and rewrites it to a public URL before generating KML.\n\nBefore upload, Pinbook automatically normalizes local photos by rotating them\nwhen the image says it should be shown differently, cropping from the center to\na fixed 3:2 frame, resizing them to a consistent size, and converting them to\nWebP.\n\nBy default, Pinbook creates or reuses this folder structure in Google Drive:\n\n```text\nPinbook/\n  \u003cMap title\u003e/\n```\n\nFor example, a map with `map.title: Japan Trip` uploads local photos into:\n\n```text\nPinbook/Japan Trip\n```\n\nIf `GOOGLE_DRIVE_FOLDER_ID` is set, Pinbook uses that folder as the parent and\ncreates or reuses:\n\n```text\n\u003cConfigured folder\u003e/\u003cMap title\u003e\n```\n\nFor imported YAML files, local photo paths are resolved relative to the file\nthat declared them.\n\nTo authorize local photo uploads:\n\n```bash\npnpm exec pinbook drive-auth\n```\n\nThat flow stores the Google Drive OAuth client ID, client secret, and refresh\ntoken in the local project `.env` file.\n\n## Geocoding\n\nIf a pin has an `address` but no `coords`, Pinbook geocodes the address during\nbuild.\n\nTo do that, set `GOOGLE_MAPS_API_KEY` in your environment or in a local `.env`\nfile inside the map project. In an interactive terminal, Pinbook can also ask\nfor the key and save it for you.\n\nResolved addresses are cached locally at\n`node_modules/.cache/pinbook/cache.json` so repeated builds stay fast and\nstable.\n\n## Google Drive Auth\n\nPinbook uses two separate Google integrations:\n\n- `GOOGLE_MAPS_API_KEY` for address geocoding\n- Google Drive OAuth credentials for uploading local photo files\n\nGoogle Drive uploads do **not** use a Drive API key.\n\nRun `pinbook drive-auth` to save a refresh token locally, or provide these\nvariables in the local `.env` file:\n\n```bash\nGOOGLE_DRIVE_CLIENT_ID=your-google-oauth-client-id\nGOOGLE_DRIVE_CLIENT_SECRET=your-google-oauth-client-secret\nGOOGLE_DRIVE_REFRESH_TOKEN=your-google-drive-refresh-token\n# Optional parent folder. Pinbook will then upload into\n# \u003cthat folder\u003e/\u003cMap title\u003e instead of Pinbook/\u003cMap title\u003e.\nGOOGLE_DRIVE_FOLDER_ID=your-google-drive-folder-id\n```\n\nUploaded photo metadata is cached locally at\n`node_modules/.cache/pinbook/photo-cache.json` so unchanged files are not\nuploaded again on every build.\n\n`pinbook drive-auth` stores these values in the local project `.env` file next\nto your YAML config and ensures that `.env` is ignored by Git.\n\n## Google Drive Setup\n\nUse this once per Pinbook map project when you want to reference local photo\npaths such as `./photos/senso-ji.jpg`.\n\n1. Create or choose a Google Cloud project.\n2. Enable the Google Drive API for that project.\n3. Open `Google Auth Platform`.\n4. Complete the initial app setup in `Branding`. For personal use, a simple app\n   name and support email are enough.\n5. Open `Audience`. If you are using a personal Google account, choose\n   `External`.\n6. Decide whether the app should stay in `Testing` or move to `Production`.\n   `Testing` is fine for quick experiments, but Google limits it to test users\n   and test-user authorizations expire after 7 days. `Production` is better for\n   long-lived personal use.\n7. Open `Clients` and create an OAuth client with application type\n   `Desktop app`.\n8. In your Pinbook project, run:\n\n```bash\npnpm exec pinbook drive-auth\n```\n\n9. Paste the `Client ID` and `Client Secret`.\n10. Open the Google URL printed by Pinbook and finish the sign-in flow.\n11. Wait for the terminal message:\n\n```text\nGoogle Drive auth saved to the local .env file.\n```\n\nAfter that, `pnpm build` can upload local photos automatically.\n\n## Google Drive Notes\n\n- `pinbook drive-auth` must run in a local interactive terminal with access to a\n  browser. Google explicitly documents desktop OAuth as a local flow.\n- Pinbook uses the `https://www.googleapis.com/auth/drive.file` scope. Google\n  classifies it as `Recommended / Non-sensitive`.\n- The OAuth exchange can take a little while after the browser says\n  `Pinbook authorization complete`. The refresh token is not saved until the\n  terminal prints the success message.\n\n## Google Drive Troubleshooting\n\n- `access_denied` after sign-in usually means the app is still in `Testing` and\n  your Google account was not added as a test user in `Audience`.\n- If local photos are uploaded into Drive root, `GOOGLE_DRIVE_FOLDER_ID` is not\n  set and the map title folder has not been created yet. Pinbook now creates\n  `Pinbook/\u003cMap title\u003e` automatically on the next build.\n- If unchanged photos are already in\n  `node_modules/.cache/pinbook/photo-cache.json`, Pinbook reuses their public\n  URLs and skips re-uploading them.\n- If Pinbook reports that `photo-cache.json` is invalid after an upgrade, delete\n  it and run build again so the cache can be recreated in the new format.\n\n## Compatibility Target\n\nPinbook targets manual KML import into Google My Maps.\n\nThe generated KML is designed around that workflow, including Pinbook's color,\nicon, layer, and photo conventions.\n\n## Known Limitations\n\n- Import behavior ultimately depends on Google My Maps.\n- Rich import details such as folders, custom icon rendering, photos, and\n  description HTML may vary with My Maps behavior.\n- Large maps may hit Google My Maps import limits.\n- Address-based builds depend on network access to Google Geocoding unless the\n  required coordinates are already present in the local cache.\n- Local photo uploads depend on Google Drive OAuth and public Drive download\n  links.\n- Imported files may contain only `pins`; nested imports are not supported.\n\n## AI-Assisted Workflow\n\nPinbook publishes a repo-level skill that can be installed into coding agents\nwith:\n\n```bash\nnpx skills add azat-io/pinbook\n```\n\nThat gives AI agents a reusable Pinbook reference for:\n\n- the expected YAML shape\n- supported fields\n- color and icon conventions\n- a consistent authoring style for map configs\n\n## Current Scope\n\nPinbook currently focuses on one job: building KML from YAML for Google My Maps.\n\nThat means:\n\n- it exports KML for manual import into Google My Maps\n- it does not sync directly with Google My Maps\n- it can upload local pin photos to Google Drive during build\n- it is optimized for travel-planning style maps with readable YAML configs\n\n## Contributing\n\nSee\n[Contributing Guide](https://github.com/azat-io/pinbook/blob/main/contributing.md).\n\n## License\n\nMIT \u0026copy; [Azat S.](https://azat.io)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fazat-io%2Fpinbook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fazat-io%2Fpinbook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fazat-io%2Fpinbook/lists"}