nordic-data 0.2.0 → 0.4.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 (3) hide show
  1. package/README.md +134 -88
  2. package/bin/cli.js +620 -13
  3. package/package.json +19 -2
package/README.md CHANGED
@@ -1,118 +1,160 @@
1
- # Nordic Data MCP Server
1
+ # Nordic Data
2
2
 
3
- > Live Nordic public-sector and Norwegian + Swedish corporate data, exposed as 29 MCP tools for AI agents.
3
+ > Norwegian, Swedish, and US business intelligence in one terminal plus a hosted MCP server for AI agents.
4
4
 
5
- **Hosted at:** `https://api.nordicdata.cloud/mcp`
6
- **Get a free API key:** [nordicdata.cloud](https://nordicdata.cloud)
7
- **Docs:** [nordicdata.cloud/docs#mcp](https://nordicdata.cloud/docs#mcp)
8
- **Smithery one-click install:** [smithery.ai/server/sofia-jameson-20/Nordic-Data](https://smithery.ai/server/sofia-jameson-20/Nordic-Data)
5
+ ```bash
6
+ npx nordic-data lookup 923609016 # full snapshot of Equinor
7
+ npx nordic-data list elektriker --city Stavanger --employees-min 5
8
+ npx nordic-data nl "Norwegian fintech startups under 20 employees"
9
+ npx nordic-data kyb 918274758 # full due-diligence pack
10
+ npx nordic-data verify-invoice 923609016 DE89370400440532013000
11
+ npx nordic-data us-search Microsoft
12
+ ```
9
13
 
10
- This repository contains setup instructions and configuration snippets. The server itself is hosted — no local install required.
14
+ **Get a free key:** [nordicdata.cloud](https://nordicdata.cloud) 500 requests/month, no card
15
+ **Docs:** [nordicdata.cloud/docs](https://nordicdata.cloud/docs)
11
16
 
12
17
  ---
13
18
 
14
- ## What's covered
19
+ ## Install
15
20
 
16
- - **Public procurement** — Nordic tender notices and contract awards across two coverage tiers: an EU-tier (above-EU-threshold notices for Norway, Sweden, Denmark, Finland, Iceland) AND a Norway-tier (Norway-only, **including below-EU-threshold municipal/county tenders that no other API exposes cleanly**)
17
- - **Norwegian company registry** — full company data + complete shareholder cap tables (3M+ positions across 396K companies)
18
- - **Swedish company data** — identity (name, address, VAT registration) for any of ~1.6M Swedish companies, plus AI-enriched contact data (verified emails, direct phones, named executives) via the dedicated Sweden contact tool
19
- - **Danish + Finnish company data** — identity, addresses, basic phone/employees (Denmark) and multilingual name history (Finland)
20
- - **Officer & ownership network** — board memberships, shortest-path queries, full role history (Norway)
21
- - **News** — Norwegian-language news mentioning a company
22
- - **R&D activity** — research-grant history per recipient
23
- - **Tech intelligence** find companies using a specific technology
24
- - **Compliance** — sanctions screening for any name or company + officers (global watchlist coverage)
25
- - **AI summaries** — executive narratives and peer benchmarks per company
21
+ ```bash
22
+ npx nordic-data --help # zero install
23
+ # or
24
+ npm install -g nordic-data
25
+ nordic-data --help
26
+ ```
27
+
28
+ ## Configure your API key
29
+
30
+ ```bash
31
+ export NORDIC_DATA_KEY=nrd_live_… # or pass --key on each call
32
+ ```
33
+
34
+ Most commands work without a key on the public widget tier (4 lookups per IP per 24h). Discovery, KYB, AI, and US commands all require a key.
26
35
 
27
- ## Setup
36
+ ---
37
+
38
+ ## Commands
39
+
40
+ ### Core lookup (Norway + Sweden)
41
+ | Command | What it does |
42
+ |---|---|
43
+ | `search <query>` | Search companies by name or org number |
44
+ | `lookup <orgnr>` | Full snapshot of one company (NO 9-digit + SE 10-digit auto-routes) |
45
+ | `contacts <orgnr>` | Emails, phones, and named executives |
46
+ | `board <orgnr>` | Board + leadership |
47
+ | `finances <orgnr>` | Latest financial summary |
48
+ | `procurement <orgnr>` | Public-sector contract aggregates |
49
+ | `grants <orgnr>` | EU R&D grant participations |
50
+ | `sanctions <orgnr>` | Sanctions screening (OFAC + EU) on company + officers |
51
+ | `shareholders <orgnr>` | Shareholder cap-table |
52
+ | `contacts-se <orgnr>` | Sweden: identity + AI-enriched contacts |
53
+
54
+ ### Discovery (v0.4.0)
55
+ | Command | What it does |
56
+ |---|---|
57
+ | `find <q>` | Universal resolver: name / orgnr / domain / email → company |
58
+ | `list <profession>` | Filter NO companies by industry (× `--city` × `--employees-min`) |
59
+ | `nl "<question>"` | Natural-language discovery — LLM parses to filter spec |
60
+ | `near <lat> <lng>` | Geo-radius search (`--radius` km, `--profession` <slug>) |
61
+ | `similar <orgnr>` | Lookalike companies by industry + geo + size |
62
+ | `trades <slug>` | Vertical shortcut: elektriker, rorlegger, snekker, tannlege, etc. |
63
+ | `decision-makers <orgnr>` | CEO + board + named contacts in one call |
64
+ | `resolve <type> <value>` | email / domain / phone / iban / vat / lei / address / person → entity |
65
+
66
+ ### KYB & Compliance & AI (v0.4.0)
67
+ | Command | What it does |
68
+ |---|---|
69
+ | `kyb <orgnr>` | One-call due-diligence pack (identity + sanctions + officers + signals) |
70
+ | `diff <orgnr>` | Typed changelog of recent changes (`--since YYYY-MM-DD`) |
71
+ | `verify-invoice <o> <iban>` | Invoice fraud check via IBAN country-mismatch heuristic |
72
+ | `explain <orgnr>` | AI 3-sentence narrative |
73
+ | `ask <orgnr> "<q>"` | Q&A grounded in the company's data |
74
+ | `pitch <orgnr>` | Sales talking-points referencing concrete facts |
75
+
76
+ ### United States (v0.4.0 — SEC EDGAR + USA Spending)
77
+ | Command | What it does |
78
+ |---|---|
79
+ | `us-search <query>` | Search ~10K US public companies by name / ticker |
80
+ | `us-lookup <q>` | Resolve a US public company by CIK, ticker, or name |
81
+ | `us-filings <cik>` | Recent SEC filings (`--type 10-K`) with direct EDGAR URLs |
82
+ | `us-kyb <cik>` | US KYB pack: identity + filings + sanctions + federal contracts |
83
+
84
+ ### Meta
85
+ | Command | What it does |
86
+ |---|---|
87
+ | `mcp` | Print MCP config snippet for Claude Desktop / Cursor |
88
+ | `signup` | Open the free-tier signup in your browser |
89
+ | `--help`, `-h` | Help (per-command help with `<cmd> --help`) |
90
+ | `--version`, `-v` | Show version |
91
+
92
+ ### Flags
93
+ - `--json` — output raw JSON instead of formatted
94
+ - `--key <api-key>` — API key (or set `NORDIC_DATA_KEY` env var)
95
+ - `--no-color` — disable ANSI colors
96
+
97
+ ---
98
+
99
+ ## MCP server (for AI agents)
28
100
 
29
- ### Claude Desktop
101
+ The same backend is also a [Model Context Protocol](https://modelcontextprotocol.io) server with **69 tools** exposed natively for AI agents — no SDK glue required.
30
102
 
31
- Edit `claude_desktop_config.json`:
103
+ ### Hosted endpoint
104
+
105
+ ```
106
+ URL: https://api.nordicdata.cloud/mcp
107
+ Header: X-API-Key: <your key>
108
+ Transport: Streamable HTTP
109
+ ```
110
+
111
+ ### Claude Desktop / Cursor config
112
+
113
+ Edit `claude_desktop_config.json` (or `~/.cursor/mcp.json`):
32
114
 
33
115
  ```json
34
116
  {
35
117
  "mcpServers": {
36
118
  "nordic-data": {
37
119
  "url": "https://api.nordicdata.cloud/mcp",
38
- "headers": {
39
- "X-API-Key": "YOUR_API_KEY_HERE"
40
- }
120
+ "headers": { "X-API-Key": "YOUR_API_KEY_HERE" }
41
121
  }
42
122
  }
43
123
  }
44
124
  ```
45
125
 
46
- Restart Claude Desktop. The 29 tools appear in the MCP picker.
126
+ Restart the client all 69 tools appear in the MCP picker.
47
127
 
48
- ### Cursor
128
+ ### Smithery one-click install
49
129
 
50
- Edit `~/.cursor/mcp.json` with the same JSON.
130
+ [smithery.ai/server/sofia-jameson-20/Nordic-Data](https://smithery.ai/server/sofia-jameson-20/Nordic-Data)
51
131
 
52
- ### Any MCP-aware client
132
+ ---
53
133
 
54
- ```
55
- URL: https://api.nordicdata.cloud/mcp
56
- Header: X-API-Key: <your key>
57
- Transport: Streamable HTTP
58
- ```
134
+ ## What's covered
135
+
136
+ - **Norwegian company registry** — full company data + complete shareholder cap tables (3M+ positions across 396K companies)
137
+ - **Swedish company data** — identity (~1.6M companies) + AI-enriched contacts (verified emails, direct phones, named executives)
138
+ - **Danish + Finnish company data** — identity + basic enrichment
139
+ - **United States public companies** — SEC EDGAR (~10K listed entities, full identity + filings) + USA Spending (federal contracts since 2008)
140
+ - **Officer & ownership network (Norway)** — board memberships, shortest-path queries, full role history
141
+ - **Public procurement (5 Nordic countries)** — EU-tier (above-threshold) + Norway-tier (below-EU-threshold municipal/county tenders)
142
+ - **Sanctions / AML** — OFAC SDN + EU consolidated lists, with strict-surname disambiguation to suppress first-name-only false positives
143
+ - **R&D grants** — Horizon Europe participations per recipient
144
+ - **Tech intelligence** — find companies using a specific technology stack
145
+ - **News** — Norwegian-language news mentioning a company
146
+ - **AI / NL** — natural-language search, Q&A, talking-points, narrative for any covered company
147
+
148
+ ## Example prompts (via the MCP server in Claude/Cursor)
59
149
 
60
- ## Tools (29 total)
61
-
62
- ### Procurement
63
- - `search_tenders` — Nordic procurement notices by country, keyword, CPV, date. Filter by coverage tier (EU-tier across all 5 Nordic countries, or Norway-tier including below-threshold municipal awards). Filter by `buyer_orgnr` to find all tenders from a specific Norwegian buyer.
64
- - `get_tender` — Full details by ID. Accepts both EU-tier format (`317565-2026`) and Norway-tier format (`2026-108414`).
65
- - `search_awards` — Search contract awards (who won what)
66
- - `get_tender_leaderboard` — Top public-sector buyers in a Nordic country
67
- - `get_company_contract_wins` — Public-sector contracts won by a Norwegian company
68
-
69
- ### Company data
70
- - `search_companies` — Search by name, industry, location (Norway: full-text; Denmark + Finland: identity-only; Sweden + Iceland: not yet supported for full-text — use `get_company` for orgnr lookup)
71
- - `get_company` — Full registry record. `country=NOR` (richest), `SWE` (identity), `DNK` (identity + phone + employees), `FIN` (identity + name history)
72
- - `get_company_contact` — Norway: public email + phone (with MX-verified email candidates) + named executives
73
- - **`get_company_se_contact` — Sweden: identity + AI-enriched contacts (MX-verified emails, direct phones, named executives where publicly documented)**
74
- - `get_company_narrative` — AI-generated executive summary
75
- - `get_company_peers` — Peer-cohort benchmarks
76
- - `get_company_snapshot` — One-call snapshot across every data layer
77
- - `get_company_changes` — Registry change history
78
- - `get_company_subsidiaries` — Subsidiaries registered under this orgnr
79
- - `bulk_get_companies` — Enrich a list of up to 100 companies in one call
80
-
81
- ### Financials & ownership (Norway)
82
- - `get_company_accounts` — Annual accounts (revenue, profit, equity)
83
- - `get_company_shareholders` — Full shareholder list — 3M+ positions across 396K companies
84
- - `get_shareholder_portfolio` — All companies a person/entity owns shares in
85
-
86
- ### Officer & network graph (Norway)
87
- - `search_persons` — Search persons in the Norwegian officer network
88
- - `get_person` — Full role history across Norwegian companies
89
- - `find_company_path` — Shortest path between two companies through shared officers
90
- - `get_person_network` — Find who is connected to a person via shared boards
91
-
92
- ### News
93
- - `get_company_news` — Recent Norwegian-language news mentioning a company
94
- - `search_news` — Search Norwegian news headlines
95
-
96
- ### R&D
97
- - `search_eu_grants` — Search EU research-grant history
98
- - `get_company_eu_grants` — Norwegian company's R&D grant participations
99
-
100
- ### Tech intelligence
101
- - `find_companies_using_tech` — Norwegian companies using a specific technology
102
-
103
- ### Compliance
104
- - `screen_for_sanctions` — Screen any name against international sanctions lists
105
- - `check_company_sanctions` — Sanctions screening for a Norwegian company + its officers
106
-
107
- ## Example prompts
108
-
109
- - *"Which Norwegian municipalities tendered snow-clearing contracts under 5M NOK with deadlines in the next 30 days?"* (uses below-threshold municipal coverage)
150
+ - *"Find Norwegian electricians in Stavanger with 5+ employees and verified contact emails."*
110
151
  - *"Pull the latest accounts and shareholders for orgnr 923609016."*
111
- - *"Get the verified contact emails and CFO for Klarna AB (Swedish orgnr 5566370985)."*
112
- - *"Find Norwegian companies using Snowflake."*
113
- - *"Who sits on the boards of all three of these companies?"*
114
- - *"Screen this list of suppliers against sanctions lists."*
115
- - *"Show me R&D grants won by Norwegian SMBs in clean energy."*
152
+ - *"Give me a full KYB pack on this counterparty before we onboard them."*
153
+ - *"Verify this invoice orgnr 923609016 asking us to pay German IBAN DE89370400440532013000. Is that suspicious?"*
154
+ - *"Which Norwegian municipalities tendered snow-clearing contracts under 5M NOK with deadlines in the next 30 days?"*
155
+ - *"Resolve ceo@equinor.com to a company."*
156
+ - *"Find 20 lookalike companies for Cognite for our prospect list."*
157
+ - *"Look up Tesla's most recent 10-K filing."*
116
158
 
117
159
  ## Pricing
118
160
 
@@ -127,3 +169,7 @@ See [nordicdata.cloud](https://nordicdata.cloud).
127
169
  - Docs: [nordicdata.cloud/docs](https://nordicdata.cloud/docs)
128
170
  - LLM-friendly site index: [nordicdata.cloud/llms.txt](https://nordicdata.cloud/llms.txt)
129
171
  - OpenAPI spec: [api.nordicdata.cloud/openapi.json](https://api.nordicdata.cloud/openapi.json)
172
+
173
+ ## License
174
+
175
+ MIT — see `LICENSE`.
package/bin/cli.js CHANGED
@@ -28,8 +28,9 @@ const HELP = `${c('bold', 'nordic-data')} ${c('dim', '— every Norwegian compan
28
28
 
29
29
  ${c('bold', 'USAGE')}
30
30
  nordic-data <command> [args]
31
+ nordic-data <command> --help ${c('dim', '# per-command help with examples')}
31
32
 
32
- ${c('bold', 'COMMANDS')}
33
+ ${c('bold', 'COMMANDS — core lookup (NO + SE)')}
33
34
  search <query> Search companies by name or org number
34
35
  lookup <orgnr> Full snapshot of one company
35
36
  contacts <orgnr> Emails, phones, and named executives
@@ -40,6 +41,32 @@ ${c('bold', 'COMMANDS')}
40
41
  sanctions <orgnr> Sanctions screening (EU/UN/OFAC) hits
41
42
  shareholders <orgnr> Shareholder graph aggregates (Norway)
42
43
  contacts-se <orgnr> Sweden: identity + AI-enriched contacts (10-digit orgnr)
44
+
45
+ ${c('bold', 'COMMANDS — discovery (v0.4.0)')}
46
+ find <q> Universal resolver: name / orgnr / domain / email → company
47
+ list <profession> Filter NO companies by industry (× --city × --employees-min)
48
+ nl "<question>" Natural-language discovery (LLM parses → filter spec → results)
49
+ near <lat> <lng> Geo-radius search (--radius km, --profession <slug>)
50
+ similar <orgnr> Lookalike companies by industry + geo + size
51
+ trades <slug> Vertical shortcut: elektriker, rorlegger, snekker, tannlege, etc.
52
+ decision-makers <orgnr> CEO + board + named contacts in one call
53
+ resolve <type> <value> email / domain / phone / iban / vat / lei / address / person → entity
54
+
55
+ ${c('bold', 'COMMANDS — KYB / Compliance / AI')}
56
+ kyb <orgnr> One-call due-diligence pack (identity + sanctions + officers + signals)
57
+ diff <orgnr> Typed changelog of recent changes (--since YYYY-MM-DD)
58
+ explain <orgnr> AI-generated 3-sentence narrative about the company
59
+ ask <orgnr> "<question>" Q&A grounded in the company's data
60
+ pitch <orgnr> Sales talking-points (referencing concrete facts)
61
+ verify-invoice <o> <iban> Invoice fraud check via IBAN country-mismatch heuristic
62
+
63
+ ${c('bold', 'COMMANDS — United States (SEC EDGAR + USA Spending)')}
64
+ us-search <query> Search US public companies by name / ticker
65
+ us-lookup <q> Resolve a US public company by CIK, ticker, or name
66
+ us-filings <cik> Recent SEC filings (--type 10-K to filter)
67
+ us-kyb <cik> US KYB pack (identity + filings + sanctions + federal contracts)
68
+
69
+ ${c('bold', 'COMMANDS — meta')}
43
70
  mcp Show MCP setup snippet for Claude Desktop / Cursor
44
71
  signup Open the free-tier signup page in your browser
45
72
  --help, -h Show this help
@@ -51,15 +78,39 @@ ${c('bold', 'FLAGS')}
51
78
  --no-color Disable ANSI colors
52
79
 
53
80
  ${c('bold', 'EXAMPLES')}
54
- ${c('dim', '# Look up Equinor (no key — uses the public widget tier)')}
81
+ ${c('dim', '# Core lookup')}
55
82
  nordic-data search equinor
56
83
  nordic-data lookup 923609016
57
84
  nordic-data contacts 923609016
58
-
59
- ${c('dim', '# Sweden: lookup + AI-enriched contacts (10-digit orgnr)')}
60
- nordic-data contacts-se 5566370985
61
-
62
- ${c('dim', '# Use your API key for higher limits')}
85
+ nordic-data contacts-se 5566370985 ${c('dim', '# Sweden (10-digit orgnr)')}
86
+
87
+ ${c('dim', '# Discovery — find a list of companies')}
88
+ nordic-data list elektriker --city Stavanger --employees-min 5
89
+ nordic-data nl "dentists in Bergen with 10+ employees"
90
+ nordic-data trades snekker --city Tromsø
91
+ nordic-data near 58.97 5.73 --radius 5 --profession rorlegger
92
+
93
+ ${c('dim', '# Universal resolver')}
94
+ nordic-data find Cognite ${c('dim', '# name → company')}
95
+ nordic-data resolve email ceo@equinor.com ${c('dim', '# email → company')}
96
+ nordic-data resolve domain cognite.com ${c('dim', '# domain → company')}
97
+
98
+ ${c('dim', '# KYB / Compliance')}
99
+ nordic-data kyb 923609016
100
+ nordic-data verify-invoice 923609016 DE89370400440532013000
101
+ nordic-data diff 923609016 --since 2026-01-01
102
+
103
+ ${c('dim', '# AI / NL')}
104
+ nordic-data explain 923609016
105
+ nordic-data ask 923609016 "who chairs the board"
106
+ nordic-data pitch 923609016
107
+
108
+ ${c('dim', '# United States (SEC EDGAR + USA Spending)')}
109
+ nordic-data us-search Microsoft
110
+ nordic-data us-kyb 0000789019
111
+ nordic-data us-filings 0000320193 --type 10-K
112
+
113
+ ${c('dim', '# Use your API key for full access (most v0.4.0 commands require a key)')}
63
114
  export NORDIC_DATA_KEY=nrd_live_...
64
115
  nordic-data lookup 923609016 --json | jq
65
116
 
@@ -72,22 +123,241 @@ ${c('bold', 'FREE TIER')}
72
123
  ${c('dim', 'Without a key, this CLI uses the public widget tier (4 lookups/IP/24h).')}
73
124
  `;
74
125
 
126
+ // Per-command help text. Shown when user runs `nordic-data <cmd> --help`.
127
+ const COMMAND_HELP = {
128
+ search: `${c('bold', 'nordic-data search')} ${c('dim', '<query> [--json]')}
129
+
130
+ Search Norwegian companies by name or organisation number.
131
+
132
+ ${c('bold', 'EXAMPLES')}
133
+ ${c('dim', '# Fuzzy name search')}
134
+ nordic-data search equinor
135
+
136
+ ${c('dim', '# JSON for scripting')}
137
+ nordic-data search "telenor" --json | jq '.results[0].orgnr'
138
+
139
+ ${c('dim', '# Lookup by orgnr also works as a single-result search')}
140
+ nordic-data search 923609016
141
+ `,
142
+ lookup: `${c('bold', 'nordic-data lookup')} ${c('dim', '<orgnr> [--json]')}
143
+
144
+ Full snapshot of one company. Includes identity, address, key personnel,
145
+ contacts, and a sanctions hit count.
146
+
147
+ ${c('bold', 'ORGNR FORMATS')}
148
+ 9 digits ${c('dim', '— Norway (e.g. 923609016)')}
149
+ 10 digits ${c('dim', '— Sweden (e.g. 5566370985 or 556637-0985) — auto-routes to Sweden command')}
150
+
151
+ ${c('bold', 'EXAMPLES')}
152
+ ${c('dim', '# Norway')}
153
+ nordic-data lookup 923609016
154
+
155
+ ${c('dim', '# Sweden (10-digit orgnr — auto-detected)')}
156
+ nordic-data lookup 5566370985
157
+
158
+ ${c('dim', '# Raw JSON')}
159
+ nordic-data lookup 923609016 --json | jq .identity.name
160
+ `,
161
+ contacts: `${c('bold', 'nordic-data contacts')} ${c('dim', '<orgnr> [--json]')}
162
+
163
+ Verified emails, phones, and named executives for a Norwegian company.
164
+ Cached 30 days. Empty when no public contact info is available.
165
+
166
+ ${c('bold', 'EXAMPLES')}
167
+ nordic-data contacts 923609016
168
+ nordic-data contacts 923609016 --json
169
+ `,
170
+ 'contacts-se': `${c('bold', 'nordic-data contacts-se')} ${c('dim', '<orgnr> [--json]')}
171
+
172
+ Sweden: identity + AI-enriched contacts (verified emails, phones, named executives).
173
+
174
+ ${c('bold', 'EXAMPLES')}
175
+ nordic-data contacts-se 5566370985
176
+ nordic-data contacts-se 556637-0985 ${c('dim', '# dash is accepted')}
177
+ `,
178
+ board: `${c('bold', 'nordic-data board')} ${c('dim', '<orgnr> [--json]')}
179
+
180
+ Current board + leadership for a Norwegian company. Shows role category
181
+ (styre / ledelse / other) and full role description.
182
+
183
+ ${c('bold', 'EXAMPLES')}
184
+ nordic-data board 923609016
185
+ `,
186
+ finances: `${c('bold', 'nordic-data finances')} ${c('dim', '<orgnr> [--json]')}
187
+
188
+ Latest annual accounts for a Norwegian company: revenue, operating profit,
189
+ net result, balance sheet totals, equity, and computed ratios.
190
+
191
+ ${c('bold', 'EXAMPLES')}
192
+ nordic-data finances 923609016
193
+ nordic-data finances 923609016 --json | jq .ratios
194
+ `,
195
+ procurement: `${c('bold', 'nordic-data procurement')} ${c('dim', '<orgnr> [--json]')}
196
+
197
+ Public-sector contract aggregates for a Norwegian company — count of
198
+ contracts won, estimated total value, top buyers.
199
+
200
+ ${c('bold', 'EXAMPLES')}
201
+ nordic-data procurement 923609016
202
+ `,
203
+ grants: `${c('bold', 'nordic-data grants')} ${c('dim', '<orgnr> [--json]')}
204
+
205
+ EU R&D grant participations for a Norwegian company. Returns each grant
206
+ with role (coordinator/participant), EU contribution, project budget.
207
+
208
+ ${c('bold', 'EXAMPLES')}
209
+ nordic-data grants 923609016
210
+ `,
211
+ sanctions: `${c('bold', 'nordic-data sanctions')} ${c('dim', '<orgnr> [--json]')}
212
+
213
+ Sanctions / AML / KYC check. Screens the company AND its officers against
214
+ international watchlists (OFAC SDN; EU + UN forthcoming). Auto-fetches
215
+ officers if not yet cached — one call gives you the full picture.
216
+
217
+ ${c('bold', 'EXAMPLES')}
218
+ nordic-data sanctions 923609016
219
+ `,
220
+ shareholders: `${c('bold', 'nordic-data shareholders')} ${c('dim', '<orgnr> [--json]')}
221
+
222
+ Shareholder cap table for a Norwegian AS — ownership %, share count, identity
223
+ of each holder.
224
+
225
+ ${c('bold', 'EXAMPLES')}
226
+ nordic-data shareholders 923609016
227
+ `,
228
+ mcp: `${c('bold', 'nordic-data mcp')}
229
+
230
+ Print the MCP server config snippet to drop into Claude Desktop / Cursor.
231
+ Set NORDIC_DATA_KEY first to embed your key in the snippet.
232
+
233
+ ${c('bold', 'EXAMPLES')}
234
+ ${c('dim', '# Print the snippet')}
235
+ nordic-data mcp
236
+
237
+ ${c('dim', '# Wire your key in the printed snippet')}
238
+ export NORDIC_DATA_KEY=nrd_live_...
239
+ nordic-data mcp
240
+ `,
241
+ signup: `${c('bold', 'nordic-data signup')}
242
+
243
+ Open the free-tier signup page in your browser. 500 requests/month, no card.
244
+ `,
245
+ // v0.4.0 — Discovery + KYB + AI/NL + US
246
+ find: `${c('bold', 'nordic-data find')} ${c('dim', '<query>')}
247
+ Universal resolver. Pass anything (name, orgnr, domain, email); get the best company match plus alternates.
248
+ nordic-data find Cognite
249
+ nordic-data find 918274758
250
+ nordic-data find cognite.com
251
+ nordic-data find ceo@equinor.com
252
+ `,
253
+ list: `${c('bold', 'nordic-data list')} ${c('dim', '<profession> [--city <city>] [--employees-min N] [--employees-max N] [--limit N]')}
254
+ Filter Norwegian companies by plain-text profession (auto-mapped to NACE) + city + size.
255
+ nordic-data list elektriker --city Stavanger --employees-min 5
256
+ nordic-data list tannlege --city Bergen
257
+ nordic-data list snekker --city Tromsø --limit 20
258
+ `,
259
+ nl: `${c('bold', 'nordic-data nl')} ${c('dim', '"<natural-language query>"')}
260
+ LLM parses your sentence into a filter spec and runs the search. Returns the parsed filter plus matching companies.
261
+ nordic-data nl "electricians in Stavanger with 5+ employees"
262
+ nordic-data nl "Norwegian fintech startups under 20 employees"
263
+ `,
264
+ kyb: `${c('bold', 'nordic-data kyb')} ${c('dim', '<orgnr>')}
265
+ One-call due-diligence bundle: identity + officers + sanctions (company + all officers) + bankruptcy + sister companies + recent changes + shell-likelihood signals.
266
+ nordic-data kyb 923609016
267
+ `,
268
+ explain: `${c('bold', 'nordic-data explain')} ${c('dim', '<orgnr>')}
269
+ AI-generated 3-sentence narrative: who the company is, who runs it, where they're going. Uses only structured registry data — no invented facts.
270
+ nordic-data explain 923609016
271
+ `,
272
+ ask: `${c('bold', 'nordic-data ask')} ${c('dim', '<orgnr> "<question>"')}
273
+ Q&A grounded in the company's registry + contact + contract data. Answers "Not available in the data provided" when the answer isn't in the data.
274
+ nordic-data ask 923609016 "who chairs the board"
275
+ nordic-data ask 923609016 "how many employees and where are they based"
276
+ `,
277
+ pitch: `${c('bold', 'nordic-data pitch')} ${c('dim', '<orgnr>')}
278
+ Sales talking points (4-5 numbered items) referencing concrete facts: recent contract wins, new execs, growth signals.
279
+ nordic-data pitch 923609016
280
+ `,
281
+ 'verify-invoice': `${c('bold', 'nordic-data verify-invoice')} ${c('dim', '<orgnr> <iban>')}
282
+ Invoice fraud heuristic via IBAN-country-mismatch check. If a supposed Norwegian supplier asks you to pay a German IBAN, that's a classic CEO-fraud pattern.
283
+ nordic-data verify-invoice 923609016 DE89370400440532013000
284
+ nordic-data verify-invoice 923609016 NO9386011117947
285
+ `,
286
+ resolve: `${c('bold', 'nordic-data resolve')} ${c('dim', '<type> <value>')}
287
+ Resolvers — anything → entity. Types: email, domain, phone, iban, vat, lei, address, person, wikipedia.
288
+ nordic-data resolve email ceo@equinor.com
289
+ nordic-data resolve domain cognite.com
290
+ nordic-data resolve phone "+47 519 90 000"
291
+ nordic-data resolve iban DE89370400440532013000
292
+ `,
293
+ near: `${c('bold', 'nordic-data near')} ${c('dim', '<lat> <lng> [--radius 5] [--profession <slug>] [--limit N]')}
294
+ Geo-radius search around a coordinate. Optional industry filter.
295
+ nordic-data near 58.97 5.73 --radius 5 --profession rorlegger
296
+ `,
297
+ similar: `${c('bold', 'nordic-data similar')} ${c('dim', '<orgnr>')}
298
+ Find up to 20 lookalike companies (same industry + country, prioritised by same-city and closest employee count).
299
+ nordic-data similar 918274758
300
+ `,
301
+ 'decision-makers': `${c('bold', 'nordic-data decision-makers')} ${c('dim', '<orgnr>')}
302
+ Synthesised list of key roles (CEO, CFO, chair, board) plus role-labelled named contacts.
303
+ nordic-data decision-makers 923609016
304
+ `,
305
+ trades: `${c('bold', 'nordic-data trades')} ${c('dim', '<slug> [--city <city>] [--limit N]')}
306
+ Vertical shortcut for common Norwegian trades. Slugs include: elektriker, rorlegger, snekker, tomrer, maler, murer, tannlege, lege, advokat, frisor, restaurant, kafe, etc.
307
+ nordic-data trades elektriker --city Oslo
308
+ `,
309
+ diff: `${c('bold', 'nordic-data diff')} ${c('dim', '<orgnr> [--since YYYY-MM-DD]')}
310
+ Typed changelog: board changes, ownership transfers, bankruptcy events etc. since a given date (default 30 days ago).
311
+ nordic-data diff 923609016 --since 2026-01-01
312
+ `,
313
+ 'us-search': `${c('bold', 'nordic-data us-search')} ${c('dim', '<query>')}
314
+ Search ~10,000 US public companies (SEC EDGAR) by name or ticker.
315
+ nordic-data us-search Microsoft
316
+ nordic-data us-search AAPL
317
+ `,
318
+ 'us-lookup': `${c('bold', 'nordic-data us-lookup')} ${c('dim', '<query>')}
319
+ Resolve a US public company by CIK, ticker, or name. Returns SEC identity (SIC, state, city, address).
320
+ nordic-data us-lookup Tesla
321
+ nordic-data us-lookup 0000789019
322
+ `,
323
+ 'us-filings': `${c('bold', 'nordic-data us-filings')} ${c('dim', '<cik> [--type 10-K]')}
324
+ Recent SEC filings for a US public company with direct EDGAR URLs.
325
+ nordic-data us-filings 0000320193 --type 10-K
326
+ `,
327
+ 'us-kyb': `${c('bold', 'nordic-data us-kyb')} ${c('dim', '<cik>')}
328
+ US KYB pack: SEC identity + recent filings + sanctions screen + recent federal contracts (USA Spending).
329
+ nordic-data us-kyb 0000789019
330
+ `,
331
+ };
332
+
75
333
  const args = process.argv.slice(2);
76
334
  let useJson = false;
77
335
  let useColor = isTTY;
336
+ let wantsHelp = false;
78
337
  const cleanArgs = [];
79
338
  for (let i = 0; i < args.length; i++) {
80
339
  const a = args[i];
81
340
  if (a === '--json') useJson = true;
82
341
  else if (a === '--no-color') useColor = false;
83
342
  else if (a === '--key') { process.env.NORDIC_DATA_KEY = args[++i] || ''; }
84
- else if (a === '--help' || a === '-h') { console.log(HELP); process.exit(0); }
343
+ else if (a === '--help' || a === '-h') wantsHelp = true;
85
344
  else if (a === '--version' || a === '-v') {
86
345
  const { version } = require('../package.json');
87
346
  console.log(version);
88
347
  process.exit(0);
89
348
  } else cleanArgs.push(a);
90
349
  }
350
+
351
+ // Resolve per-command help: `nordic-data <cmd> --help` shows command-specific help.
352
+ // `nordic-data --help` (no command) shows the global help.
353
+ if (wantsHelp) {
354
+ if (cleanArgs.length > 0 && COMMAND_HELP[cleanArgs[0]]) {
355
+ console.log(COMMAND_HELP[cleanArgs[0]]);
356
+ } else {
357
+ console.log(HELP);
358
+ }
359
+ process.exit(0);
360
+ }
91
361
  if (cleanArgs.length === 0) { console.log(HELP); process.exit(0); }
92
362
 
93
363
  const [cmd, ...rest] = cleanArgs;
@@ -104,10 +374,26 @@ async function publicRequest(path) {
104
374
 
105
375
  const res = await fetch(`${API_BASE}${path}`, { headers });
106
376
  if (!res.ok) {
107
- if (res.status === 429) die('Rate limited. Free tier is 4 lookups per IP per 24h. Set NORDIC_DATA_KEY=... for higher limits.');
108
- if (res.status === 401) die('Auth required. Set NORDIC_DATA_KEY=... or use --key. Sign up at https://nordicdata.cloud/?signup=free');
109
- if (res.status === 404) die('Not found.');
110
- die(`API error ${res.status}: ${await res.text()}`);
377
+ if (res.status === 429) die('Rate limited. Free tier is 4 lookups per IP per 24h. Set NORDIC_DATA_KEY=... for higher limits.\n Sign up at https://nordicdata.cloud/?signup=free (500 requests/month, no card).');
378
+ if (res.status === 401) die('Auth required. Set NORDIC_DATA_KEY=... or pass --key <key>.\n Sign up at https://nordicdata.cloud/?signup=free (500 requests/month, no card).');
379
+ // Try to parse a structured JSON error and surface a useful message + hint.
380
+ let body = null;
381
+ try { body = await res.json(); } catch {}
382
+ const errCode = body && body.error;
383
+ const errMsg = body && body.message;
384
+ if (res.status === 404) {
385
+ if (errCode === 'not_found' && body.orgnr) {
386
+ die(`Company ${body.orgnr} not found in the official register.\n ${c('dim', 'If you do not know the orgnr, try:')} nordic-data search <name>`);
387
+ }
388
+ die(`Not found.\n ${c('dim', 'Tip:')} nordic-data search <name> ${c('dim', 'to find the orgnr first.')}`);
389
+ }
390
+ if (res.status === 400 && errCode === 'invalid_orgnr') {
391
+ die(`Invalid Norwegian organisation number — must be 9 digits.\n ${c('dim', 'Tip:')} nordic-data search <name> ${c('dim', 'to find the orgnr.')}`);
392
+ }
393
+ if (res.status === 400 && errMsg) die(errMsg);
394
+ if (res.status === 402) die(`Payment required. ${errMsg || 'Plan does not include this endpoint.'}\n See plans: https://nordicdata.cloud/#pricing`);
395
+ if (res.status >= 500) die(`Server error (${res.status}). Try again in a moment. If it persists, email support@nordicdata.cloud.`);
396
+ die(`API error ${res.status}: ${errMsg || (body && JSON.stringify(body)) || 'unknown error'}`);
111
397
  }
112
398
  return res.json();
113
399
  }
@@ -153,7 +439,16 @@ async function search(query) {
153
439
  }
154
440
 
155
441
  async function lookup(orgnr) {
156
- if (!orgnr) die('Usage: nordic-data lookup <orgnr>');
442
+ if (!orgnr) die('Usage: nordic-data lookup <orgnr>\n Norway: 9-digit orgnr. Sweden: 10-digit orgnr (with or without dash).');
443
+ // Auto-route Swedish orgnrs (10 digits, with or without dash) to the SE flow.
444
+ const cleaned = String(orgnr).replace(/[-\s]/g, '');
445
+ if (/^\d{10}$/.test(cleaned)) {
446
+ console.log(c('dim', `(10-digit orgnr detected — routing to Sweden command)\n`));
447
+ return contactsSe(orgnr);
448
+ }
449
+ if (!/^\d{9}$/.test(cleaned)) {
450
+ die(`Invalid orgnr "${orgnr}".\n Norway uses 9 digits (e.g. 923609016).\n Sweden uses 10 digits (e.g. 5566370985 or 556637-0985).\n ${c('dim', 'Tip:')} nordic-data search <name> ${c('dim', 'to find it.')}`);
451
+ }
157
452
  const snap = await publicRequest(`/_/look/${orgnr}`);
158
453
  if (useJson) return console.log(JSON.stringify(snap, null, 2));
159
454
 
@@ -384,6 +679,296 @@ function fmt(n, currency) {
384
679
  return `${cur} ${n.toLocaleString('nb-NO')}`;
385
680
  }
386
681
 
682
+ // ── v0.4.0 — Discovery + KYB + AI/NL + US ─────────────────
683
+ // Helper to parse --flag value args from a sub-array.
684
+ function flag(arr, name, def) {
685
+ const i = arr.indexOf(`--${name}`);
686
+ if (i < 0 || i === arr.length - 1) return def;
687
+ return arr[i + 1];
688
+ }
689
+ function bool(arr, name) { return arr.includes(`--${name}`); }
690
+
691
+ // Find: universal lookup (resolves name / orgnr / domain / email)
692
+ async function find(q) {
693
+ if (!q) die('Usage: nordic-data find <query>\n Pass anything — name, orgnr, domain, or email.');
694
+ const data = await publicRequest(`/companies/lookup?q=${encodeURIComponent(q)}`);
695
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
696
+ if (!data.resolved) {
697
+ console.log(c('dim', `No match for "${q}".`));
698
+ if (data.note) console.log(c('dim', ' ' + data.note));
699
+ return;
700
+ }
701
+ const r = data.resolved;
702
+ header(`${r.name || 'Match'}${r.orgnr ? c('dim', ` (${r.orgnr})`) : ''}`);
703
+ pretty('Resolved by', data.resolved_by);
704
+ pretty('Country', r.country || (r.business_address && r.business_address.country_code) || '');
705
+ pretty('Status', r.status || '');
706
+ pretty('City', (r.business_address && r.business_address.city) || r.city || '');
707
+ if (r.nace && r.nace[0]) pretty('NACE', `${r.nace[0].code} ${r.nace[0].description || ''}`);
708
+ pretty('Employees', r.employees);
709
+ pretty('Website', r.website);
710
+ if (data.alternates && data.alternates.length) {
711
+ console.log(c('dim', `\n ${data.alternates.length} alternate match(es):`));
712
+ for (const a of data.alternates.slice(0, 5)) console.log(` ${c('dim', a.orgnr || '')} ${a.name}`);
713
+ }
714
+ }
715
+
716
+ // List: filter Norwegian companies by industry × city × size
717
+ async function list(args) {
718
+ const profession = args[0];
719
+ if (!profession) die('Usage: nordic-data list <profession> [--city <city>] [--employees-min N] [--employees-max N] [--limit N]\n Examples:\n nordic-data list elektriker --city Stavanger --employees-min 5\n nordic-data list tannlege --city Bergen\n nordic-data list snekker --city Tromsø');
720
+ const params = new URLSearchParams({ profession });
721
+ if (flag(args, 'city')) params.set('city', flag(args, 'city'));
722
+ if (flag(args, 'employees-min')) params.set('min_employees', flag(args, 'employees-min'));
723
+ if (flag(args, 'employees-max')) params.set('max_employees', flag(args, 'employees-max'));
724
+ if (flag(args, 'limit')) params.set('limit', flag(args, 'limit'));
725
+ if (bool(args, 'active')) params.set('active', 'true');
726
+ const data = await publicRequest(`/companies/no/list?${params}`);
727
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
728
+ header(`${data.count} ${profession}${data.filter && data.filter.city ? ` in ${data.filter.city}` : ''}`);
729
+ for (const r of (data.results || []).slice(0, 50)) {
730
+ const city = (r.business_address && r.business_address.city) || '';
731
+ const emp = r.employees != null ? c('dim', ` · ${r.employees} emp`) : '';
732
+ console.log(` ${c('orange', r.orgnr)} ${r.name}${city ? c('dim', ` · ${city}`) : ''}${emp}`);
733
+ }
734
+ }
735
+
736
+ // NL: natural-language discovery
737
+ async function nl(query) {
738
+ if (!query) die('Usage: nordic-data nl "<natural language query>"\n Examples:\n nordic-data nl "electricians in Stavanger with 5+ employees"\n nordic-data nl "dentists in Bergen"');
739
+ const data = await publicRequest(`/companies/nl-search?q=${encodeURIComponent(query)}`);
740
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
741
+ if (data.filter_spec) {
742
+ console.log(c('dim', `Interpreted as: ${JSON.stringify(data.filter_spec)}`));
743
+ }
744
+ header(`${data.count} match(es)`);
745
+ for (const r of (data.results || []).slice(0, 50)) {
746
+ const city = (r.business_address && r.business_address.city) || '';
747
+ const emp = r.employees != null ? c('dim', ` · ${r.employees} emp`) : '';
748
+ console.log(` ${c('orange', r.orgnr)} ${r.name}${city ? c('dim', ` · ${city}`) : ''}${emp}`);
749
+ }
750
+ }
751
+
752
+ // KYB: one-call due-diligence bundle
753
+ async function kyb(orgnr) {
754
+ if (!orgnr) die('Usage: nordic-data kyb <orgnr>\n Returns: identity + officers + sanctions on company AND officers + sister companies + recent changes + shell-likelihood.');
755
+ const data = await publicRequest(`/companies/${orgnr}/kyb-pack`);
756
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
757
+ const i = data.identity || {};
758
+ header(`KYB pack — ${i.name || orgnr}`);
759
+ pretty('Orgnr', data.orgnr);
760
+ pretty('Status', i.status);
761
+ pretty('Employees', i.employees);
762
+ pretty('Generated', data.generated_at);
763
+ if (data.officers && data.officers.all) {
764
+ console.log(c('bold', `\n Officers (${data.officers.all.length}):`));
765
+ for (const o of data.officers.all.slice(0, 10)) {
766
+ console.log(` ${c('orange', (o.role || '').padEnd(20))} ${c('bold', (o.person && o.person.name) || o.name || '')}`);
767
+ }
768
+ }
769
+ const sa = data.sanctions || {};
770
+ console.log(c('bold', '\n Sanctions:'));
771
+ console.log(` Company matches: ${(sa.company_matches || []).length}`);
772
+ const om = (sa.officer_matches || []).filter(x => (x.matches || []).length).length;
773
+ console.log(` Officers with hits: ${om}`);
774
+ if (data.sister_companies && data.sister_companies.length) pretty('Sister cos', data.sister_companies.length);
775
+ if (data.recent_changes && data.recent_changes.length) pretty('Recent changes', data.recent_changes.length);
776
+ const sh = data.shell_signals || {};
777
+ const shell = [sh.no_employees && 'no employees', sh.fresh && 'fresh<1y', sh.no_website && 'no website'].filter(Boolean).join(', ');
778
+ if (shell) pretty('Shell signals', c('yellow', shell));
779
+ }
780
+
781
+ // Explain: AI 3-sentence narrative
782
+ async function explain(orgnr) {
783
+ if (!orgnr) die('Usage: nordic-data explain <orgnr>');
784
+ const data = await publicRequest(`/companies/${orgnr}/explain`);
785
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
786
+ header(`Narrative for ${orgnr}`);
787
+ console.log(` ${data.narrative || c('dim', '(no narrative)')}`);
788
+ }
789
+
790
+ // Ask: Q&A about a company
791
+ async function ask(args) {
792
+ const orgnr = args[0];
793
+ const question = args.slice(1).join(' ');
794
+ if (!orgnr || !question) die('Usage: nordic-data ask <orgnr> <question>\n Example: nordic-data ask 923609016 "who chairs the board"');
795
+ const data = await publicRequest(`/companies/${orgnr}/q?question=${encodeURIComponent(question)}`);
796
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
797
+ header(`Q: ${question}`);
798
+ console.log(` ${data.answer || c('dim', '(no answer)')}`);
799
+ }
800
+
801
+ // Pitch: sales talking points
802
+ async function pitch(orgnr) {
803
+ if (!orgnr) die('Usage: nordic-data pitch <orgnr>\n Returns 4-5 sales talking points referencing concrete facts.');
804
+ const data = await publicRequest(`/companies/${orgnr}/talking-points`);
805
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
806
+ header(`Sales talking points for ${orgnr}`);
807
+ console.log(data.talking_points || c('dim', '(none)'));
808
+ }
809
+
810
+ // Verify invoice
811
+ async function verifyInvoice(args) {
812
+ const orgnr = args[0];
813
+ const iban = args[1];
814
+ if (!orgnr || !iban) die('Usage: nordic-data verify-invoice <orgnr> <iban>\n Flags fraud risk if IBAN country mismatches the company’s registered country.');
815
+ const data = await publicRequest(`/verify/invoice?orgnr=${encodeURIComponent(orgnr)}&iban=${encodeURIComponent(iban)}`);
816
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
817
+ header(`Invoice check — orgnr ${orgnr} → IBAN ${iban}`);
818
+ const risk = data.risk === 'high' ? c('red', 'HIGH') : data.risk === 'low' ? c('green', 'LOW') : data.risk;
819
+ pretty('Risk', risk);
820
+ pretty('IBAN country', data.iban_country);
821
+ pretty('Company country', data.company_country);
822
+ console.log('\n ' + c('dim', data.reason || ''));
823
+ }
824
+
825
+ // Universal /resolve/* — type one of: email, domain, phone, iban, vat, lei, address, person, wikipedia
826
+ async function resolve(args) {
827
+ const type = args[0];
828
+ const value = args[1];
829
+ const validTypes = ['email','domain','phone','iban','vat','lei','address','person','wikipedia'];
830
+ if (!type || !validTypes.includes(type)) die(`Usage: nordic-data resolve <type> <value>\n Type: ${validTypes.join(' | ')}\n Examples:\n nordic-data resolve email ceo@equinor.com\n nordic-data resolve domain cognite.com\n nordic-data resolve phone "+47 519 90 000"\n nordic-data resolve iban DE89370400440532013000`);
831
+ if (!value) die(`Usage: nordic-data resolve ${type} <value>`);
832
+ const param = type === 'email' ? 'address' : type === 'iban' ? 'iban' : type === 'vat' ? 'vat' : type === 'lei' ? 'lei' : type === 'wikipedia' ? 'title' : type === 'address' ? 'street' : type === 'person' ? 'name' : type === 'phone' ? 'number' : 'domain';
833
+ const data = await publicRequest(`/resolve/${type}?${param}=${encodeURIComponent(value)}`);
834
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
835
+ console.log(JSON.stringify(data, null, 2));
836
+ }
837
+
838
+ // Near: geo-radius search
839
+ async function near(args) {
840
+ const lat = args[0];
841
+ const lng = args[1];
842
+ if (!lat || !lng) die('Usage: nordic-data near <lat> <lng> [--radius 5] [--profession <profession>] [--limit N]');
843
+ const params = new URLSearchParams({ lat, lng });
844
+ params.set('radius_km', flag(args, 'radius', '5'));
845
+ if (flag(args, 'profession')) params.set('profession', flag(args, 'profession'));
846
+ if (flag(args, 'limit')) params.set('limit', flag(args, 'limit'));
847
+ const data = await publicRequest(`/companies/near?${params}`);
848
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
849
+ header(`${data.count} companies within ${params.get('radius_km')} km of ${lat},${lng}`);
850
+ for (const r of (data.results || []).slice(0, 50)) {
851
+ console.log(` ${c('orange', r.orgnr)} ${r.name}${c('dim', ` · ${r.distance_km} km`)}`);
852
+ }
853
+ }
854
+
855
+ // Similar companies
856
+ async function similar(orgnr) {
857
+ if (!orgnr) die('Usage: nordic-data similar <orgnr>\n Returns 20 lookalike companies based on industry + geo + size.');
858
+ const data = await publicRequest(`/companies/similar/${orgnr}?limit=20`);
859
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
860
+ header(`Similar to ${orgnr}`);
861
+ for (const r of (data.similar || []).slice(0, 30)) {
862
+ console.log(` ${c('orange', r.orgnr)} ${r.name}${r.city ? c('dim', ` · ${r.city}`) : ''}${r.employees ? c('dim', ` · ${r.employees} emp`) : ''}`);
863
+ }
864
+ }
865
+
866
+ // Decision makers
867
+ async function decisionMakers(orgnr) {
868
+ if (!orgnr) die('Usage: nordic-data decision-makers <orgnr>');
869
+ const data = await publicRequest(`/companies/${orgnr}/decision-makers`);
870
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
871
+ header(`Decision-makers for ${orgnr}`);
872
+ for (const o of (data.key_officers || [])) {
873
+ console.log(` ${c('orange', (o.role || '').padEnd(24))} ${c('bold', (o.person && o.person.name) || '')}`);
874
+ }
875
+ if ((data.named_contacts || []).length) {
876
+ console.log(c('dim', '\n Named contacts:'));
877
+ for (const n of data.named_contacts) {
878
+ console.log(` ${c('orange', n.role)} ${c('bold', n.name)}${n.email ? c('dim', ` · ${n.email}`) : ''}`);
879
+ }
880
+ }
881
+ }
882
+
883
+ // Trades vertical shortcut
884
+ async function trades(args) {
885
+ const slug = args[0];
886
+ if (!slug) die('Usage: nordic-data trades <slug> [--city <city>]\n Examples: nordic-data trades elektriker --city Oslo\n nordic-data trades rorlegger --city Stavanger\n nordic-data trades snekker --city Tromsø');
887
+ const params = new URLSearchParams();
888
+ if (flag(args, 'city')) params.set('city', flag(args, 'city'));
889
+ if (flag(args, 'limit')) params.set('limit', flag(args, 'limit'));
890
+ const data = await publicRequest(`/trades/${slug}/directory?${params}`);
891
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
892
+ header(`${data.count} ${slug}${flag(args, 'city') ? ` in ${flag(args, 'city')}` : ''}`);
893
+ for (const r of (data.results || []).slice(0, 50)) {
894
+ const city = (r.business_address && r.business_address.city) || '';
895
+ console.log(` ${c('orange', r.orgnr)} ${r.name}${city ? c('dim', ` · ${city}`) : ''}`);
896
+ }
897
+ }
898
+
899
+ // Diff: typed changelog
900
+ async function diff(args) {
901
+ const orgnr = args[0];
902
+ if (!orgnr) die('Usage: nordic-data diff <orgnr> [--since YYYY-MM-DD]');
903
+ const params = new URLSearchParams();
904
+ if (flag(args, 'since')) params.set('since', flag(args, 'since'));
905
+ const data = await publicRequest(`/companies/${orgnr}/diff?${params}`);
906
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
907
+ header(`Changes for ${orgnr} since ${data.since}`);
908
+ for (const ch of (data.changes || []).slice(0, 50)) {
909
+ console.log(` ${c('orange', ch.change_date || '')} ${ch.change_type || ''}`);
910
+ }
911
+ if (!data.changes || !data.changes.length) console.log(c('dim', ' (no changes)'));
912
+ }
913
+
914
+ // US: search public companies
915
+ async function usSearch(query) {
916
+ if (!query) die('Usage: nordic-data us-search <query>');
917
+ const data = await publicRequest(`/companies/us/search?q=${encodeURIComponent(query)}`);
918
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
919
+ header(`${data.count} US public company matches for "${query}"`);
920
+ for (const r of (data.results || []).slice(0, 25)) {
921
+ console.log(` ${c('orange', r.cik)} ${r.name}${r.ticker ? c('dim', ` · ${r.ticker}`) : ''}`);
922
+ }
923
+ }
924
+
925
+ async function usLookup(query) {
926
+ if (!query) die('Usage: nordic-data us-lookup <query>\n Resolves a US public company by CIK, ticker, or name.');
927
+ const data = await publicRequest(`/companies/us/lookup?q=${encodeURIComponent(query)}`);
928
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
929
+ if (!data.resolved) return console.log(c('dim', data.note || 'No match.'));
930
+ const r = data.resolved;
931
+ header(`${r.name} ${c('dim', `(CIK ${r.cik}${r.tickers && r.tickers[0] ? ' · ' + r.tickers[0] : ''})`)}`);
932
+ pretty('SIC', `${r.sic || ''} ${r.sic_description || ''}`.trim());
933
+ pretty('State', r.state);
934
+ pretty('City', r.city);
935
+ pretty('Address', r.address);
936
+ }
937
+
938
+ async function usFilings(args) {
939
+ const cik = args[0];
940
+ if (!cik) die('Usage: nordic-data us-filings <cik> [--type 10-K]');
941
+ const params = new URLSearchParams();
942
+ if (flag(args, 'type')) params.set('type', flag(args, 'type'));
943
+ const data = await publicRequest(`/companies/us/${cik}/filings?${params}`);
944
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
945
+ header(`${data.company} — ${data.count} filing(s)`);
946
+ for (const f of (data.filings || []).slice(0, 25)) {
947
+ console.log(` ${c('orange', f.date.padEnd(11))} ${c('bold', f.form.padEnd(8))} ${c('dim', f.url)}`);
948
+ }
949
+ }
950
+
951
+ async function usKyb(cik) {
952
+ if (!cik) die('Usage: nordic-data us-kyb <cik>');
953
+ const data = await publicRequest(`/companies/us/${cik}/kyb-pack`);
954
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
955
+ const i = data.identity || {};
956
+ header(`US KYB pack — ${i.name || cik}`);
957
+ pretty('CIK', i.cik);
958
+ pretty('SIC', `${i.sic || ''} ${i.sic_description || ''}`.trim());
959
+ pretty('State', i.state);
960
+ pretty('City', i.city);
961
+ if (data.recent_filings && data.recent_filings.length) {
962
+ console.log(c('bold', '\n Recent filings:'));
963
+ for (const f of data.recent_filings) console.log(` ${c('orange', f.date)} ${f.form}`);
964
+ }
965
+ const sa = data.sanctions || {};
966
+ pretty('Sanctions matches', (sa.company_matches || []).length);
967
+ if (data.recent_federal_contracts && data.recent_federal_contracts.length) {
968
+ pretty('Recent federal contracts', data.recent_federal_contracts.length);
969
+ }
970
+ }
971
+
387
972
  // ── Dispatch ───────────────────────────────────────────────
388
973
 
389
974
  (async () => {
@@ -398,6 +983,28 @@ function fmt(n, currency) {
398
983
  case 'grants': await grants(rest[0]); break;
399
984
  case 'sanctions': await sanctions(rest[0]); break;
400
985
  case 'shareholders': await shareholders(rest[0]); break;
986
+ case 'contacts-se': await contactsSe(rest[0]); break;
987
+ // v0.4.0 — Discovery, KYB, AI/NL, US
988
+ case 'find': await find(rest.join(' ')); break;
989
+ case 'list': await list(rest); break;
990
+ case 'nl': await nl(rest.join(' ')); break;
991
+ case 'kyb': await kyb(rest[0]); break;
992
+ case 'explain': await explain(rest[0]); break;
993
+ case 'ask': await ask(rest); break;
994
+ case 'pitch': await pitch(rest[0]); break;
995
+ case 'talking-points': await pitch(rest[0]); break;
996
+ case 'verify-invoice': await verifyInvoice(rest); break;
997
+ case 'resolve': await resolve(rest); break;
998
+ case 'near': await near(rest); break;
999
+ case 'similar': await similar(rest[0]); break;
1000
+ case 'decision-makers': await decisionMakers(rest[0]); break;
1001
+ case 'trades': await trades(rest); break;
1002
+ case 'diff': await diff(rest); break;
1003
+ case 'us-search': await usSearch(rest.join(' ')); break;
1004
+ case 'us-lookup': await usLookup(rest.join(' ')); break;
1005
+ case 'us-filings': await usFilings(rest); break;
1006
+ case 'us-kyb': await usKyb(rest[0]); break;
1007
+ // Meta
401
1008
  case 'mcp': mcp(); break;
402
1009
  case 'signup': signup(); break;
403
1010
  default:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nordic-data",
3
- "version": "0.2.0",
4
- "description": "CLI for Nordic Data \u2014 every Norwegian and Swedish company as one API. Look up companies, board members, owners, sanctions, public procurement, R&D grants, AI-enriched contacts (NO + SE), and financial summaries from your terminal.",
3
+ "version": "0.4.0",
4
+ "description": "CLI for Nordic Data \u2014 Norwegian, Swedish, and US business intelligence in one terminal. Discovery (filter by industry \u00d7 city \u00d7 size, natural-language search, geo-radius, similar-companies, trades verticals), universal resolvers (email / domain / phone / IBAN / VAT / LEI \u2192 company), KYB / compliance (one-call due-diligence pack, typed changelog, invoice-fraud heuristic), AI / NL (explain, Q&A, sales talking-points, risk narrative), United States coverage (SEC EDGAR + USA Spending federal contracts), plus the original Nordic core: identity, accounts, officers, sanctions, public procurement, R&D grants, shareholders, AI-enriched contacts.",
5
5
  "bin": {
6
6
  "nordic-data": "./bin/cli.js"
7
7
  },
@@ -16,14 +16,31 @@
16
16
  "sweden",
17
17
  "swedish",
18
18
  "nordic",
19
+ "united-states",
20
+ "us",
21
+ "sec-edgar",
19
22
  "company",
20
23
  "data",
21
24
  "cli",
22
25
  "kyb",
26
+ "kyc",
23
27
  "aml",
24
28
  "sanctions",
29
+ "ofac",
25
30
  "procurement",
31
+ "tenders",
32
+ "lookup",
33
+ "resolver",
34
+ "enrichment",
35
+ "discovery",
36
+ "natural-language",
26
37
  "mcp",
38
+ "model-context-protocol",
39
+ "agent",
40
+ "ai",
41
+ "fraud",
42
+ "invoice-fraud",
43
+ "due-diligence",
27
44
  "nordic-data"
28
45
  ],
29
46
  "homepage": "https://nordicdata.cloud",