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.
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/adapters/adapter.d.ts +1 -0
- package/dist/adapters/adapter.js +1 -0
- package/dist/adapters/sheets/sheets-adapter.d.ts +66 -0
- package/dist/adapters/sheets/sheets-adapter.js +531 -0
- package/dist/cli/config.d.ts +2 -0
- package/dist/cli/config.js +397 -0
- package/dist/cli/env.d.ts +3 -0
- package/dist/cli/env.js +103 -0
- package/dist/cli/format.d.ts +5 -0
- package/dist/cli/format.js +6 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +39 -0
- package/dist/cli/init.d.ts +10 -0
- package/dist/cli/init.js +72 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.js +212 -0
- package/dist/cli/runs.d.ts +2 -0
- package/dist/cli/runs.js +108 -0
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +108 -0
- package/dist/cli/sync.d.ts +2 -0
- package/dist/cli/sync.js +84 -0
- package/dist/cli/watch.d.ts +2 -0
- package/dist/cli/watch.js +348 -0
- package/dist/core/condition.d.ts +25 -0
- package/dist/core/condition.js +66 -0
- package/dist/core/defaults.d.ts +3 -0
- package/dist/core/defaults.js +7 -0
- package/dist/core/engine.d.ts +50 -0
- package/dist/core/engine.js +234 -0
- package/dist/core/env.d.ts +13 -0
- package/dist/core/env.js +72 -0
- package/dist/core/exec.d.ts +24 -0
- package/dist/core/exec.js +134 -0
- package/dist/core/extractor.d.ts +10 -0
- package/dist/core/extractor.js +33 -0
- package/dist/core/http-client.d.ts +32 -0
- package/dist/core/http-client.js +161 -0
- package/dist/core/rate-limiter.d.ts +25 -0
- package/dist/core/rate-limiter.js +64 -0
- package/dist/core/reconcile.d.ts +24 -0
- package/dist/core/reconcile.js +192 -0
- package/dist/core/run-format.d.ts +39 -0
- package/dist/core/run-format.js +201 -0
- package/dist/core/run-state.d.ts +64 -0
- package/dist/core/run-state.js +141 -0
- package/dist/core/run-tracker.d.ts +15 -0
- package/dist/core/run-tracker.js +57 -0
- package/dist/core/safe-compare.d.ts +8 -0
- package/dist/core/safe-compare.js +19 -0
- package/dist/core/shell-escape.d.ts +7 -0
- package/dist/core/shell-escape.js +9 -0
- package/dist/core/tab-resolver.d.ts +17 -0
- package/dist/core/tab-resolver.js +44 -0
- package/dist/core/template.d.ts +32 -0
- package/dist/core/template.js +82 -0
- package/dist/core/types.d.ts +105 -0
- package/dist/core/types.js +2 -0
- package/dist/core/url-guard.d.ts +21 -0
- package/dist/core/url-guard.js +184 -0
- package/dist/core/validator.d.ts +11 -0
- package/dist/core/validator.js +261 -0
- package/dist/core/waterfall.d.ts +26 -0
- package/dist/core/waterfall.js +55 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +943 -0
- 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
|
+

|
|
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
|
+
}
|