nordic-data 0.1.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 +130 -115
  2. package/bin/cli.js +655 -17
  3. package/package.json +24 -7
package/README.md CHANGED
@@ -1,160 +1,175 @@
1
- # nordic-data
1
+ # Nordic Data
2
2
 
3
- Every Norwegian company as one API, from your terminal.
3
+ > Norwegian, Swedish, and US business intelligence in one terminal plus a hosted MCP server for AI agents.
4
4
 
5
- ```sh
6
- npx nordic-data search equinor
7
- npx nordic-data lookup 923609016
8
- npx nordic-data contacts 923609016
9
- npx nordic-data finances 923609016
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
10
12
  ```
11
13
 
14
+ **Get a free key:** [nordicdata.cloud](https://nordicdata.cloud) — 500 requests/month, no card
15
+ **Docs:** [nordicdata.cloud/docs](https://nordicdata.cloud/docs)
16
+
17
+ ---
18
+
12
19
  ## Install
13
20
 
14
- ```sh
21
+ ```bash
22
+ npx nordic-data --help # zero install
23
+ # or
15
24
  npm install -g nordic-data
16
- # or just use npx
17
- npx nordic-data --help
25
+ nordic-data --help
18
26
  ```
19
27
 
20
- ## Free tier
28
+ ## Configure your API key
21
29
 
22
- 5,000 requests per month with an API key. Without a key, the CLI uses the public widget tier (4 full snapshots per IP per 24 hours). Get a key:
23
-
24
- ```sh
25
- npx nordic-data signup
26
- # or visit https://nordicdata.cloud/?signup=free
30
+ ```bash
31
+ export NORDIC_DATA_KEY=nrd_live_… # or pass --key on each call
27
32
  ```
28
33
 
29
- Set the key as an environment variable:
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.
30
35
 
31
- ```sh
32
- export NORDIC_DATA_KEY=nrd_live_...
33
- ```
34
-
35
- You can also pass `--key <key>` per invocation.
36
+ ---
36
37
 
37
38
  ## Commands
38
39
 
40
+ ### Core lookup (Norway + Sweden)
39
41
  | Command | What it does |
40
42
  |---|---|
41
- | `search <query>` | Search Norwegian companies by name or fragment |
42
- | `lookup <orgnr>` | Full snapshot for one organisation number (registry + officers + finances + sanctions + contacts) |
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) |
43
45
  | `contacts <orgnr>` | Emails, phones, and named executives |
44
- | `board <orgnr>` | Current board + leadership |
45
- | `finances <orgnr>` | Latest financial summary (revenue, operating profit, equity, ratios) |
46
- | `procurement <orgnr>` | Doffin public-sector contract aggregates |
47
- | `grants <orgnr>` | EU R&D grants (Horizon Europe, EIC) |
48
- | `sanctions <orgnr>` | Sanctions screening (EU, UN, OFAC) hits |
49
- | `shareholders <orgnr>` | Aksjonærregisteret aggregates |
50
- | `mcp` | Show MCP setup snippet for Claude Desktop or Cursor |
51
- | `signup` | Open the free-tier signup in your browser |
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 |
52
83
 
53
- ## Examples
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 |
54
91
 
55
- Find a company:
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
56
96
 
57
- ```sh
58
- $ npx nordic-data search equinor
59
- 8 result(s) for "equinor"
60
- 923609016 EQUINOR ASA · STAVANGER
61
- 959733600 EQUINOR PENSJON · STAVANGER
62
- ...
63
- ```
97
+ ---
64
98
 
65
- Get the full picture (verified live against api.nordicdata.cloud):
66
-
67
- ```sh
68
- $ npx nordic-data lookup 923609016
69
- EQUINOR ASA (923609016)
70
- Status active
71
- Founded 1995-03-12
72
- Legal form ASA (Allmennaksjeselskap)
73
- NACE 06.100 Utvinning av råolje
74
- Address Forusbeen 50
75
- City 4035 STAVANGER
76
- Employees 21376
77
- VAT reg. yes
78
- Website www.equinor.com
79
- Phone +47 406 37 334
80
- Email apost@equinor.com
81
-
82
- Key personnel
83
- Chief Executive Officer Anders Opedal
84
- Chief Financial Officer Torgrim Reitan
85
- Chairman of the Board Jon Erik Reinhardsen
86
- ...
87
- ```
99
+ ## MCP server (for AI agents)
88
100
 
89
- Financials:
90
-
91
- ```sh
92
- $ npx nordic-data finances 923609016
93
- Financials (FY2024) for EQUINOR ASA
94
- Revenue USD 72.54B
95
- Operating profit USD 10.35B
96
- Net result USD 8.14B
97
- Total assets USD 109.15B
98
- Equity USD 41.09B
99
- Equity ratio 37.6%
100
- Net margin 11.2%
101
- ```
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.
102
102
 
103
- JSON output for scripting:
103
+ ### Hosted endpoint
104
104
 
105
- ```sh
106
- $ npx nordic-data lookup 923609016 --json | jq .identity.name
107
- "EQUINOR ASA"
105
+ ```
106
+ URL: https://api.nordicdata.cloud/mcp
107
+ Header: X-API-Key: <your key>
108
+ Transport: Streamable HTTP
108
109
  ```
109
110
 
110
- Sanctions screening:
111
+ ### Claude Desktop / Cursor config
111
112
 
112
- ```sh
113
- $ npx nordic-data sanctions 923609016
114
- Sanctions screening for EQUINOR ASA
115
- ● Officer hits: 1 (of 15 screened)
113
+ Edit `claude_desktop_config.json` (or `~/.cursor/mcp.json`):
114
+
115
+ ```json
116
+ {
117
+ "mcpServers": {
118
+ "nordic-data": {
119
+ "url": "https://api.nordicdata.cloud/mcp",
120
+ "headers": { "X-API-Key": "YOUR_API_KEY_HERE" }
121
+ }
122
+ }
123
+ }
116
124
  ```
117
125
 
118
- ## MCP setup for Claude Desktop / Cursor
126
+ Restart the client all 69 tools appear in the MCP picker.
119
127
 
120
- ```sh
121
- $ npx nordic-data mcp
122
- ```
128
+ ### Smithery one-click install
123
129
 
124
- prints the JSON snippet to drop into your MCP client config. Or visit our listings:
130
+ [smithery.ai/server/sofia-jameson-20/Nordic-Data](https://smithery.ai/server/sofia-jameson-20/Nordic-Data)
125
131
 
126
- - [Smithery](https://smithery.ai/servers/sofia-jameson-20/Nordic-Data)
127
- - [mcp.so](https://mcp.so/server/nordic-data)
128
- - [PulseMCP](https://www.pulsemcp.com/servers/nordic-data)
132
+ ---
129
133
 
130
- ## What data is in Nordic Data?
134
+ ## What's covered
131
135
 
132
- Every Norwegian company joined on the organisation number:
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
133
147
 
134
- - **Brønnøysundregistrene** name, address, NACE, status, board, signatories (real-time, < 5 min lag)
135
- - **Aksjonærregisteret** — shareholders with recursive UBO chain (annual snapshot)
136
- - **Doffin** — public-sector procurement filings (live)
137
- - **EU R&D grants** — Horizon Europe, EIC, joined to Norwegian recipients
138
- - **Sanctions** — EU, UN, OFAC, screened by org and by officer
139
- - **Enriched contacts** — 4-layer pipeline lifts contact-fill rate from 23% to 81% on the top 5,000 companies. [How it works](https://nordicdata.cloud/blog/four-layer-contact-enrichment).
140
- - **Financial summaries** — revenue, operating profit, equity, ratios — last 5 reported years
148
+ ## Example prompts (via the MCP server in Claude/Cursor)
141
149
 
142
- ## Pricing
150
+ - *"Find Norwegian electricians in Stavanger with 5+ employees and verified contact emails."*
151
+ - *"Pull the latest accounts and shareholders for orgnr 923609016."*
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."*
143
158
 
144
- Free tier: 5,000 requests / month. Paid tiers from €29/mo (25,000 req) to €499/mo (500,000 req). [Full pricing](https://nordicdata.cloud/#pricing).
159
+ ## Pricing
145
160
 
146
- ## Comparison vs other vendors
161
+ - **Free** 500 requests/month, no card required
162
+ - **Paid plans** from €49/mo
147
163
 
148
- We publish an honest benchmark page comparing Nordic Data against OpenCorporates, BvD/Orbis, Bisnode, Proff, Strise, and Sumsub: [https://nordicdata.cloud/coverage](https://nordicdata.cloud/coverage).
164
+ See [nordicdata.cloud](https://nordicdata.cloud).
149
165
 
150
- ## License
166
+ ## Support
151
167
 
152
- MIT see LICENSE.
168
+ - Email: [hello@nordicdata.cloud](mailto:hello@nordicdata.cloud)
169
+ - Docs: [nordicdata.cloud/docs](https://nordicdata.cloud/docs)
170
+ - LLM-friendly site index: [nordicdata.cloud/llms.txt](https://nordicdata.cloud/llms.txt)
171
+ - OpenAPI spec: [api.nordicdata.cloud/openapi.json](https://api.nordicdata.cloud/openapi.json)
153
172
 
154
- ## Links
173
+ ## License
155
174
 
156
- - Web: https://nordicdata.cloud
157
- - Docs: https://nordicdata.cloud/docs
158
- - Blog: https://nordicdata.cloud/blog
159
- - Smithery (MCP): https://smithery.ai/servers/sofia-jameson-20/Nordic-Data
160
- - Issues: support@nordicdata.cloud
175
+ MIT see `LICENSE`.
package/bin/cli.js CHANGED
@@ -28,17 +28,45 @@ 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
36
37
  board <orgnr> Board + leadership
37
38
  finances <orgnr> Latest financial summary
38
- procurement <orgnr> Public-sector contract aggregates (Doffin)
39
- grants <orgnr> EU R&D grants (Horizon, EIC)
39
+ procurement <orgnr> Public-sector contract aggregates (NO + EU)
40
+ grants <orgnr> EU R&D grant participations
40
41
  sanctions <orgnr> Sanctions screening (EU/UN/OFAC) hits
41
- shareholders <orgnr> Aksjonærregisteret aggregates
42
+ shareholders <orgnr> Shareholder graph aggregates (Norway)
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')}
42
70
  mcp Show MCP setup snippet for Claude Desktop / Cursor
43
71
  signup Open the free-tier signup page in your browser
44
72
  --help, -h Show this help
@@ -50,12 +78,39 @@ ${c('bold', 'FLAGS')}
50
78
  --no-color Disable ANSI colors
51
79
 
52
80
  ${c('bold', 'EXAMPLES')}
53
- ${c('dim', '# Look up Equinor (no key — uses the public widget tier)')}
81
+ ${c('dim', '# Core lookup')}
54
82
  nordic-data search equinor
55
83
  nordic-data lookup 923609016
56
84
  nordic-data contacts 923609016
57
-
58
- ${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)')}
59
114
  export NORDIC_DATA_KEY=nrd_live_...
60
115
  nordic-data lookup 923609016 --json | jq
61
116
 
@@ -63,27 +118,246 @@ ${c('bold', 'EXAMPLES')}
63
118
  nordic-data mcp
64
119
 
65
120
  ${c('bold', 'FREE TIER')}
66
- ${c('orange', '5,000 requests per month, no card.')} Get a key at
121
+ ${c('orange', '500 requests per month, no card.')} Get a key at
67
122
  ${c('cyan', 'https://nordicdata.cloud/?signup=free')}
68
123
  ${c('dim', 'Without a key, this CLI uses the public widget tier (4 lookups/IP/24h).')}
69
124
  `;
70
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
+
71
333
  const args = process.argv.slice(2);
72
334
  let useJson = false;
73
335
  let useColor = isTTY;
336
+ let wantsHelp = false;
74
337
  const cleanArgs = [];
75
338
  for (let i = 0; i < args.length; i++) {
76
339
  const a = args[i];
77
340
  if (a === '--json') useJson = true;
78
341
  else if (a === '--no-color') useColor = false;
79
342
  else if (a === '--key') { process.env.NORDIC_DATA_KEY = args[++i] || ''; }
80
- else if (a === '--help' || a === '-h') { console.log(HELP); process.exit(0); }
343
+ else if (a === '--help' || a === '-h') wantsHelp = true;
81
344
  else if (a === '--version' || a === '-v') {
82
345
  const { version } = require('../package.json');
83
346
  console.log(version);
84
347
  process.exit(0);
85
348
  } else cleanArgs.push(a);
86
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
+ }
87
361
  if (cleanArgs.length === 0) { console.log(HELP); process.exit(0); }
88
362
 
89
363
  const [cmd, ...rest] = cleanArgs;
@@ -100,10 +374,26 @@ async function publicRequest(path) {
100
374
 
101
375
  const res = await fetch(`${API_BASE}${path}`, { headers });
102
376
  if (!res.ok) {
103
- if (res.status === 429) die('Rate limited. Free tier is 4 lookups per IP per 24h. Set NORDIC_DATA_KEY=... for higher limits.');
104
- if (res.status === 401) die('Auth required. Set NORDIC_DATA_KEY=... or use --key. Sign up at https://nordicdata.cloud/?signup=free');
105
- if (res.status === 404) die('Not found.');
106
- 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'}`);
107
397
  }
108
398
  return res.json();
109
399
  }
@@ -149,7 +439,16 @@ async function search(query) {
149
439
  }
150
440
 
151
441
  async function lookup(orgnr) {
152
- 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
+ }
153
452
  const snap = await publicRequest(`/_/look/${orgnr}`);
154
453
  if (useJson) return console.log(JSON.stringify(snap, null, 2));
155
454
 
@@ -258,7 +557,7 @@ async function procurement(orgnr) {
258
557
  const snap = await publicRequest(`/_/look/${orgnr}`);
259
558
  const p = snap.procurement || {};
260
559
  if (useJson) return console.log(JSON.stringify(p, null, 2));
261
- header(`Doffin procurement aggregates for ${id(snap).name || orgnr}`);
560
+ header(`Public procurement aggregates for ${id(snap).name || orgnr}`);
262
561
  pretty('Tenders as buyer', `${p.tenders_as_buyer_24m || 0} (24m)`);
263
562
  pretty('Contracts won', `${p.contracts_won_24m || 0} (24m)`);
264
563
  pretty('Contract value', p.contract_wins_value_24m && `NOK ${p.contract_wins_value_24m.toLocaleString('nb-NO')} (24m)`);
@@ -311,13 +610,40 @@ async function shareholders(orgnr) {
311
610
  const snap = await publicRequest(`/_/look/${orgnr}`);
312
611
  const sh = snap.shareholders || {};
313
612
  if (useJson) return console.log(JSON.stringify(sh, null, 2));
314
- header(`Aksjonærregisteret summary for ${id(snap).name || orgnr}`);
613
+ header(`Shareholder summary for ${id(snap).name || orgnr}`);
315
614
  pretty('Fiscal year', sh.fiscal_year);
316
615
  pretty('Shareholders', sh.count);
317
616
  pretty('Total shares', sh.total_shares && sh.total_shares.toLocaleString('nb-NO'));
318
617
  console.log(c('dim', '\n Full UBO chain available via authenticated /companies/:orgnr/ownership.'));
319
618
  }
320
619
 
620
+
621
+ async function contactsSe(orgnr) {
622
+ if (!orgnr) die('Usage: nordic-data contacts-se <orgnr> (Swedish 10-digit organisation number)');
623
+ const cleaned = String(orgnr).replace(/[-\s]/g, '');
624
+ if (!/^\d{10}$/.test(cleaned)) die('Swedish orgnr must be 10 digits (with or without dash).');
625
+ const data = await publicRequest(`/companies/se/${cleaned}/contact`);
626
+ if (useJson) return console.log(JSON.stringify(data, null, 2));
627
+ header(`${data.name || 'Swedish company'} ${c('dim', `(${data.orgnr_formatted || cleaned})`)}`);
628
+ pretty('VAT number', data.vat_number);
629
+ pretty('Address', data.address);
630
+ const emails = data.emails || [];
631
+ const phones = data.phones || [];
632
+ if (phones.length) pretty('Phone', phones[0]);
633
+ if (emails.length) pretty('Email', emails[0]);
634
+ const nc = data.named_contacts || [];
635
+ if (nc.length) {
636
+ header('Named contacts');
637
+ for (const p of nc.slice(0, 10)) {
638
+ const extras = [p.email, p.phone].filter(Boolean).join(' · ');
639
+ console.log(` ${c('orange', (p.role || '').slice(0, 32).padEnd(32))} ${c('bold', p.name)}${extras ? c('dim', ` · ${extras}`) : ''}`);
640
+ }
641
+ }
642
+ if (data.cached) console.log(c('dim', `\n (cached, fetched ${data.fetched_at || ''})`));
643
+ console.log('');
644
+ console.log(c('dim', 'Sweden contact data uses your monthly contact-enrichment credits.'));
645
+ }
646
+
321
647
  function mcp() {
322
648
  console.log(`${c('bold', 'MCP setup for Claude Desktop / Cursor')}\n`);
323
649
  console.log(c('dim', '# Add to your MCP client config (e.g. claude_desktop_config.json):'));
@@ -331,7 +657,7 @@ function mcp() {
331
657
  };
332
658
  console.log(JSON.stringify(snippet, null, 2));
333
659
  console.log('');
334
- console.log(c('dim', 'Get a free key (5,000 req/mo) at ') + c('cyan', 'https://nordicdata.cloud/?signup=free'));
660
+ console.log(c('dim', 'Get a free key (500 req/mo) at ') + c('cyan', 'https://nordicdata.cloud/?signup=free'));
335
661
  console.log(c('dim', 'Listed on Smithery: ') + c('cyan', 'https://smithery.ai/servers/sofia-jameson-20/Nordic-Data'));
336
662
  console.log(c('dim', 'Listed on mcp.so: ') + c('cyan', 'https://mcp.so/server/nordic-data'));
337
663
  }
@@ -353,6 +679,296 @@ function fmt(n, currency) {
353
679
  return `${cur} ${n.toLocaleString('nb-NO')}`;
354
680
  }
355
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
+
356
972
  // ── Dispatch ───────────────────────────────────────────────
357
973
 
358
974
  (async () => {
@@ -367,6 +983,28 @@ function fmt(n, currency) {
367
983
  case 'grants': await grants(rest[0]); break;
368
984
  case 'sanctions': await sanctions(rest[0]); break;
369
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
370
1008
  case 'mcp': mcp(); break;
371
1009
  case 'signup': signup(); break;
372
1010
  default:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nordic-data",
3
- "version": "0.1.0",
4
- "description": "CLI for Nordic Data every Norwegian company as one API. Look up companies, board members, owners, sanctions, procurement, EU grants, 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
  },
@@ -13,17 +13,34 @@
13
13
  "keywords": [
14
14
  "norway",
15
15
  "norwegian",
16
+ "sweden",
17
+ "swedish",
18
+ "nordic",
19
+ "united-states",
20
+ "us",
21
+ "sec-edgar",
16
22
  "company",
17
23
  "data",
18
- "brreg",
19
- "bronnoysund",
20
- "aksjonaerregisteret",
21
- "doffin",
24
+ "cli",
22
25
  "kyb",
26
+ "kyc",
23
27
  "aml",
24
28
  "sanctions",
25
- "cli",
29
+ "ofac",
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",