salesprompter-cli 0.1.20 → 0.1.22
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/README.md +28 -308
- package/dist/cli.js +213 -27
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -1,338 +1,58 @@
|
|
|
1
1
|
# salesprompter-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Salesprompter CLI helps you go from company or product input to qualified leads with a guided terminal flow.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- Crawl LinkedIn product categories
|
|
7
|
-
- Derive intended-role job titles
|
|
8
|
-
- Split broad Sales Navigator searches into exportable slices
|
|
9
|
-
- Export those slices through Phantombuster
|
|
10
|
-
- Store product and people data in Salesprompter
|
|
11
|
-
- Enrich leads
|
|
12
|
-
- Score leads
|
|
13
|
-
- Sync leads into CRM and outreach systems
|
|
14
|
-
- Analyze upstream lead-list and domain-enrichment bottlenecks
|
|
15
|
-
- Replace opaque Pipedream logic with deterministic CLI workflows
|
|
16
|
-
|
|
17
|
-
It is built for two users at the same time:
|
|
18
|
-
|
|
19
|
-
- humans working in a terminal
|
|
20
|
-
- coding agents such as Codex, Claude Code, and other LLM-driven shell workflows
|
|
21
|
-
|
|
22
|
-
The app, CLI, and Chrome extension all write into one shared Salesprompter backend and database. Access is scoped to
|
|
23
|
-
the active workspace you choose at login.
|
|
24
|
-
|
|
25
|
-
## Start Here
|
|
26
|
-
|
|
27
|
-
If someone discovers Salesprompter from a vague prompt, give them the shortest working path for their context.
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
## Human-friendly guided path
|
|
31
|
-
npx -y salesprompter-cli@latest
|
|
32
|
-
|
|
33
|
-
## Explicit guided path
|
|
34
|
-
npx -y salesprompter-cli@latest wizard
|
|
35
|
-
|
|
36
|
-
## Primary raw command for agents and scripts
|
|
37
|
-
npx -y salesprompter-cli@latest salesnav:from-product-category --input deel.com --dry-run
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Or install it globally:
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
npm install -g salesprompter-cli
|
|
44
|
-
salesprompter
|
|
45
|
-
salesprompter wizard
|
|
46
|
-
salesprompter --help
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Bare `salesprompter` now opens a guided wizard in an interactive terminal. Keep using explicit subcommands for agents, CI, and copy-paste docs.
|
|
50
|
-
If your Salesprompter user belongs to multiple workspaces, browser login asks which workspace the CLI session should use.
|
|
51
|
-
|
|
52
|
-
## Prompt To Command
|
|
53
|
-
|
|
54
|
-
The primary live path is:
|
|
55
|
-
|
|
56
|
-
1. crawl a LinkedIn product category
|
|
57
|
-
2. derive the intended job titles
|
|
58
|
-
3. turn those into Sales Navigator queries
|
|
59
|
-
4. split broad searches into exportable chunks
|
|
60
|
-
5. export them through Phantombuster
|
|
61
|
-
6. store everything in Salesprompter
|
|
62
|
-
|
|
63
|
-
### "I sell for Deel and want the right people at similar companies"
|
|
64
|
-
|
|
65
|
-
Start from the company, product, or category input:
|
|
5
|
+
## Install
|
|
66
6
|
|
|
67
7
|
```bash
|
|
68
|
-
|
|
8
|
+
npm i -g salesprompter-cli
|
|
69
9
|
```
|
|
70
10
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
Preview the generated titles and first split queries before creating crawl jobs:
|
|
11
|
+
or run directly:
|
|
74
12
|
|
|
75
13
|
```bash
|
|
76
|
-
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### "Find people at deel.com"
|
|
80
|
-
|
|
81
|
-
That is still a direct target-company lookup:
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
salesprompter account:resolve --domain deel.com --company-name Deel --out ./data/deel-account.json
|
|
85
|
-
salesprompter leads:generate --icp ./data/icp.json --count 5 --domain deel.com --company-name Deel --out ./data/deel-leads.json
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### Historical warehouse fallback
|
|
89
|
-
|
|
90
|
-
`salesnav:ensure-count` remains a historical backfill path, not the primary workflow:
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
salesprompter salesnav:ensure-count --target-count 200000 --org-id "$SALESPROMPTER_ORG_ID"
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Documentation
|
|
97
|
-
|
|
98
|
-
This repository now includes the public Salesprompter docs site for the wider Salesprompter universe, including the app contract, CLI surface, Chrome extension contract, and the main warehouse-backed workflows.
|
|
99
|
-
|
|
100
|
-
- Live docs: `https://salesprompter-cli.vercel.app`
|
|
101
|
-
|
|
102
|
-
- Docs home: `./index.mdx`
|
|
103
|
-
- Quickstart: `./quickstart.mdx`
|
|
104
|
-
- Architecture: `./architecture.mdx`
|
|
105
|
-
- App: `./platform/app.mdx`
|
|
106
|
-
- CLI: `./platform/cli.mdx`
|
|
107
|
-
- Chrome extension: `./platform/chrome-extension.mdx`
|
|
108
|
-
- Domain finder: `./workflows/domain-finder.mdx`
|
|
109
|
-
- Command reference: `./reference/cli.mdx`
|
|
110
|
-
- Environment variables: `./reference/environment-variables.mdx`
|
|
111
|
-
- Troubleshooting: `./operations/troubleshooting.mdx`
|
|
112
|
-
|
|
113
|
-
Run the docs locally with:
|
|
114
|
-
|
|
115
|
-
```bash
|
|
116
|
-
npm run docs:dev
|
|
14
|
+
npx -y salesprompter-cli@latest
|
|
117
15
|
```
|
|
118
16
|
|
|
119
|
-
|
|
17
|
+
## Quickstart
|
|
120
18
|
|
|
121
19
|
```bash
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
## Integration Contract
|
|
126
|
-
|
|
127
|
-
This CLI is not a standalone toy. It is a production integration surface for the Salesprompter app.
|
|
128
|
-
|
|
129
|
-
- The Domain Finder flow in this repository is a real end-to-end test case for Salesprompter app integration.
|
|
130
|
-
- CLI behavior should be treated as an app contract: auth, BigQuery execution, artifacts, and writeback semantics must remain stable.
|
|
131
|
-
- Changes to domain selection, writeback, or audit logic should always be validated with:
|
|
132
|
-
- CLI tests (`npm test`)
|
|
133
|
-
- BigQuery-backed runs (`domainfinder:run:bq`, `domainfinder:audit-existing:bq`)
|
|
134
|
-
- before/after delta checks (`domainfinder:audit-delta`)
|
|
135
|
-
|
|
136
|
-
The current live path is product-first. It resolves a LinkedIn product category, derives intended-role Sales Navigator searches, and lets the app run durable exports through Phantombuster. Account-first lead generation still exists for direct target-company lookups.
|
|
137
|
-
|
|
138
|
-
When the output `mode` is `fallback`, the leads are modeled contacts for workflow testing, not verified real contacts. A real provider path should return `mode: "real"` using the same JSON shape.
|
|
139
|
-
|
|
140
|
-
All non-auth commands require a logged-in CLI session. This gives you one identity model across Salesprompter app, CLI, and Chrome extension.
|
|
141
|
-
|
|
142
|
-
Global output flags:
|
|
143
|
-
|
|
144
|
-
- `--json`: compact machine-readable JSON (optimized for agent/LLM parsers)
|
|
145
|
-
- `--quiet`: suppress successful stdout payloads (errors still surface)
|
|
146
|
-
|
|
147
|
-
## Auth and Session
|
|
148
|
-
|
|
149
|
-
The CLI stores a local session file at `~/.config/salesprompter/auth-session.json` (or `SALESPROMPTER_CONFIG_DIR`).
|
|
20
|
+
# Start the interactive wizard
|
|
21
|
+
salesprompter
|
|
150
22
|
|
|
151
|
-
|
|
152
|
-
# Preferred path: browser/device login
|
|
23
|
+
# Login explicitly
|
|
153
24
|
salesprompter auth:login
|
|
154
25
|
|
|
155
|
-
#
|
|
156
|
-
salesprompter auth:
|
|
157
|
-
|
|
158
|
-
# Verify active identity with backend
|
|
159
|
-
salesprompter auth:whoami --verify
|
|
160
|
-
|
|
161
|
-
# Clear local session
|
|
162
|
-
salesprompter auth:logout
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
If your user belongs to multiple workspaces, the browser flow asks you to choose the workspace for that CLI session before returning to the terminal.
|
|
166
|
-
|
|
167
|
-
Environment variables:
|
|
168
|
-
|
|
169
|
-
- `SALESPROMPTER_API_BASE_URL`: override backend URL (default `https://salesprompter.ai`)
|
|
170
|
-
- `SALESPROMPTER_CONFIG_DIR`: override local config dir
|
|
171
|
-
- `SALESPROMPTER_ORG_ID`: optional org override for privileged Sales Navigator backfills
|
|
172
|
-
- `SALESPROMPTER_SKIP_AUTH=1`: bypass auth guard (tests/dev only)
|
|
173
|
-
- `INSTANTLY_API_KEY`: required for real `sync:outreach --target instantly`
|
|
174
|
-
- `INSTANTLY_CAMPAIGN_ID`: default campaign id for Instantly sync
|
|
175
|
-
- `SALESPROMPTER_INSTANTLY_BASE_URL`: override Instantly API base URL (tests/local proxies)
|
|
176
|
-
- `NEXT_PUBLIC_SUPABASE_URL` or `SALESPROMPTER_SUPABASE_URL`: required for `salesnav:ensure-count` and for CLI-managed Sales Navigator exports
|
|
177
|
-
- `SUPABASE_SERVICE_ROLE_KEY`: required for `salesnav:ensure-count` and for CLI-managed Sales Navigator exports
|
|
178
|
-
- `LINKEDIN_SESSION_COOKIE_ENCRYPTION_KEY` or `EXTENSION_AUTH_SECRET` or `CLERK_SECRET_KEY`: required for Sales Navigator export and crawl commands that launch exports
|
|
179
|
-
- `SALESPROMPTER_LINKEDIN_SESSION_EXCLUDED_EMAILS`: optional comma-separated emails to keep out of CLI-managed Sales Navigator rotation
|
|
180
|
-
- `SALESPROMPTER_LINKEDIN_SESSION_EXCLUDED_HANDLES`: optional comma-separated LinkedIn handles to keep out of CLI-managed Sales Navigator rotation
|
|
181
|
-
- `SALESPROMPTER_CLI_MANAGE_LINKEDIN_SESSIONS=0`: optional test or legacy fallback override; when omitted, the CLI claims, rotates, and preflights Sales Navigator session cookies before export launch
|
|
182
|
-
- `GOOGLE_SERVICE_ACCOUNT_KEY`: required for `salesnav:ensure-count`
|
|
183
|
-
- `BIGQUERY_PROJECT_ID`: optional when the Google service-account JSON already includes `project_id`
|
|
184
|
-
|
|
185
|
-
App compatibility:
|
|
186
|
-
|
|
187
|
-
- Salesprompter app should expose `/api/cli/auth/device/start`, `/api/cli/auth/device/poll`, and `/api/cli/auth/me`.
|
|
188
|
-
- `salesprompter auth:login` uses browser/device login and prints the verification URL plus code before polling.
|
|
189
|
-
- `POST /api/cli/auth/token` remains the fallback path when browser/device login is disabled or unavailable.
|
|
190
|
-
|
|
191
|
-
Fallback command:
|
|
192
|
-
|
|
193
|
-
```bash
|
|
194
|
-
salesprompter auth:login --token "<token-from-app>" --api-url "https://salesprompter.ai"
|
|
26
|
+
# Verify your session
|
|
27
|
+
salesprompter auth:whoami
|
|
195
28
|
```
|
|
196
29
|
|
|
197
|
-
##
|
|
198
|
-
|
|
199
|
-
- Every command reads and writes plain JSON.
|
|
200
|
-
- Output is machine-readable and composable (`--json` for compact transport).
|
|
201
|
-
- The top-level use cases map ambiguous prompts like "determine the ICP of deel.com" into explicit command paths.
|
|
202
|
-
- Domain contracts are explicit and validated with `zod`.
|
|
203
|
-
- External integrations are behind narrow provider interfaces.
|
|
204
|
-
- Lead generation reports which provider and mode produced the result.
|
|
205
|
-
- Workflow bottlenecks become inspectable artifacts instead of hidden Pipedream state.
|
|
206
|
-
- Real prospect lookup can be normalized straight into the CLI lead schema with `--lead-out`.
|
|
207
|
-
|
|
208
|
-
## Commands
|
|
30
|
+
## Common commands
|
|
209
31
|
|
|
210
32
|
```bash
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
--industries "Software,Financial Services" \
|
|
214
|
-
--company-sizes "50-199,200-499" \
|
|
215
|
-
--regions "Europe" \
|
|
216
|
-
--countries "DE,NL,GB" \
|
|
217
|
-
--titles "Head of Revenue Operations,VP Sales" \
|
|
218
|
-
--required-signals "recent funding,growing outbound team" \
|
|
219
|
-
--keywords "revenue operations,outbound,sales tooling" \
|
|
220
|
-
--out ./data/icp.json
|
|
33
|
+
# Product/category to leads workflow
|
|
34
|
+
salesprompter salesnav:from-product-category --input "https://www.linkedin.com/company/example/"
|
|
221
35
|
|
|
222
|
-
|
|
223
|
-
salesprompter auth:whoami --verify
|
|
224
|
-
salesprompter --json salesnav:from-product-category --input deel.com --dry-run
|
|
225
|
-
salesprompter salesnav:from-product-category --input deel.com
|
|
226
|
-
# Historical fallback only. Resumes from prior CLI historical backfill runs unless you pass --start-offset.
|
|
227
|
-
salesprompter salesnav:ensure-count --target-count 200000 --org-id "$SALESPROMPTER_ORG_ID"
|
|
36
|
+
# Run a Sales Navigator crawl from a query URL
|
|
228
37
|
salesprompter salesnav:crawl --query-url "https://www.linkedin.com/sales/search/people?query=..."
|
|
229
|
-
salesprompter icp:vendor --vendor deel --market dach --out ./data/deel-icp.json
|
|
230
|
-
salesprompter leads:lookup:bq --icp ./data/deel-icp.json --limit 100 --execute --out ./data/deel-leads-raw.json --lead-out ./data/deel-leads.json
|
|
231
|
-
salesprompter leads:enrich --in ./data/deel-leads.json --out ./data/deel-enriched.json
|
|
232
|
-
salesprompter leads:score --icp ./data/deel-icp.json --in ./data/deel-enriched.json --out ./data/deel-scored.json
|
|
233
|
-
salesprompter sync:outreach --target instantly --in ./data/deel-scored.json --campaign-id "$INSTANTLY_CAMPAIGN_ID"
|
|
234
|
-
salesprompter sync:outreach --target instantly --in ./data/deel-scored.json --campaign-id "$INSTANTLY_CAMPAIGN_ID" --apply
|
|
235
|
-
salesprompter icp:from-historical-queries:bq --vendor deel --market dach --out ./data/deel-icp-historical.json --report-out ./data/deel-historical-report.json
|
|
236
|
-
salesprompter leadlists:funnel:bq --vendor deel --market dach --out ./data/deel-leadlists-funnel.json
|
|
237
|
-
salesprompter leadlists:direct-export:bq --vendor deel --market dach --limit 20000 --out-dir ./data/direct-path --sql-out ./data/direct-path/deel-direct-dach.sql
|
|
238
|
-
salesprompter domainfinder:backlog:bq --market dach --out ./data/deel-domainfinder-backlog.json
|
|
239
|
-
salesprompter domainfinder:candidates:bq --market dach --limit 500 --out ./data/domain-candidates.json --sql-out ./data/domain-candidates.sql
|
|
240
|
-
salesprompter domainfinder:input-sql --market dach --out ./data/domainFinder_input_v2.sql
|
|
241
|
-
salesprompter domainfinder:select --in ./data/domain-candidates.json --out ./data/domain-decisions.json
|
|
242
|
-
salesprompter domainfinder:audit --in ./data/domain-decisions.json --out ./data/domain-audit.json
|
|
243
|
-
salesprompter domainfinder:compare-pipedream --in ./data/domain-candidates.json --out ./data/domain-comparison.json
|
|
244
|
-
salesprompter domainfinder:audit-existing:bq --market dach --out ./data/domain-existing-audit.json
|
|
245
|
-
salesprompter domainfinder:audit-delta --before ./data/domain-existing-audit-before.json --after ./data/domain-existing-audit-after.json --out ./data/domain-existing-audit-delta.json
|
|
246
|
-
salesprompter domainfinder:repair-existing:bq --market dach --mode conservative --limit 5000 --out ./data/domain-repair.sql --trace-id salesprompter-cli-repair-dach
|
|
247
|
-
salesprompter domainfinder:writeback-sql --in ./data/domain-decisions.json --out ./data/domain-writeback.sql --trace-id salesprompter-cli-dach-20260308
|
|
248
|
-
salesprompter domainfinder:writeback:bq --in ./data/domain-decisions.json --out ./data/domain-writeback.sql --trace-id salesprompter-cli-dach-20260308
|
|
249
|
-
salesprompter domainfinder:run:bq --market dach --limit 500 --out-dir ./data/domainfinder-run --trace-id salesprompter-cli-dach-20260308
|
|
250
|
-
salesprompter account:resolve --domain deel.com --company-name Deel --out ./data/deel-account.json
|
|
251
|
-
salesprompter leads:generate --icp ./data/icp.json --count 5 --out ./data/leads.json
|
|
252
|
-
salesprompter leads:generate --icp ./data/icp.json --count 5 --domain deel.com --company-name Deel --out ./data/deel-leads.json
|
|
253
|
-
salesprompter leads:enrich --in ./data/leads.json --out ./data/enriched.json
|
|
254
|
-
salesprompter leads:score --icp ./data/icp.json --in ./data/enriched.json --out ./data/scored.json
|
|
255
|
-
salesprompter leads:lookup:bq --icp ./data/deel-icp.json --limit 100
|
|
256
|
-
salesprompter queries:analyze:bq --search-kind sales-people --include-function "Human Resources" --out ./data/hr-query-report.json
|
|
257
|
-
salesprompter sync:crm --target hubspot --in ./data/scored.json
|
|
258
|
-
salesprompter sync:outreach --target instantly --in ./data/scored.json
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
## Domain Finder Migration
|
|
262
|
-
|
|
263
|
-
The original Pipedream `domainFinder` workflow was doing three things:
|
|
264
|
-
|
|
265
|
-
1. fetch a small input set from BigQuery
|
|
266
|
-
2. ask OpenAI / Hunter for candidate domains
|
|
267
|
-
3. pick a domain and write it back
|
|
268
|
-
|
|
269
|
-
The CLI now models that logic directly and improves it:
|
|
270
38
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
- `domainfinder:input-sql` generates a replacement input view driven by `linkedin_companies`, not `leadPool_new`
|
|
274
|
-
- `domainfinder:select` applies deterministic domain selection rules
|
|
275
|
-
- `domainfinder:audit` turns decisions into a review queue and writeback summary
|
|
276
|
-
- `domainfinder:compare-pipedream` quantifies how often the old selector would disagree with the improved selector
|
|
277
|
-
- `domainfinder:audit-existing:bq` measures current warehouse-visible mismatches and bad chosen domains
|
|
278
|
-
- `domainfinder:audit-delta` compares two audit snapshots and reports metric deltas
|
|
279
|
-
- `domainfinder:repair-existing:bq` generates (and optionally executes) targeted repair writes with selectable mode:
|
|
280
|
-
- `conservative`: only missing/blacklisted chosen domains
|
|
281
|
-
- `aggressive`: missing/blacklisted plus all mismatches
|
|
282
|
-
- `mismatch-only`: only mismatched chosen domains
|
|
283
|
-
- `domainfinder:writeback-sql` emits conservative SQL for `domainFinder_output`
|
|
284
|
-
- `domainfinder:writeback:bq` can execute that writeback in BigQuery when explicitly asked
|
|
285
|
-
- `domainfinder:run:bq` runs the full candidate -> decision -> audit -> writeback pipeline and stores all artifacts
|
|
286
|
-
|
|
287
|
-
Improved selection policy:
|
|
288
|
-
|
|
289
|
-
1. prefer `domain_linkedin` when present and not blacklisted
|
|
290
|
-
2. otherwise prefer `website_linkedin` root domain when present and not blacklisted
|
|
291
|
-
3. otherwise choose the non-blacklisted candidate with the highest Hunter email count
|
|
292
|
-
4. otherwise fall back to the first non-null candidate
|
|
293
|
-
|
|
294
|
-
This removes the earlier failure mode where OpenAI or Hunter could override a good LinkedIn domain purely because of a higher Hunter count.
|
|
295
|
-
|
|
296
|
-
Writeback policy:
|
|
297
|
-
|
|
298
|
-
- write to `SalesPrompter.domainFinder_output`, which is the source feeding `linkedin_companies.domain`
|
|
299
|
-
- exclude `no-domain` decisions
|
|
300
|
-
- exclude blacklisted domains from generated writeback SQL
|
|
301
|
-
- preserve batch provenance through `trace_id`
|
|
302
|
-
|
|
303
|
-
## Development
|
|
304
|
-
|
|
305
|
-
```bash
|
|
306
|
-
npm install
|
|
307
|
-
npm run build
|
|
308
|
-
node ./dist/cli.js --help
|
|
309
|
-
npm test
|
|
39
|
+
# Check crawl status
|
|
40
|
+
salesprompter salesnav:crawl:status --job-id "<job-id>"
|
|
310
41
|
```
|
|
311
42
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
- The CLI runs `bq query` with `--project_id` from `BQ_PROJECT_ID`.
|
|
315
|
-
- Fallback order: `BQ_PROJECT_ID` -> `GOOGLE_CLOUD_PROJECT` -> `GCLOUD_PROJECT` -> `icpidentifier`.
|
|
316
|
-
|
|
317
|
-
## Real Deel Flow
|
|
43
|
+
## Output modes
|
|
318
44
|
|
|
319
|
-
|
|
45
|
+
- `--json` for machine-readable output
|
|
46
|
+
- `--quiet` to suppress successful payload output
|
|
320
47
|
|
|
321
|
-
|
|
48
|
+
## Notes
|
|
322
49
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
4. `salesprompter leads:score --icp ./data/deel-icp.json --in ./data/deel-enriched.json --out ./data/deel-scored.json`
|
|
327
|
-
5. `salesprompter sync:outreach --target instantly --in ./data/deel-scored.json --campaign-id "$INSTANTLY_CAMPAIGN_ID"`
|
|
328
|
-
6. Add `--apply` only when the dry-run output looks correct.
|
|
50
|
+
- Use your own authorized LinkedIn / Sales Navigator access.
|
|
51
|
+
- Respect LinkedIn and provider terms.
|
|
52
|
+
- The CLI is designed for interactive users and agent-assisted workflows.
|
|
329
53
|
|
|
330
|
-
##
|
|
54
|
+
## Links
|
|
331
55
|
|
|
332
|
-
-
|
|
333
|
-
-
|
|
334
|
-
-
|
|
335
|
-
- Replace `DryRunSyncProvider` with real HubSpot, Salesforce, Pipedrive, Instantly, Apollo, or Outreach clients.
|
|
336
|
-
- Add provider selection and credentials for a first real `--domain deel.com` workflow.
|
|
337
|
-
- Replace configurable `bq` field mapping with a typed adapter per warehouse schema.
|
|
338
|
-
- Add a real candidate-fetch command that reads domain candidates from BigQuery and feeds them into `domainfinder:select`.
|
|
56
|
+
- Docs: https://salesprompter-cli.vercel.app
|
|
57
|
+
- App: https://salesprompter.ai
|
|
58
|
+
- Repository: https://github.com/danielsinewe/salesprompter-cli
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { access, appendFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { access, appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
+
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { emitKeypressEvents } from "node:readline";
|
|
7
8
|
import { createInterface } from "node:readline/promises";
|
|
@@ -19,6 +20,7 @@ import { analyzeHistoricalQueries } from "./historical-queries.js";
|
|
|
19
20
|
import { buildHistoricalVendorIcp, buildVendorIcp } from "./icp-templates.js";
|
|
20
21
|
import { InstantlySyncProvider } from "./instantly.js";
|
|
21
22
|
import { crawlLinkedInProductCategory } from "./linkedin-products.js";
|
|
23
|
+
import { claimValidatedSalesNavigatorSessionCookieForCli } from "./linkedin-session.js";
|
|
22
24
|
import { buildLeadlistsFunnelQueries } from "./leadlists-funnel.js";
|
|
23
25
|
import { readJsonFile, splitCsv, writeJsonFile, writeTextFile } from "./io.js";
|
|
24
26
|
import { buildSalesNavigatorCrawlPreview, createSalesNavigatorCrawlSeed, DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS, buildSalesNavigatorPeopleSlice, deriveSalesNavigatorTitleQuerySeeds, expandSalesNavigatorCrawlAttempt, SalesNavigatorSliceTooBroadError } from "./sales-navigator.js";
|
|
@@ -783,29 +785,10 @@ function summarizeSalesNavigatorQuery(url, appliedFilters) {
|
|
|
783
785
|
appliedFilters
|
|
784
786
|
};
|
|
785
787
|
}
|
|
786
|
-
function extractSalesNavigatorFilterTypes(url, appliedFilters) {
|
|
787
|
-
const filterTypes = new Set(appliedFilters.map((filter) => filter.type));
|
|
788
|
-
const decodedQuery = decodeSalesNavigatorQueryParam(url) ?? "";
|
|
789
|
-
for (const match of decodedQuery.matchAll(/type:([A-Z_]+)/g)) {
|
|
790
|
-
const value = match[1]?.trim();
|
|
791
|
-
if (value) {
|
|
792
|
-
filterTypes.add(value);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
return [...filterTypes];
|
|
796
|
-
}
|
|
797
788
|
function shouldPreSplitSalesNavigatorRootSlice(slice, maxSplitDepth) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
if (!nextSalesNavigatorSplitDimension(slice, maxSplitDepth)) {
|
|
802
|
-
return false;
|
|
803
|
-
}
|
|
804
|
-
const filterTypes = new Set(extractSalesNavigatorFilterTypes(slice.slicedQueryUrl, slice.appliedFilters));
|
|
805
|
-
if (!filterTypes.has("CURRENT_TITLE")) {
|
|
806
|
-
return false;
|
|
807
|
-
}
|
|
808
|
-
return !DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS.some((dimension) => filterTypes.has(dimension.filterType));
|
|
789
|
+
void slice;
|
|
790
|
+
void maxSplitDepth;
|
|
791
|
+
return false;
|
|
809
792
|
}
|
|
810
793
|
function buildTraceHeaders(traceId) {
|
|
811
794
|
return traceId ? { "X-Salesprompter-Trace-Id": traceId } : {};
|
|
@@ -1632,6 +1615,10 @@ async function runSalesNavigatorCrawlAttempt(session, attempt, options, context)
|
|
|
1632
1615
|
if (totalResults === null || totalResults > attempt.maxResultsPerSearch) {
|
|
1633
1616
|
return probeResult;
|
|
1634
1617
|
}
|
|
1618
|
+
const splitTriggerResults = Math.min(attempt.maxResultsPerSearch, SALES_NAVIGATOR_SPLIT_TRIGGER_RESULTS);
|
|
1619
|
+
if (totalResults > splitTriggerResults) {
|
|
1620
|
+
throw new SalesNavigatorSliceTooBroadError(`Sales Navigator slice produced ${totalResults} results, exceeding the split trigger of ${splitTriggerResults}.`, { totalResults });
|
|
1621
|
+
}
|
|
1635
1622
|
return await runSalesNavigatorExportWithAgentWait(session, {
|
|
1636
1623
|
sourceQueryUrl: attempt.sourceQueryUrl,
|
|
1637
1624
|
slicedQueryUrl: attempt.slicedQueryUrl,
|
|
@@ -1731,10 +1718,112 @@ function nextSalesNavigatorSplitDimension(slice, maxSplitDepth) {
|
|
|
1731
1718
|
if (slice.depth >= maxSplitDepth) {
|
|
1732
1719
|
return null;
|
|
1733
1720
|
}
|
|
1734
|
-
|
|
1721
|
+
const usedDimensionKeys = new Set(slice.splitTrail.map((entry) => entry.key));
|
|
1722
|
+
const orderedDimensions = getLearnedSalesNavigatorDimensionOrder();
|
|
1723
|
+
return orderedDimensions.find((dimension) => !usedDimensionKeys.has(dimension.key)) ?? null;
|
|
1735
1724
|
}
|
|
1736
1725
|
const SALES_NAVIGATOR_COOKIE_RETRY_LIMIT = 8;
|
|
1737
1726
|
const SALES_NAVIGATOR_RESULT_RETRY_LIMIT = 3;
|
|
1727
|
+
const SALES_NAVIGATOR_SPLIT_TRIGGER_RESULTS = 1500;
|
|
1728
|
+
const SALES_NAVIGATOR_FILTER_IMPACT_MIN_OBSERVATIONS = 3;
|
|
1729
|
+
let salesNavigatorFilterImpactModel = null;
|
|
1730
|
+
let salesNavigatorFilterImpactLoaded = false;
|
|
1731
|
+
function getSalesprompterConfigDir() {
|
|
1732
|
+
const override = process.env.SALESPROMPTER_CONFIG_DIR?.trim();
|
|
1733
|
+
if (override !== undefined && override.length > 0) {
|
|
1734
|
+
return override;
|
|
1735
|
+
}
|
|
1736
|
+
return path.join(os.homedir(), ".config", "salesprompter");
|
|
1737
|
+
}
|
|
1738
|
+
function getSalesNavigatorFilterImpactPath() {
|
|
1739
|
+
return path.join(getSalesprompterConfigDir(), "salesnav-filter-impact.json");
|
|
1740
|
+
}
|
|
1741
|
+
async function loadSalesNavigatorFilterImpactModel() {
|
|
1742
|
+
if (salesNavigatorFilterImpactLoaded) {
|
|
1743
|
+
return salesNavigatorFilterImpactModel;
|
|
1744
|
+
}
|
|
1745
|
+
salesNavigatorFilterImpactLoaded = true;
|
|
1746
|
+
const filePath = getSalesNavigatorFilterImpactPath();
|
|
1747
|
+
try {
|
|
1748
|
+
const content = await readFile(filePath, "utf8");
|
|
1749
|
+
const parsed = JSON.parse(content);
|
|
1750
|
+
if (parsed && parsed.version === 1 && parsed.dimensions && typeof parsed.dimensions === "object") {
|
|
1751
|
+
salesNavigatorFilterImpactModel = parsed;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
catch {
|
|
1755
|
+
salesNavigatorFilterImpactModel = null;
|
|
1756
|
+
}
|
|
1757
|
+
return salesNavigatorFilterImpactModel;
|
|
1758
|
+
}
|
|
1759
|
+
async function persistSalesNavigatorFilterImpactModel() {
|
|
1760
|
+
if (!salesNavigatorFilterImpactModel) {
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
const filePath = getSalesNavigatorFilterImpactPath();
|
|
1764
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
1765
|
+
await writeFile(filePath, `${JSON.stringify(salesNavigatorFilterImpactModel, null, 2)}\n`, "utf8");
|
|
1766
|
+
}
|
|
1767
|
+
function getLearnedSalesNavigatorDimensionOrder() {
|
|
1768
|
+
const model = salesNavigatorFilterImpactModel;
|
|
1769
|
+
if (!model) {
|
|
1770
|
+
return DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS;
|
|
1771
|
+
}
|
|
1772
|
+
const defaultIndex = new Map(DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS.map((dimension, index) => [dimension.key, index]));
|
|
1773
|
+
return [...DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS].sort((left, right) => {
|
|
1774
|
+
const leftStats = model.dimensions[left.key];
|
|
1775
|
+
const rightStats = model.dimensions[right.key];
|
|
1776
|
+
const leftReliable = (leftStats?.observations ?? 0) >= SALES_NAVIGATOR_FILTER_IMPACT_MIN_OBSERVATIONS;
|
|
1777
|
+
const rightReliable = (rightStats?.observations ?? 0) >= SALES_NAVIGATOR_FILTER_IMPACT_MIN_OBSERVATIONS;
|
|
1778
|
+
if (leftReliable && rightReliable) {
|
|
1779
|
+
const delta = (leftStats?.avgResults ?? Number.POSITIVE_INFINITY) -
|
|
1780
|
+
(rightStats?.avgResults ?? Number.POSITIVE_INFINITY);
|
|
1781
|
+
if (delta !== 0) {
|
|
1782
|
+
return delta;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
else if (leftReliable !== rightReliable) {
|
|
1786
|
+
return leftReliable ? -1 : 1;
|
|
1787
|
+
}
|
|
1788
|
+
return (defaultIndex.get(left.key) ?? 0) - (defaultIndex.get(right.key) ?? 0);
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
async function recordSalesNavigatorFilterImpactObservation(slice, totalResults, options) {
|
|
1792
|
+
if (totalResults === null || totalResults === undefined || !Number.isFinite(totalResults)) {
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
const learnedDimension = slice.splitTrail.at(-1)?.key ?? null;
|
|
1796
|
+
if (!learnedDimension) {
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
await loadSalesNavigatorFilterImpactModel();
|
|
1800
|
+
if (!salesNavigatorFilterImpactModel) {
|
|
1801
|
+
salesNavigatorFilterImpactModel = {
|
|
1802
|
+
version: 1,
|
|
1803
|
+
updatedAt: new Date().toISOString(),
|
|
1804
|
+
dimensions: {}
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
const previous = salesNavigatorFilterImpactModel.dimensions[learnedDimension];
|
|
1808
|
+
const observations = (previous?.observations ?? 0) + 1;
|
|
1809
|
+
const sumResults = (previous?.sumResults ?? 0) + totalResults;
|
|
1810
|
+
const avgResults = sumResults / observations;
|
|
1811
|
+
salesNavigatorFilterImpactModel.dimensions[learnedDimension] = {
|
|
1812
|
+
observations,
|
|
1813
|
+
sumResults,
|
|
1814
|
+
avgResults,
|
|
1815
|
+
lastObservedAt: new Date().toISOString()
|
|
1816
|
+
};
|
|
1817
|
+
salesNavigatorFilterImpactModel.updatedAt = new Date().toISOString();
|
|
1818
|
+
await persistSalesNavigatorFilterImpactModel();
|
|
1819
|
+
await options?.logger?.log("salesnav.filter_impact.updated", {
|
|
1820
|
+
dimensionKey: learnedDimension,
|
|
1821
|
+
observations,
|
|
1822
|
+
avgResults,
|
|
1823
|
+
totalResults,
|
|
1824
|
+
outcome: options?.outcome ?? null
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1738
1827
|
function buildSalesNavigatorSplitChildren(slice, dimension) {
|
|
1739
1828
|
const attempt = buildSalesNavigatorCrawlAttemptFromClaimedSlice(slice);
|
|
1740
1829
|
return expandSalesNavigatorCrawlAttempt(attempt, dimension).map((child) => ({
|
|
@@ -1812,6 +1901,42 @@ function buildSalesNavigatorSliceFailureReport(slice, error, options) {
|
|
|
1812
1901
|
function formatSalesNavigatorSplitTrail(splitTrail) {
|
|
1813
1902
|
return splitTrail.map((entry) => `${entry.key}:${entry.value.text}`);
|
|
1814
1903
|
}
|
|
1904
|
+
async function ensureSalesNavigatorSessionPoolReady(queryUrl, options) {
|
|
1905
|
+
try {
|
|
1906
|
+
await options.logger?.log("salesnav.session_pool.preflight.started", {
|
|
1907
|
+
source: options.source,
|
|
1908
|
+
queryUrl
|
|
1909
|
+
});
|
|
1910
|
+
const claimed = await claimValidatedSalesNavigatorSessionCookieForCli({
|
|
1911
|
+
queryUrl,
|
|
1912
|
+
source: options.source,
|
|
1913
|
+
env: process.env
|
|
1914
|
+
});
|
|
1915
|
+
await options.logger?.log("salesnav.session_pool.preflight.completed", {
|
|
1916
|
+
source: options.source,
|
|
1917
|
+
queryUrl,
|
|
1918
|
+
status: claimed ? "ok" : "skipped",
|
|
1919
|
+
selectedSessionUserEmail: claimed?.userEmail ?? null,
|
|
1920
|
+
selectedSessionUserHandle: claimed?.userHandle ?? null,
|
|
1921
|
+
selectedSessionCookieSha256: claimed?.sessionCookieSha256 ?? null
|
|
1922
|
+
});
|
|
1923
|
+
return {
|
|
1924
|
+
ready: true
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
catch (error) {
|
|
1928
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1929
|
+
await options.logger?.log("salesnav.session_pool.preflight.failed", {
|
|
1930
|
+
source: options.source,
|
|
1931
|
+
queryUrl,
|
|
1932
|
+
error: message
|
|
1933
|
+
});
|
|
1934
|
+
return {
|
|
1935
|
+
ready: false,
|
|
1936
|
+
error: message
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1815
1940
|
async function processSalesNavigatorClaimedCrawlSlice(session, jobId, slice, options) {
|
|
1816
1941
|
let currentSession = session;
|
|
1817
1942
|
await options.logger?.log("salesnav.crawl.slice.claimed", {
|
|
@@ -1869,7 +1994,8 @@ async function processSalesNavigatorClaimedCrawlSlice(session, jobId, slice, opt
|
|
|
1869
1994
|
error: `Pre-split by ${nextDimension.key}`,
|
|
1870
1995
|
errorCode: "presplit_root_title_query",
|
|
1871
1996
|
totalResults: null
|
|
1872
|
-
}
|
|
1997
|
+
},
|
|
1998
|
+
forceSessionPoolRecheck: false
|
|
1873
1999
|
};
|
|
1874
2000
|
}
|
|
1875
2001
|
}
|
|
@@ -1909,6 +2035,10 @@ async function processSalesNavigatorClaimedCrawlSlice(session, jobId, slice, opt
|
|
|
1909
2035
|
})
|
|
1910
2036
|
}, options.traceId);
|
|
1911
2037
|
currentSession = reported.session;
|
|
2038
|
+
await recordSalesNavigatorFilterImpactObservation(slice, result.totalResults ?? null, {
|
|
2039
|
+
logger: options.logger,
|
|
2040
|
+
outcome: "exported"
|
|
2041
|
+
});
|
|
1912
2042
|
await options.logger?.log("salesnav.crawl.slice.exported", {
|
|
1913
2043
|
jobId,
|
|
1914
2044
|
sliceId: slice.id,
|
|
@@ -1927,7 +2057,8 @@ async function processSalesNavigatorClaimedCrawlSlice(session, jobId, slice, opt
|
|
|
1927
2057
|
outcome: "exported",
|
|
1928
2058
|
runId: result.runId,
|
|
1929
2059
|
totalResults: result.totalResults ?? null
|
|
1930
|
-
}
|
|
2060
|
+
},
|
|
2061
|
+
forceSessionPoolRecheck: false
|
|
1931
2062
|
};
|
|
1932
2063
|
}
|
|
1933
2064
|
catch (error) {
|
|
@@ -1956,6 +2087,10 @@ async function processSalesNavigatorClaimedCrawlSlice(session, jobId, slice, opt
|
|
|
1956
2087
|
});
|
|
1957
2088
|
const reported = await reportSalesNavigatorCrawlSlice(currentSession, jobId, payload, options.traceId);
|
|
1958
2089
|
currentSession = reported.session;
|
|
2090
|
+
await recordSalesNavigatorFilterImpactObservation(slice, payload.totalResults ?? null, {
|
|
2091
|
+
logger: options.logger,
|
|
2092
|
+
outcome: payload.outcome
|
|
2093
|
+
});
|
|
1959
2094
|
await options.logger?.log("salesnav.crawl.slice.reported", {
|
|
1960
2095
|
jobId,
|
|
1961
2096
|
sliceId: slice.id,
|
|
@@ -1976,11 +2111,13 @@ async function processSalesNavigatorClaimedCrawlSlice(session, jobId, slice, opt
|
|
|
1976
2111
|
error: payload.error,
|
|
1977
2112
|
errorCode: payload.errorCode,
|
|
1978
2113
|
totalResults: payload.totalResults
|
|
1979
|
-
}
|
|
2114
|
+
},
|
|
2115
|
+
forceSessionPoolRecheck: payload.errorCode === "invalid_session"
|
|
1980
2116
|
};
|
|
1981
2117
|
}
|
|
1982
2118
|
}
|
|
1983
2119
|
async function executeSalesNavigatorCrawlJob(session, jobId, options) {
|
|
2120
|
+
await loadSalesNavigatorFilterImpactModel();
|
|
1984
2121
|
let currentSession = session;
|
|
1985
2122
|
let claimedSlices = 0;
|
|
1986
2123
|
const seenSliceIds = new Set();
|
|
@@ -1992,11 +2129,56 @@ async function executeSalesNavigatorCrawlJob(session, jobId, options) {
|
|
|
1992
2129
|
const inFlight = new Map();
|
|
1993
2130
|
let nextSlot = 0;
|
|
1994
2131
|
let noMoreClaimableWork = false;
|
|
2132
|
+
let sessionPoolFailures = 0;
|
|
2133
|
+
let nextSessionPoolRetryAt = 0;
|
|
2134
|
+
let lastSessionPoolReadyAt = 0;
|
|
2135
|
+
const sessionPoolReadinessCooldownMs = 120_000;
|
|
1995
2136
|
while (true) {
|
|
1996
2137
|
while (!noMoreClaimableWork && inFlight.size < parallelExports) {
|
|
1997
2138
|
if (claimedSlices >= options.maxSlices) {
|
|
1998
2139
|
break;
|
|
1999
2140
|
}
|
|
2141
|
+
if (inFlight.size === 0) {
|
|
2142
|
+
const now = Date.now();
|
|
2143
|
+
if (now < nextSessionPoolRetryAt) {
|
|
2144
|
+
await delay(Math.max(0, nextSessionPoolRetryAt - now));
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
2147
|
+
if (now - lastSessionPoolReadyAt >= sessionPoolReadinessCooldownMs) {
|
|
2148
|
+
const readiness = await ensureSalesNavigatorSessionPoolReady(job?.sourceQueryUrl ?? "https://www.linkedin.com/sales/search/people", {
|
|
2149
|
+
logger: options.logger,
|
|
2150
|
+
source: "cli_salesnav_crawl_preflight"
|
|
2151
|
+
});
|
|
2152
|
+
if (!readiness.ready) {
|
|
2153
|
+
sessionPoolFailures += 1;
|
|
2154
|
+
idlePollCount += 1;
|
|
2155
|
+
const waitSeconds = Math.min(120, 10 * Math.max(1, sessionPoolFailures));
|
|
2156
|
+
nextSessionPoolRetryAt = Date.now() + waitSeconds * 1000;
|
|
2157
|
+
await options.logger?.log("salesnav.crawl.session_pool.waiting", {
|
|
2158
|
+
jobId,
|
|
2159
|
+
idlePollCount,
|
|
2160
|
+
idleMaxPolls: options.idleMaxPolls,
|
|
2161
|
+
sessionPoolFailures,
|
|
2162
|
+
waitSeconds,
|
|
2163
|
+
error: readiness.error
|
|
2164
|
+
});
|
|
2165
|
+
if (idlePollCount >= options.idleMaxPolls) {
|
|
2166
|
+
lastOutcome = {
|
|
2167
|
+
outcome: "terminal_failed",
|
|
2168
|
+
error: readiness.error ??
|
|
2169
|
+
`Sales Navigator session pool stayed unavailable for ${options.idleMaxPolls} checks.`,
|
|
2170
|
+
errorCode: "blocked_no_valid_salesnav_session"
|
|
2171
|
+
};
|
|
2172
|
+
noMoreClaimableWork = true;
|
|
2173
|
+
break;
|
|
2174
|
+
}
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
sessionPoolFailures = 0;
|
|
2178
|
+
nextSessionPoolRetryAt = 0;
|
|
2179
|
+
lastSessionPoolReadyAt = Date.now();
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2000
2182
|
const claimed = await claimNextSalesNavigatorCrawlSlice(currentSession, jobId, options.traceId);
|
|
2001
2183
|
currentSession = claimed.session;
|
|
2002
2184
|
job = claimed.value.job;
|
|
@@ -2089,6 +2271,10 @@ async function executeSalesNavigatorCrawlJob(session, jobId, options) {
|
|
|
2089
2271
|
job = completed.value.job;
|
|
2090
2272
|
activeSlice = completed.value.activeSlice;
|
|
2091
2273
|
lastOutcome = completed.value.lastOutcome;
|
|
2274
|
+
if (completed.value.forceSessionPoolRecheck) {
|
|
2275
|
+
lastSessionPoolReadyAt = 0;
|
|
2276
|
+
nextSessionPoolRetryAt = 0;
|
|
2277
|
+
}
|
|
2092
2278
|
}
|
|
2093
2279
|
if (!job) {
|
|
2094
2280
|
const status = await getSalesNavigatorCrawlStatus(currentSession, jobId, options.traceId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "salesprompter-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "JSON-first sales prospecting CLI for ICP definition, lead generation, enrichment, scoring, and CRM/outreach sync.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
"verify:all": "npm run check && npm test && npm run build:docs:site && npm run docs:broken-links && npm run docs:a11y",
|
|
23
23
|
"vercel-build": "npm run build:docs:site"
|
|
24
24
|
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20.0.0"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
25
31
|
"keywords": [
|
|
26
32
|
"sales",
|
|
27
33
|
"salesprompter",
|
|
@@ -57,6 +63,9 @@
|
|
|
57
63
|
"commander": "^14.0.1",
|
|
58
64
|
"zod": "^4.1.5"
|
|
59
65
|
},
|
|
66
|
+
"overrides": {
|
|
67
|
+
"undici": "^7.24.0"
|
|
68
|
+
},
|
|
60
69
|
"devDependencies": {
|
|
61
70
|
"@types/node": "^24.3.0",
|
|
62
71
|
"gray-matter": "^4.0.3",
|