rowbound 1.0.2

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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/dist/adapters/adapter.d.ts +1 -0
  4. package/dist/adapters/adapter.js +1 -0
  5. package/dist/adapters/sheets/sheets-adapter.d.ts +66 -0
  6. package/dist/adapters/sheets/sheets-adapter.js +531 -0
  7. package/dist/cli/config.d.ts +2 -0
  8. package/dist/cli/config.js +397 -0
  9. package/dist/cli/env.d.ts +3 -0
  10. package/dist/cli/env.js +103 -0
  11. package/dist/cli/format.d.ts +5 -0
  12. package/dist/cli/format.js +6 -0
  13. package/dist/cli/index.d.ts +2 -0
  14. package/dist/cli/index.js +39 -0
  15. package/dist/cli/init.d.ts +10 -0
  16. package/dist/cli/init.js +72 -0
  17. package/dist/cli/run.d.ts +2 -0
  18. package/dist/cli/run.js +212 -0
  19. package/dist/cli/runs.d.ts +2 -0
  20. package/dist/cli/runs.js +108 -0
  21. package/dist/cli/status.d.ts +2 -0
  22. package/dist/cli/status.js +108 -0
  23. package/dist/cli/sync.d.ts +2 -0
  24. package/dist/cli/sync.js +84 -0
  25. package/dist/cli/watch.d.ts +2 -0
  26. package/dist/cli/watch.js +348 -0
  27. package/dist/core/condition.d.ts +25 -0
  28. package/dist/core/condition.js +66 -0
  29. package/dist/core/defaults.d.ts +3 -0
  30. package/dist/core/defaults.js +7 -0
  31. package/dist/core/engine.d.ts +50 -0
  32. package/dist/core/engine.js +234 -0
  33. package/dist/core/env.d.ts +13 -0
  34. package/dist/core/env.js +72 -0
  35. package/dist/core/exec.d.ts +24 -0
  36. package/dist/core/exec.js +134 -0
  37. package/dist/core/extractor.d.ts +10 -0
  38. package/dist/core/extractor.js +33 -0
  39. package/dist/core/http-client.d.ts +32 -0
  40. package/dist/core/http-client.js +161 -0
  41. package/dist/core/rate-limiter.d.ts +25 -0
  42. package/dist/core/rate-limiter.js +64 -0
  43. package/dist/core/reconcile.d.ts +24 -0
  44. package/dist/core/reconcile.js +192 -0
  45. package/dist/core/run-format.d.ts +39 -0
  46. package/dist/core/run-format.js +201 -0
  47. package/dist/core/run-state.d.ts +64 -0
  48. package/dist/core/run-state.js +141 -0
  49. package/dist/core/run-tracker.d.ts +15 -0
  50. package/dist/core/run-tracker.js +57 -0
  51. package/dist/core/safe-compare.d.ts +8 -0
  52. package/dist/core/safe-compare.js +19 -0
  53. package/dist/core/shell-escape.d.ts +7 -0
  54. package/dist/core/shell-escape.js +9 -0
  55. package/dist/core/tab-resolver.d.ts +17 -0
  56. package/dist/core/tab-resolver.js +44 -0
  57. package/dist/core/template.d.ts +32 -0
  58. package/dist/core/template.js +82 -0
  59. package/dist/core/types.d.ts +105 -0
  60. package/dist/core/types.js +2 -0
  61. package/dist/core/url-guard.d.ts +21 -0
  62. package/dist/core/url-guard.js +184 -0
  63. package/dist/core/validator.d.ts +11 -0
  64. package/dist/core/validator.js +261 -0
  65. package/dist/core/waterfall.d.ts +26 -0
  66. package/dist/core/waterfall.js +55 -0
  67. package/dist/index.d.ts +15 -0
  68. package/dist/index.js +16 -0
  69. package/dist/mcp/server.d.ts +1 -0
  70. package/dist/mcp/server.js +943 -0
  71. package/package.json +67 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Rowbound Contributors
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,258 @@
1
+ <p align="center">
2
+ <img src="assets/logo.png" width="80" alt="Rowbound logo" />
3
+ </p>
4
+
5
+ <h1 align="center">Rowbound</h1>
6
+
7
+ <p align="center">
8
+ A CLI for GTM Engineering in Google Sheets with Claude Code.
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License" /></a>
13
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg" alt="Node.js" /></a>
14
+ </p>
15
+
16
+ ---
17
+
18
+ ## Demo
19
+
20
+ ![Rowbound demo](assets/demo.gif)
21
+
22
+ ## Install
23
+
24
+ ### Prerequisites
25
+
26
+ - **Node.js 22+** — `node --version` must be >= 22.0.0
27
+ - **gws CLI** — [Google Workspace CLI](https://github.com/googleworkspace/cli) for Sheets access
28
+
29
+ ```bash
30
+ npm install -g @googleworkspace/cli
31
+ gws auth setup # first time: creates Cloud project, enables APIs, logs in
32
+ gws auth login # subsequent logins
33
+ ```
34
+
35
+ ### Quick install
36
+
37
+ ```bash
38
+ npm install -g github:eliasstravik/rowbound
39
+ ```
40
+
41
+ ### Build from source
42
+
43
+ ```bash
44
+ git clone https://github.com/eliasstravik/rowbound.git
45
+ cd rowbound
46
+ npm install
47
+ npm run dev -- <command>
48
+ ```
49
+
50
+ ## Quick Start
51
+
52
+ ```bash
53
+ # 1. Initialize a sheet
54
+ rowbound init <spreadsheet-id>
55
+
56
+ # 2. Add an action
57
+ rowbound config add-action <spreadsheet-id> --json '{
58
+ "id": "enrich_company",
59
+ "type": "http",
60
+ "target": "company_info",
61
+ "method": "GET",
62
+ "url": "https://api.example.com/company?domain={{row.domain}}",
63
+ "headers": { "Authorization": "Bearer {{env.API_KEY}}" },
64
+ "extract": "$.name"
65
+ }'
66
+
67
+ # 3. Store API keys and run
68
+ rowbound env set API_KEY=your_key_here
69
+ rowbound run <spreadsheet-id>
70
+ rowbound run <spreadsheet-id> --dry-run # preview first
71
+ ```
72
+
73
+ Column names are automatically resolved to stable IDs when you run `rowbound sync`.
74
+
75
+ ## Features
76
+
77
+ - **HTTP actions** — call any REST API with templated URLs, headers, and bodies; extract values with JSONPath
78
+ - **Waterfall actions** — try multiple providers in order until one returns a result (e.g., Clearbit → Apollo → Hunter)
79
+ - **Transform actions** — compute derived values with sandboxed JavaScript expressions
80
+ - **Exec actions** — run shell commands and capture stdout
81
+ - **Conditional execution** — skip actions per-row with `when` expressions
82
+ - **Smart skip** — automatically skips rows where the target cell already has a value
83
+ - **Watch mode** — poll sheets on an interval or trigger runs via webhook
84
+ - **Column tracking** — automatic column registry that survives header renames
85
+ - **Rate limiting** — global token-bucket rate limiter with configurable requests/second
86
+ - **Retry with backoff** — exponential, linear, or fixed backoff on failures
87
+ - **Structured error handling** — per-action `onError` config maps status codes to actions (skip, write fallback)
88
+ - **MCP server** — expose all operations as Model Context Protocol tools for Claude Desktop and other AI assistants
89
+ - **Run history** — track pipeline executions with per-action summaries, durations, and error logs
90
+ - **Dry run** — preview what would change without writing to the sheet
91
+ - **BYOK** — bring your own API keys, pay only for the APIs you use
92
+
93
+ ## CLI Commands
94
+
95
+ | Command | Description |
96
+ |---------|-------------|
97
+ | `rowbound init <sheetId>` | Initialize a sheet with a default pipeline config |
98
+ | `rowbound run <sheetId>` | Run the enrichment pipeline (`--dry-run`, `--rows`, `--action`) |
99
+ | `rowbound status <sheetId>` | Show pipeline status and enrichment rates |
100
+ | `rowbound watch <sheetId>` | Watch for changes and run continuously (`--interval`, `--port`) |
101
+ | `rowbound sync <sheetId>` | Reconcile columns, validate config, fix issues |
102
+ | `rowbound config show <sheetId>` | Display the pipeline config as JSON |
103
+ | `rowbound config add-action <sheetId>` | Add an action to the pipeline |
104
+ | `rowbound config remove-action <sheetId>` | Remove an action by ID |
105
+ | `rowbound config update-action <sheetId>` | Update an action (merge partial JSON) |
106
+ | `rowbound config set <sheetId>` | Update pipeline settings (concurrency, rate limit, retry) |
107
+ | `rowbound config validate <sheetId>` | Validate the pipeline config |
108
+ | `rowbound runs [runId]` | List recent runs or view a specific run |
109
+ | `rowbound runs clear` | Delete all run history |
110
+ | `rowbound env set <KEY=value>` | Store an API key globally |
111
+ | `rowbound env remove <KEY>` | Remove a stored key |
112
+ | `rowbound env list` | List stored keys (values masked) |
113
+ | `rowbound mcp` | Start the MCP server (stdio) |
114
+
115
+ ## MCP Server
116
+
117
+ Rowbound exposes all pipeline operations as MCP tools. Add this to your Claude Desktop config:
118
+
119
+ ```json
120
+ {
121
+ "mcpServers": {
122
+ "rowbound": {
123
+ "command": "rowbound",
124
+ "args": ["mcp"]
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
130
+ | Tool | Description |
131
+ |------|-------------|
132
+ | `init_pipeline` | Initialize a sheet with a default pipeline config |
133
+ | `run_pipeline` | Run the enrichment pipeline |
134
+ | `add_action` / `remove_action` / `update_action` | Manage pipeline actions |
135
+ | `update_settings` | Update pipeline settings (concurrency, rate limit, retry) |
136
+ | `sync_columns` | Sync the column registry with the current sheet state |
137
+ | `get_config` / `validate_config` | Read or validate the pipeline config |
138
+ | `get_status` | Return pipeline status with enrichment rates |
139
+ | `dry_run` | Run in dry mode (no writes) |
140
+ | `start_watch` / `stop_watch` | Manage watch mode |
141
+ | `preview_rows` | Read and display rows from the sheet |
142
+ | `list_runs` / `get_run` | View pipeline run history |
143
+
144
+ ## Action Types
145
+
146
+ Templates use `{{row.column}}` for row data and `{{env.KEY}}` for environment variables. Actions support conditional execution with `when` expressions and structured error handling with `onError`.
147
+
148
+ ### http
149
+
150
+ Call a REST API and extract a value with JSONPath.
151
+
152
+ ```json
153
+ {
154
+ "id": "get_company",
155
+ "type": "http",
156
+ "target": "company_name",
157
+ "when": "row.domain !== ''",
158
+ "method": "GET",
159
+ "url": "https://api.clearbit.com/v2/companies/find?domain={{row.domain}}",
160
+ "headers": { "Authorization": "Bearer {{env.CLEARBIT_API_KEY}}" },
161
+ "extract": "$.name",
162
+ "onError": { "404": "skip", "429": "skip", "default": { "write": "ERROR" } }
163
+ }
164
+ ```
165
+
166
+ ### waterfall
167
+
168
+ Try multiple providers in order. First non-empty result wins.
169
+
170
+ ```json
171
+ {
172
+ "id": "find_email",
173
+ "type": "waterfall",
174
+ "target": "email",
175
+ "providers": [
176
+ {
177
+ "name": "hunter",
178
+ "method": "GET",
179
+ "url": "https://api.hunter.io/v2/email-finder?domain={{row.domain}}&first_name={{row.first_name}}&last_name={{row.last_name}}&api_key={{env.HUNTER_API_KEY}}",
180
+ "extract": "$.data.email"
181
+ },
182
+ {
183
+ "name": "apollo",
184
+ "method": "POST",
185
+ "url": "https://api.apollo.io/api/v1/people/match",
186
+ "headers": { "Content-Type": "application/json", "X-Api-Key": "{{env.APOLLO_API_KEY}}" },
187
+ "body": { "email": "{{row.personal_email}}", "domain": "{{row.domain}}" },
188
+ "extract": "$.person.email"
189
+ }
190
+ ]
191
+ }
192
+ ```
193
+
194
+ ### transform
195
+
196
+ Compute a value with a sandboxed JavaScript expression.
197
+
198
+ ```json
199
+ {
200
+ "id": "full_name",
201
+ "type": "transform",
202
+ "target": "full_name",
203
+ "expression": "`${row.first_name} ${row.last_name}`"
204
+ }
205
+ ```
206
+
207
+ ### exec
208
+
209
+ Run a shell command and capture stdout. Template values are shell-escaped.
210
+
211
+ ```json
212
+ {
213
+ "id": "whois_lookup",
214
+ "type": "exec",
215
+ "target": "registrar",
216
+ "command": "whois {{row.domain}} | grep 'Registrar:' | head -1 | cut -d: -f2",
217
+ "timeout": 10000,
218
+ "onError": { "default": "skip" }
219
+ }
220
+ ```
221
+
222
+ ### Error handling
223
+
224
+ Actions can define `onError` to map HTTP status codes (or exit codes for exec) to behaviors:
225
+
226
+ | Action | Effect |
227
+ |--------|--------|
228
+ | `"skip"` | Skip this action for the current row |
229
+ | `"stop_provider"` | Stop the current waterfall provider, try the next |
230
+ | `{"write": "value"}` | Write a fallback value to the target cell |
231
+
232
+ ## Development
233
+
234
+ ```bash
235
+ npm install
236
+ npm run dev -- <command>
237
+ ```
238
+
239
+ | Command | Description |
240
+ |---------|-------------|
241
+ | `npm run dev -- <command>` | Run a CLI command in development mode |
242
+ | `npm run build` | Type-check and build for production |
243
+ | `npm test` | Run tests |
244
+ | `npm run lint` | Lint with Biome |
245
+
246
+ ## Security
247
+
248
+ - **Expression sandbox** — `when` conditions and transform expressions run in Node.js `vm.runInContext` with keyword blocking; convenience sandbox, not a security boundary
249
+ - **Exec actions** — shell commands run locally; template values are shell-escaped but use only with trusted data
250
+ - **SSRF protection** — HTTP requests enforce HTTPS by default and block private/internal IP ranges; set `ROWBOUND_ALLOW_HTTP=true` for local dev
251
+ - **Webhook auth** — set `ROWBOUND_WEBHOOK_TOKEN` to require bearer token authentication; server binds to localhost by default
252
+ - **API keys** — stored in `~/.config/rowbound/.env` with `600` permissions; `.gitignore` excludes `.env`
253
+ - **Env filtering** — only `ROWBOUND_*`, `NODE_ENV`, `PATH`, and explicitly referenced vars are exposed to actions
254
+ - **MCP permissions** — the MCP server inherits the authenticated `gws` CLI session permissions
255
+
256
+ ## License
257
+
258
+ [MIT](LICENSE)
@@ -0,0 +1 @@
1
+ export type { Adapter } from "../core/types.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ import type { Adapter, CellUpdate, PipelineConfig, Row, SheetRef } from "../../core/types.js";
2
+ /**
3
+ * Convert a 0-indexed column number to a spreadsheet column letter.
4
+ * 0 = A, 1 = B, ..., 25 = Z, 26 = AA, 27 = AB, ...
5
+ */
6
+ export declare function columnIndexToLetter(index: number): string;
7
+ /**
8
+ * Run the gws CLI with the given arguments.
9
+ * Uses execFile (not exec) to avoid shell injection.
10
+ */
11
+ export declare function runGws(args: string[]): Promise<string>;
12
+ /**
13
+ * Google Sheets adapter using the gws CLI tool.
14
+ */
15
+ export declare class SheetsAdapter implements Adapter {
16
+ private headerCache;
17
+ private headerCacheTimes;
18
+ private headerPending;
19
+ private readonly HEADER_CACHE_TTL_MS;
20
+ private escapeSheetName;
21
+ private cacheKey;
22
+ private sheetName;
23
+ /**
24
+ * Look up a column name in the headers and return its letter (A, B, ..., AA, etc.).
25
+ */
26
+ private columnNameToLetter;
27
+ readRows(ref: SheetRef, range?: string): Promise<Row[]>;
28
+ writeCell(ref: SheetRef, update: CellUpdate): Promise<void>;
29
+ writeBatch(ref: SheetRef, updates: CellUpdate[]): Promise<void>;
30
+ readConfig(ref: SheetRef): Promise<PipelineConfig | null>;
31
+ writeConfig(ref: SheetRef, config: PipelineConfig): Promise<void>;
32
+ getHeaders(ref: SheetRef): Promise<string[]>;
33
+ /**
34
+ * Clear the header cache. Useful when headers are known to have changed.
35
+ */
36
+ clearCache(): void;
37
+ /**
38
+ * List all sheets (tabs) in the spreadsheet with their GIDs and names.
39
+ */
40
+ listSheets(spreadsheetId: string): Promise<Array<{
41
+ gid: number;
42
+ name: string;
43
+ }>>;
44
+ /**
45
+ * Get the numeric sheet ID (GID) for a sheet.
46
+ * Needed for named range creation.
47
+ */
48
+ getSheetGid(ref: SheetRef): Promise<number>;
49
+ /**
50
+ * Create a named range pointing to a specific column.
51
+ * Name format: _rowbound_{actionId}
52
+ * Range: entire column (no row bounds).
53
+ */
54
+ createColumnRange(ref: SheetRef, actionId: string, columnIndex: number): Promise<void>;
55
+ /**
56
+ * Read all Rowbound named ranges for a sheet.
57
+ * Returns a map of actionId -> column index (0-based).
58
+ * When sheetGid is provided, only returns ranges belonging to that tab.
59
+ */
60
+ readColumnRanges(ref: SheetRef, sheetGid?: number): Promise<Map<string, number>>;
61
+ /**
62
+ * Delete a named range by action ID.
63
+ */
64
+ deleteColumnRange(ref: SheetRef, actionId: string): Promise<void>;
65
+ private fetchHeaders;
66
+ }