tablefacts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.env.example +14 -0
  2. package/CHANGELOG.md +12 -0
  3. package/LICENSE +21 -0
  4. package/README.md +313 -0
  5. package/bin/tablefacts.mjs +28 -0
  6. package/package.json +77 -0
  7. package/src/index.mjs +52 -0
  8. package/src/instagram/README.md +135 -0
  9. package/src/instagram/download.mjs +62 -0
  10. package/src/instagram/index.mjs +355 -0
  11. package/src/instagram/links.mjs +55 -0
  12. package/src/instagram/record.mjs +18 -0
  13. package/src/lib/edge.mjs +49 -0
  14. package/src/lib/env.mjs +35 -0
  15. package/src/lib/errors.mjs +39 -0
  16. package/src/lib/files.mjs +10 -0
  17. package/src/lib/images.mjs +20 -0
  18. package/src/lib/log.mjs +17 -0
  19. package/src/lib/photos.mjs +42 -0
  20. package/src/lib/playwright.mjs +13 -0
  21. package/src/lib/project.mjs +42 -0
  22. package/src/lib/text.mjs +7 -0
  23. package/src/lib/types.mjs +247 -0
  24. package/src/menu/README.md +97 -0
  25. package/src/menu/cluvi/config.mjs +33 -0
  26. package/src/menu/cluvi/extract.mjs +24 -0
  27. package/src/menu/cluvi/import.mjs +30 -0
  28. package/src/menu/cluvi/source.mjs +156 -0
  29. package/src/menu/index.mjs +9 -0
  30. package/src/menu/lib/db.mjs +119 -0
  31. package/src/menu/lib/import.mjs +126 -0
  32. package/src/menu/lib/menu.mjs +95 -0
  33. package/src/menu/lib/run.mjs +83 -0
  34. package/src/menu/raw/config.mjs +38 -0
  35. package/src/menu/raw/extract.mjs +72 -0
  36. package/src/menu/raw/import.mjs +99 -0
  37. package/src/menu/raw/normalize.mjs +120 -0
  38. package/src/menu/raw/source.mjs +116 -0
  39. package/src/menu/raw/vision.mjs +252 -0
  40. package/src/research/README.md +69 -0
  41. package/src/research/index.mjs +147 -0
  42. package/src/research/lib/google.mjs +93 -0
  43. package/src/research/lib/hours.mjs +109 -0
  44. package/src/research/lib/merge.mjs +119 -0
  45. package/src/research/lib/osm.mjs +49 -0
  46. package/src/research/lib/report.mjs +118 -0
  47. package/src/research/lib/social.mjs +49 -0
  48. package/src/research/lib/util.mjs +104 -0
  49. package/src/research/lib/website.mjs +285 -0
  50. package/src/research/research.mjs +67 -0
  51. package/src/tripadvisor/README.md +78 -0
  52. package/src/tripadvisor/index.mjs +178 -0
  53. package/src/tripadvisor/links.mjs +78 -0
  54. package/src/tripadvisor/photos.mjs +55 -0
  55. package/types/index.d.mts +65 -0
  56. package/types/instagram/download.d.mts +1 -0
  57. package/types/instagram/index.d.mts +13 -0
  58. package/types/instagram/links.d.mts +14 -0
  59. package/types/instagram/record.d.mts +1 -0
  60. package/types/lib/edge.d.mts +14 -0
  61. package/types/lib/env.d.mts +12 -0
  62. package/types/lib/errors.d.mts +25 -0
  63. package/types/lib/files.d.mts +1 -0
  64. package/types/lib/images.d.mts +6 -0
  65. package/types/lib/log.d.mts +5 -0
  66. package/types/lib/photos.d.mts +30 -0
  67. package/types/lib/playwright.d.mts +1677 -0
  68. package/types/lib/project.d.mts +32 -0
  69. package/types/lib/text.d.mts +4 -0
  70. package/types/lib/types.d.mts +668 -0
  71. package/types/menu/cluvi/config.d.mts +13 -0
  72. package/types/menu/cluvi/extract.d.mts +2 -0
  73. package/types/menu/cluvi/import.d.mts +35 -0
  74. package/types/menu/cluvi/source.d.mts +37 -0
  75. package/types/menu/index.d.mts +6 -0
  76. package/types/menu/lib/db.d.mts +23 -0
  77. package/types/menu/lib/import.d.mts +11 -0
  78. package/types/menu/lib/menu.d.mts +24 -0
  79. package/types/menu/lib/run.d.mts +27 -0
  80. package/types/menu/raw/config.d.mts +18 -0
  81. package/types/menu/raw/extract.d.mts +2 -0
  82. package/types/menu/raw/import.d.mts +64 -0
  83. package/types/menu/raw/normalize.d.mts +29 -0
  84. package/types/menu/raw/source.d.mts +19 -0
  85. package/types/menu/raw/vision.d.mts +13 -0
  86. package/types/research/index.d.mts +9 -0
  87. package/types/research/lib/google.d.mts +12 -0
  88. package/types/research/lib/hours.d.mts +22 -0
  89. package/types/research/lib/merge.d.mts +87 -0
  90. package/types/research/lib/osm.d.mts +36 -0
  91. package/types/research/lib/report.d.mts +6 -0
  92. package/types/research/lib/social.d.mts +118 -0
  93. package/types/research/lib/util.d.mts +45 -0
  94. package/types/research/lib/website.d.mts +283 -0
  95. package/types/research/research.d.mts +1 -0
  96. package/types/tripadvisor/index.d.mts +11 -0
  97. package/types/tripadvisor/links.d.mts +13 -0
  98. package/types/tripadvisor/photos.d.mts +1 -0
package/.env.example ADDED
@@ -0,0 +1,14 @@
1
+ SUPABASE_DB_URL=SUPABASE_DB_POOLER_URL
2
+
3
+ # Menus that are only pictures (tablefacts menu raw) are read by a vision model. Fill in
4
+ # the key of the provider you use: Claude, Gemini or Groq
5
+ ANTHROPIC_API_KEY=
6
+ GEMINI_API_KEY=
7
+ GROQ_API_KEY=
8
+
9
+ # Which one reads them when --provider is not given: anthropic (the default), gemini or groq
10
+ MENU_VISION_PROVIDER=
11
+
12
+ # Restaurant research (tablefacts research) reads Google Maps through the Places API (New).
13
+ # Optional: without it OpenStreetMap and the web pages still work, with fewer facts.
14
+ GOOGLE_PLACES_API_KEY=
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First release as an npm library.
6
+
7
+ - Every tool is a function: `research`, `downloadInstagram`, `downloadTripadvisor`, `importMenu`,
8
+ `importCluvi`, `importImageMenu`, `listMenuImages`. The `tablefacts` command is a thin wrapper over them.
9
+ - One logger contract (`log(message, level)`), an explicit `env` option, `loadEnv()` and `projectDir`.
10
+ - `TablefactsError` with codes `EUSAGE`, `ECONFIG`, `EDEPENDENCY` and `EFAILED`.
11
+ - Types generated from JSDoc (`npm run build:types`).
12
+ - Playwright is an optional peer dependency.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mateo Arias
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,313 @@
1
+ # tablefacts
2
+
3
+ Data extraction tools for restaurant sites, shared by every Cannario template. Give it a restaurant's name and
4
+ place and it gathers the public facts; point it at an Instagram or TripAdvisor page and it downloads the photos;
5
+ point it at a Cluvi menu (or pictures of a paper menu) and it loads the menu into Supabase.
6
+
7
+ Everything works two ways: as a **command line** (`tablefacts <tool>`) and as a **JavaScript library**
8
+ (`import { research } from 'tablefacts'`). Both do the same work; the CLI is a thin layer over the functions.
9
+
10
+ | Tool | Command | Library function | Docs |
11
+ | --- | --- | --- | --- |
12
+ | Research | `tablefacts research "<name>" "<city>"` | `research()` | [src/research](src/research/README.md) |
13
+ | Instagram photos | `tablefacts photos instagram` | `downloadInstagram()` | [src/instagram](src/instagram/README.md) |
14
+ | TripAdvisor photos | `tablefacts photos tripadvisor` | `downloadTripadvisor()` | [src/tripadvisor](src/tripadvisor/README.md) |
15
+ | Menu from Cluvi | `tablefacts menu cluvi` | `importCluvi()` | [src/menu](src/menu/README.md) |
16
+ | Menu from pictures | `tablefacts menu raw` | `importImageMenu()` | [src/menu](src/menu/README.md) |
17
+
18
+ Contents: [Requirements](#requirements) · [Install](#install) · [Quick start](#quick-start) ·
19
+ [Command line](#command-line) · [Configuration](#configuration) · [Library](#use-it-as-a-library) ·
20
+ [Errors](#errors) · [Where things go](#where-things-go) · [Limits](#limits) · [Develop](#develop) ·
21
+ [Architecture](docs/ARCHITECTURE.md)
22
+
23
+ ## Requirements
24
+
25
+ - **Node.js 24 or newer** (`engines` in `package.json`). The tools use the built-in `fetch` and `util.parseArgs`.
26
+ - **Playwright** (optional) for the photo tools and `research --render`: `npm install --save-dev playwright`.
27
+ - **Microsoft Edge on Windows** for the photo tools. They attach to a normal Edge window because the sites
28
+ guard their pages with bot checks that automated browsers fail (see the photo tool READMEs). Starting Edge
29
+ for you only works with Edge installed in its usual `Program Files` folder; elsewhere, start it yourself with
30
+ `--remote-debugging-port=9222`.
31
+ - **A Supabase project** with the menu tables, only for the menu import (see [Supabase tables](#supabase-tables)).
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ npm install --save-dev tablefacts
37
+ npx tablefacts --help
38
+ ```
39
+
40
+ Playwright is an **optional** peer dependency. Without it the photo tools and `research --render` stop with an
41
+ `EDEPENDENCY` error that tells you to install it; everything else works.
42
+
43
+ ## Quick start
44
+
45
+ Run these from the folder of the project you are working on (see [Where things go](#where-things-go)).
46
+
47
+ ```bash
48
+ # 1. Find a restaurant's public facts. No key needed; GOOGLE_PLACES_API_KEY adds more.
49
+ npx tablefacts research "Gaucho" "Medellín, Colombia" --country CO
50
+ # -> .tablefacts/research/gaucho/report.md (read this first)
51
+
52
+ # 2. Try a menu import without writing anything.
53
+ npx tablefacts menu cluvi https://gaucho.cluvi.co/gaucho/maincategories --dry-run
54
+
55
+ # 3. List the photos of a TripAdvisor page without saving them (needs Playwright and Edge).
56
+ npx tablefacts photos tripadvisor --out ./photos --dry-run <restaurant-link>
57
+ ```
58
+
59
+ ## Command line
60
+
61
+ `tablefacts <tool> [options]`. Run any tool with `--help` for its own text. An unknown tool exits with code 2.
62
+
63
+ ### `research`
64
+
65
+ `tablefacts research "<name>" ["<city, country>"] [options]`
66
+
67
+ | Option | Meaning |
68
+ | --- | --- |
69
+ | `--country <ISO>` | Country code that narrows the search (`CO`, `MX`, `US`…) |
70
+ | `--website <url>` | The restaurant's site, if the search does not find it |
71
+ | `--instagram <handle\|url>` | Its Instagram |
72
+ | `--tripadvisor <url>` | Its TripAdvisor page |
73
+ | `--linktree <url>` | A Linktree or other link-in-bio page |
74
+ | `--photos <n>` | Also download up to *n* Google and *n* website photos (reference only) |
75
+ | `--render` | Render the website with Playwright (sites built in JavaScript) |
76
+ | `--no-google` | Skip Google even if `GOOGLE_PLACES_API_KEY` is set |
77
+ | `--out <dir>` | Output folder (default `.tablefacts/research/<slug>`; relative paths are relative to the project) |
78
+
79
+ Writes `report.md`, `profile.json` and `setup-answers.txt`. Details, sources and trust rules:
80
+ [src/research/README.md](src/research/README.md).
81
+
82
+ ### `photos instagram`
83
+
84
+ `tablefacts photos instagram --out <folder> [options] [post-link...]`
85
+
86
+ | Option | Meaning |
87
+ | --- | --- |
88
+ | `--out <folder>` | **Required.** Created if missing |
89
+ | `--file <path>` | Text file with one link per line (`#` comments allowed) |
90
+ | `--profile <link\|@name>` | Download a whole profile's photos (needs a toolzu account signed in) |
91
+ | `--pages <n\|all>` | With `--profile`, batches of posts to load (default 1) |
92
+ | `--cdp <url>` | Attach to Edge on this debugging address; started if nothing listens |
93
+ | `--edge-dir <dir>` | Profile folder of that Edge (default `C:\ig-edge`) |
94
+ | `--google` | Reach toolzu through a Google search first (library option: `viaGoogle`) |
95
+ | `--browser <msedge\|chrome>`, `--headed`, `--user-data-dir <dir>` | Launch a browser instead of attaching (fallback) |
96
+ | `--dry-run` | Print what would be saved, write nothing |
97
+ | `--debug` | Save the page HTML into `<out>/_debug` |
98
+
99
+ ### `photos tripadvisor`
100
+
101
+ `tablefacts photos tripadvisor --out <folder> [options] <restaurant-link>...`
102
+
103
+ | Option | Meaning |
104
+ | --- | --- |
105
+ | `--out <folder>` | **Required.** Created if missing |
106
+ | `--cdp <url>` | Edge debugging address (default `http://localhost:9222`) |
107
+ | `--edge-dir <dir>` | Profile folder of the Edge it starts (default `C:\ig-edge`) |
108
+ | `--max <n>` | Stop after *n* photos per restaurant |
109
+ | `--dry-run` / `--debug` | List without saving / keep the page HTML in `<out>/_debug` |
110
+
111
+ ### `menu cluvi` and `menu raw`
112
+
113
+ Both share these options (and replace the menu in Supabase unless `--dry-run`):
114
+
115
+ | Option | Meaning |
116
+ | --- | --- |
117
+ | `--dry-run` | Extract and check, show what would change, write nothing |
118
+ | `--json <file>` | Also save the extracted menu as JSON |
119
+ | `--replace-all` | Replace the whole menu, not only the categories in this import |
120
+ | `--force` | Write even if the import has fewer than half the products it replaces |
121
+
122
+ `tablefacts menu cluvi [menu-url] [--service on_table|delivery|take_away] [--lang es]`
123
+
124
+ `tablefacts menu raw [page-or-image-url...] [--list] [--only 1,3-5] [--provider anthropic|gemini|groq] [--model <id>] [--min-width 500] [--refresh]`
125
+
126
+ Both read the restaurant-specific part (URL, category mapping, currency) from a `config.mjs` next to the tool.
127
+ **The shipped configs hold another restaurant's values: edit them first.** See [src/menu/README.md](src/menu/README.md).
128
+
129
+ ### Exit codes
130
+
131
+ | Code | Meaning |
132
+ | --- | --- |
133
+ | `0` | Success |
134
+ | `1` | A failure: a source failed, an import was refused, or (photo tools) any download failed |
135
+ | `2` | Bad arguments (`EUSAGE`) from any tool, or an unknown tool |
136
+
137
+ The CLI prints a library error's message with option names turned into flags (`` `out` `` becomes `--out <folder>`).
138
+
139
+ ## Configuration
140
+
141
+ The CLI reads `<project>/.env` (then `<project>/data/.env`) before running; variables already in the
142
+ environment win. [.env.example](.env.example) is the template.
143
+
144
+ | Variable | Used by | Meaning |
145
+ | --- | --- | --- |
146
+ | `SUPABASE_DB_URL` | `menu cluvi`, `menu raw` | Postgres connection string (Supabase *Transaction pooler* URL, with the real password). **Bypasses row level security:** keep it in `.env`, never in a browser-exposed variable |
147
+ | `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `GROQ_API_KEY` | `menu raw` | Key of the vision provider that reads the pictures |
148
+ | `MENU_VISION_PROVIDER` | `menu raw` | Provider when `--provider` is not given: `anthropic` (default), `gemini` or `groq` |
149
+ | `GOOGLE_PLACES_API_KEY` | `research` | Optional. Enables Google Maps (Places API (New)); without it OpenStreetMap and the web pages still work |
150
+ | `TABLEFACTS_PROJECT` | all | Project folder to work on instead of the current one |
151
+
152
+ ## Use it as a library
153
+
154
+ Every tool is also a function. Functions are silent unless you pass `log`, never call `process.exit`, never
155
+ read `.env` for you (call `loadEnv()` or set the variables), and throw a [`TablefactsError`](#errors) on failure.
156
+ Types ship with the package (generated from the JSDoc into `types/`, `npm run build:types`); runnable examples are in [examples/](examples).
157
+
158
+ ```js
159
+ import { loadEnv, research } from 'tablefacts'
160
+
161
+ loadEnv() // reads <project>/.env into process.env, like the CLI does; never overrides variables already set
162
+ const { profile, files } = await research({ name: 'Gaucho', location: 'Medellín, Colombia', log: (message, level) => console.log(message) })
163
+ console.log(files.report)
164
+ ```
165
+
166
+ ### Project folder
167
+
168
+ Functions never assume where they run. Pass `projectDir` to say which project they work on; without it
169
+ they use `TABLEFACTS_PROJECT`, then the current folder. It decides where `.env` is read from (`loadEnv`), the
170
+ default output folder, browser profiles, the menu transcription cache, and where menu hints are read. Relative
171
+ `out`, `file`, `json` and `userDataDir` paths resolve against it too (absolute paths are used as they are).
172
+
173
+ ### Logging and environment
174
+
175
+ - **`log(message, level)`**: every function takes an optional `log`; `level` is `'info'` (the default),
176
+ `'warn'` (a problem the run goes on from) or `'error'` (a failure). Without `log` a function prints nothing.
177
+ A callback that takes only `message` works too.
178
+ - **`env`**: `research`, `importMenu`, `importCluvi` and `importImageMenu` read keys (`GOOGLE_PLACES_API_KEY`,
179
+ `SUPABASE_DB_URL`, the vision key, `MENU_VISION_PROVIDER`) from the `env` option, `process.env` by default. Pass
180
+ your own object to keep secrets out of the process environment. `loadEnv({ projectDir, files, env })` reads the
181
+ project's `.env` files (or exactly `files`) into `env` (default `process.env`) without overriding what is set,
182
+ and returns the files it read.
183
+
184
+ ### API
185
+
186
+ | Function | Does | Needs |
187
+ | --- | --- | --- |
188
+ | `research({ name, location, country, website, instagram, tripadvisor, linktree, photos, render, google, googleKey, env, out, projectDir, log })` | Gathers public facts; writes `profile.json`, `report.md`, `setup-answers.txt`. Returns `{ profile, notes, photos, outDir, files }` | `GOOGLE_PLACES_API_KEY` is optional; `render` needs Playwright |
189
+ | `downloadInstagram({ links, file, out, profile, pages, cdp, edgeDir, viaGoogle, browser, userDataDir, headed, dryRun, debug, projectDir, log })` | Downloads post or profile photos (give `links`, `file` or `profile`). Returns `{ saved, skipped, failed: [{ item, reason }], found? }` | Playwright |
190
+ | `downloadTripadvisor({ links, out, cdp, edgeDir, max, dryRun, debug, projectDir, log })` | Downloads a restaurant page's photos. Same result | Playwright |
191
+ | `importMenu({ menu, notes, title, dryRun, json, replaceAll, force, databaseUrl, env, projectDir, log })` | Validates a menu and writes it to Supabase. Returns `{ totals, notes, written, dryRun, database }`, `database` being `{ label, current: { categories, products, kept } }` or `null` when it was not reached | `SUPABASE_DB_URL` unless `dryRun` |
192
+ | `importCluvi({ url, service, lang, config, ...importMenu options })` | Reads a Cluvi menu, then `importMenu` | `SUPABASE_DB_URL` unless `dryRun` |
193
+ | `importImageMenu({ urls, only, provider, model, minWidth, refresh, apiKey, config, ...importMenu options })` | Transcribes menu pictures with a vision model, then `importMenu` | A vision key (see [Configuration](#configuration)) |
194
+ | `listMenuImages({ urls, only, minWidth, config })` | Lists the menu pictures found on pages (numbered as `--list` shows) | none |
195
+ | `loadEnv({ projectDir, files, env })` | Loads `.env` files into `env` (default `process.env`); returns the files it read | none |
196
+
197
+ Also exported: `validateMenu`, `countMenu`, `parsePrice`, `normalizePages`, `providers`, `defaultProvider`,
198
+ `projectRoot`, `workDir`, `workDirIn`, `envFiles`, `TablefactsError`, and the TypeScript types of every option and
199
+ result (`ResearchOptions`, `PhotoSummary`, `ImportResult`, `Menu`, `Log`, `Env`...).
200
+
201
+ `config` (`importCluvi`, `importImageMenu`, `listMenuImages`) replaces the restaurant-specific `config.mjs` of that
202
+ source for one call, so a script can import a menu without editing the package. See [src/menu/README.md](src/menu/README.md).
203
+
204
+ `importMenu` takes a menu you built yourself, so you can feed it any source:
205
+
206
+ ```js
207
+ import { importMenu } from 'tablefacts'
208
+
209
+ const menu = [{
210
+ slug: 'cocina', name: 'Comida',
211
+ sections: [{ name: 'ENTRADAS', products: [
212
+ { name: 'Empanadas', description: null, price: 12000, currency: 'COP', image_url: null, recommended: false },
213
+ ] }],
214
+ }]
215
+ const result = await importMenu({ menu, dryRun: true, log: console.log })
216
+ console.log(result.totals) // { categories: 1, sections: 1, products: 1, withImage: 0 }
217
+ ```
218
+
219
+ Array order is display order. `price` is a number, `currency` an ISO 4217 code, `image_url` an `https://` URL
220
+ or empty. `validateMenu` throws an `EFAILED` error naming the problems it finds.
221
+
222
+ ### Errors
223
+
224
+ Failures the library raises on purpose throw `TablefactsError`, which has a `code`:
225
+
226
+ | Code | Meaning |
227
+ | --- | --- |
228
+ | `EUSAGE` | A missing or invalid argument (no `out`, no valid links, no `name`). CLIs exit with `2` |
229
+ | `ECONFIG` | Missing configuration: database URL, API key, unknown provider, no menu URL |
230
+ | `EDEPENDENCY` | An optional dependency is not installed (Playwright) |
231
+ | `EFAILED` | A source failed or was blocked, data did not validate, or an import was refused (invalid menu, no products, far fewer products than it replaces) |
232
+
233
+ When the error is about one option, `err.option` names it (`'out'`, `'links'`, `'force'`...) and the message writes
234
+ it in backticks (`` `out` is required ``). The CLI swaps those for its flags (`--out <folder>`); library callers see
235
+ the option names. `err.cause` holds the underlying error when there is one.
236
+
237
+ ```js
238
+ import { TablefactsError, importCluvi } from 'tablefacts'
239
+
240
+ try {
241
+ await importCluvi({ url, dryRun: true })
242
+ } catch (err) {
243
+ if (err instanceof TablefactsError && err.code === 'ECONFIG') console.error('Set SUPABASE_DB_URL')
244
+ else throw err
245
+ }
246
+ ```
247
+
248
+ Anything else that is thrown (a bug, an unwrapped network error) is not a `TablefactsError`: rethrow it.
249
+
250
+ ## Where things go
251
+
252
+ The tools work on **the project in the current folder** (or the one in `TABLEFACTS_PROJECT`), never on the
253
+ folder tablefacts is installed in, so one install serves every template.
254
+
255
+ | What | Where |
256
+ | --- | --- |
257
+ | `.env` | `<project>/.env`, then `<project>/data/.env` (older templates) |
258
+ | Research output | `<project>/.tablefacts/research/<slug>/` |
259
+ | Menu transcription cache and downloaded page images | `<project>/.tablefacts/cache/<host>/` |
260
+ | Fallback browser profile | `<project>/.tablefacts/instagram-profile` |
261
+ | Photos | The `--out` folder you give |
262
+ | Edge profile for the photo tools | `C:\ig-edge` (`--edge-dir` changes it) |
263
+
264
+ Git-ignore `.tablefacts/`: it holds third-party data and caches. Menu hints (missing dictionary keys, image
265
+ hosts) are read from the template's `frontend/src/content` and skipped silently when it is not there.
266
+
267
+ ### Supabase tables
268
+
269
+ `menu cluvi` and `menu raw` write to these tables, which your project must already have (the Cannario
270
+ templates ship the migration `supabase/migrations/0001_menu.sql`):
271
+
272
+ | Table | Columns written |
273
+ | --- | --- |
274
+ | `public.menu_categories` | `id` (uuid), `slug`, `name`, `sort_order` |
275
+ | `public.menu_sections` | `id` (uuid), `category_id`, `name`, `sort_order` |
276
+ | `public.menu_products` | `section_id`, `name`, `description`, `price` (numeric), `currency`, `image_url`, `recommended`, `sort_order` |
277
+
278
+ The write is **one transaction** and replaces only the categories in the import (their sections and products go
279
+ with them), so re-running never duplicates. `--replace-all` empties the whole menu first. An import with no
280
+ products, or with under half the products it replaces, is refused unless `--force`. Always run `--dry-run` first.
281
+
282
+ ## Limits
283
+
284
+ - It reads **public** pages only. It does not log in to Instagram, TripAdvisor or Google, and it does not get
285
+ past bot protection: a blocked source is reported, not worked around.
286
+ - Cluvi has no public API; the importer calls the two JSON endpoints its web app uses, which can change.
287
+ - Prices read from pictures can be wrong. Read the dry run against the original before writing.
288
+ - Only download photos the restaurant owns or has allowed you to use. Guest photos on TripAdvisor and
289
+ Instagram belong to their authors.
290
+
291
+ ## Develop
292
+
293
+ ```bash
294
+ npm install
295
+ npm test # vitest; no network, database or browser is used
296
+ npm run build:types # writes types/ from the JSDoc (git-ignored, shipped in the package)
297
+ ```
298
+
299
+ `npm test` builds the types and compiles `tests/fixtures/consumer.ts` against them, so a drift between the code
300
+ and its declarations fails the tests. Document options with JSDoc in `src/lib/types.mjs` and on the functions.
301
+
302
+ CI (`.github/workflows/ci.yml`) runs the tests on Ubuntu and Windows with Node 24 and checks `npm pack --dry-run`
303
+ on every push to `main` and every pull request. Dependabot (`.github/dependabot.yml`) keeps dependencies current.
304
+
305
+ **Release:** bump `version` in `package.json`, commit, then push a matching tag, e.g. `git tag v0.1.1 && git push origin v0.1.1`.
306
+ `.github/workflows/release.yml` checks the tag equals the version, runs the tests, publishes to npm with provenance
307
+ (needs an `NPM_TOKEN` repository secret) and creates a GitHub release with generated notes.
308
+
309
+ For how the code is organised and how to add a source or a tool, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
310
+
311
+ ## License
312
+
313
+ [MIT](LICENSE)
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ // One entry point for every extraction tool: `tablefacts <tool> [args]`.
3
+ // Each tool stays a plain script that reads process.argv, so the dispatcher only picks the
4
+ // script and hands over the rest of the arguments.
5
+
6
+ const tools = {
7
+ research: ['research/research.mjs', "Gather a restaurant's public facts into a report"],
8
+ 'photos instagram': ['instagram/download.mjs', 'Download photos from Instagram posts or a profile'],
9
+ 'photos tripadvisor': ['tripadvisor/photos.mjs', 'Download the photos of a TripAdvisor restaurant page'],
10
+ 'menu cluvi': ['menu/cluvi/extract.mjs', 'Import a menu from Cluvi'],
11
+ 'menu raw': ['menu/raw/extract.mjs', 'Import a menu from photos or PDFs'],
12
+ }
13
+
14
+ const args = process.argv.slice(2)
15
+ const two = `${args[0]} ${args[1]}`
16
+ const name = tools[two] ? two : tools[args[0]] ? args[0] : null
17
+
18
+ if (!name) {
19
+ const list = Object.entries(tools).map(([n, [, text]]) => ` ${n.padEnd(20)} ${text}`)
20
+ const wanted = args[0] && args[0] !== '--help' && args[0] !== '-h'
21
+ const text = `Usage: tablefacts <tool> [options]\n\nTools:\n${list.join('\n')}\n\nRun a tool with --help for its options.`
22
+ if (wanted) console.error(`Unknown tool: ${args.slice(0, 2).join(' ')}\n\n${text}`)
23
+ else console.log(text)
24
+ process.exit(wanted ? 2 : 0)
25
+ }
26
+
27
+ process.argv.splice(2, name.split(' ').length)
28
+ await import(new URL(`../src/${tools[name][0]}`, import.meta.url).href)
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "tablefacts",
3
+ "version": "0.1.0",
4
+ "description": "Data extraction tools for restaurant sites: public-facts research, photos (Instagram, TripAdvisor) and menu import",
5
+ "keywords": [
6
+ "restaurant",
7
+ "menu",
8
+ "scraper",
9
+ "tripadvisor",
10
+ "instagram",
11
+ "supabase",
12
+ "cli"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "Mateo Arias",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/MateoAriasCaicedo/tablefacts.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/MateoAriasCaicedo/tablefacts/issues"
22
+ },
23
+ "homepage": "https://github.com/MateoAriasCaicedo/tablefacts#readme",
24
+ "type": "module",
25
+ "engines": {
26
+ "node": ">=24"
27
+ },
28
+ "main": "./src/index.mjs",
29
+ "types": "./types/index.d.mts",
30
+ "bin": {
31
+ "tablefacts": "bin/tablefacts.mjs"
32
+ },
33
+ "exports": {
34
+ ".": {
35
+ "types": "./types/index.d.mts",
36
+ "default": "./src/index.mjs"
37
+ },
38
+ "./edge": {
39
+ "types": "./types/lib/edge.d.mts",
40
+ "default": "./src/lib/edge.mjs"
41
+ },
42
+ "./project": {
43
+ "types": "./types/lib/project.d.mts",
44
+ "default": "./src/lib/project.mjs"
45
+ },
46
+ "./package.json": "./package.json"
47
+ },
48
+ "files": [
49
+ "bin",
50
+ "src",
51
+ "types",
52
+ ".env.example",
53
+ "CHANGELOG.md"
54
+ ],
55
+ "scripts": {
56
+ "test": "vitest run",
57
+ "build:types": "tsc -p tsconfig.build.json",
58
+ "prepublishOnly": "npm run build:types && npm test"
59
+ },
60
+ "dependencies": {
61
+ "pg": "^8.23.1"
62
+ },
63
+ "devDependencies": {
64
+ "@types/node": "^24.19.1",
65
+ "playwright": "^1.63.0",
66
+ "typescript": "^7.0.2",
67
+ "vitest": "^5.0.3"
68
+ },
69
+ "peerDependencies": {
70
+ "playwright": "^1.63.0"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "playwright": {
74
+ "optional": true
75
+ }
76
+ }
77
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,52 @@
1
+ // Public API of the library. Every function is silent unless you pass `log`, never calls
2
+ // process.exit and never reads .env for you (the CLI does that; call loadEnv() or set the variables).
3
+ // Failures it raises on purpose are TablefactsError, with a `code` to tell them apart.
4
+ // Types of the options and results, defined in lib/types.mjs.
5
+ /** @typedef {import('./lib/types.mjs').Log} Log */
6
+ /** @typedef {import('./lib/types.mjs').Env} Env */
7
+ /** @typedef {import('./lib/types.mjs').ResearchOptions} ResearchOptions */
8
+ /** @typedef {import('./lib/types.mjs').ResearchResult} ResearchResult */
9
+ /** @typedef {import('./lib/types.mjs').ResearchPhoto} ResearchPhoto */
10
+ /** @typedef {import('./lib/types.mjs').ResearchProfile} ResearchProfile */
11
+ /** @typedef {import('./lib/types.mjs').InstagramOptions} InstagramOptions */
12
+ /** @typedef {import('./lib/types.mjs').TripadvisorOptions} TripadvisorOptions */
13
+ /** @typedef {import('./lib/types.mjs').PhotoSummary} PhotoSummary */
14
+ /** @typedef {import('./lib/types.mjs').PhotoFailure} PhotoFailure */
15
+ /** @typedef {import('./lib/types.mjs').Menu} Menu */
16
+ /** @typedef {import('./lib/types.mjs').MenuCategory} MenuCategory */
17
+ /** @typedef {import('./lib/types.mjs').MenuSection} MenuSection */
18
+ /** @typedef {import('./lib/types.mjs').MenuProduct} MenuProduct */
19
+ /** @typedef {import('./lib/types.mjs').MenuTotals} MenuTotals */
20
+ /** @typedef {import('./lib/types.mjs').MenuImage} MenuImage */
21
+ /** @typedef {import('./lib/types.mjs').MenuSourceConfig} MenuSourceConfig */
22
+ /** @typedef {import('./lib/types.mjs').CluviConfig} CluviConfig */
23
+ /** @typedef {import('./lib/types.mjs').RawConfig} RawConfig */
24
+ /** @typedef {import('./lib/types.mjs').ImportOptions} ImportOptions */
25
+ /** @typedef {import('./lib/types.mjs').ImportMenuOptions} ImportMenuOptions */
26
+ /** @typedef {import('./lib/types.mjs').ImportCluviOptions} ImportCluviOptions */
27
+ /** @typedef {import('./lib/types.mjs').ImportImageMenuOptions} ImportImageMenuOptions */
28
+ /** @typedef {import('./lib/types.mjs').ListMenuImagesOptions} ListMenuImagesOptions */
29
+ /** @typedef {import('./lib/types.mjs').ImportResult} ImportResult */
30
+ /** @typedef {import('./lib/types.mjs').PriceFormat} PriceFormat */
31
+ /** @typedef {import('./lib/types.mjs').VisionProvider} VisionProvider */
32
+ /** @typedef {import('./lib/types.mjs').LoadEnvOptions} LoadEnvOptions */
33
+ /** @typedef {import('./lib/types.mjs').TablefactsErrorCode} TablefactsErrorCode */
34
+
35
+ export { research } from './research/index.mjs'
36
+ export { downloadInstagram } from './instagram/index.mjs'
37
+ export { downloadTripadvisor } from './tripadvisor/index.mjs'
38
+ export {
39
+ importMenu,
40
+ importCluvi,
41
+ importImageMenu,
42
+ listMenuImages,
43
+ validateMenu,
44
+ countMenu,
45
+ normalizePages,
46
+ parsePrice,
47
+ providers,
48
+ defaultProvider,
49
+ } from './menu/index.mjs'
50
+ export { projectRoot, workDir, workDirIn, envFiles } from './lib/project.mjs'
51
+ export { TablefactsError } from './lib/errors.mjs'
52
+ export { loadEnv } from './lib/env.mjs'