nordic-data 0.1.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.
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/bin/cli.js +378 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nordic Data
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# nordic-data
|
|
2
|
+
|
|
3
|
+
Every Norwegian company as one API, from your terminal.
|
|
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
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install -g nordic-data
|
|
16
|
+
# or just use npx
|
|
17
|
+
npx nordic-data --help
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Free tier
|
|
21
|
+
|
|
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
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Set the key as an environment variable:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
export NORDIC_DATA_KEY=nrd_live_...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
You can also pass `--key <key>` per invocation.
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
| Command | What it does |
|
|
40
|
+
|---|---|
|
|
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
|
+
| `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 |
|
|
52
|
+
|
|
53
|
+
## Examples
|
|
54
|
+
|
|
55
|
+
Find a company:
|
|
56
|
+
|
|
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
|
+
```
|
|
64
|
+
|
|
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
|
+
```
|
|
88
|
+
|
|
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
|
+
```
|
|
102
|
+
|
|
103
|
+
JSON output for scripting:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
$ npx nordic-data lookup 923609016 --json | jq .identity.name
|
|
107
|
+
"EQUINOR ASA"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Sanctions screening:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
$ npx nordic-data sanctions 923609016
|
|
114
|
+
Sanctions screening for EQUINOR ASA
|
|
115
|
+
● Officer hits: 1 (of 15 screened)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## MCP setup for Claude Desktop / Cursor
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
$ npx nordic-data mcp
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
prints the JSON snippet to drop into your MCP client config. Or visit our listings:
|
|
125
|
+
|
|
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)
|
|
129
|
+
|
|
130
|
+
## What data is in Nordic Data?
|
|
131
|
+
|
|
132
|
+
Every Norwegian company joined on the organisation number:
|
|
133
|
+
|
|
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
|
|
141
|
+
|
|
142
|
+
## Pricing
|
|
143
|
+
|
|
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).
|
|
145
|
+
|
|
146
|
+
## Comparison vs other vendors
|
|
147
|
+
|
|
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).
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT — see LICENSE.
|
|
153
|
+
|
|
154
|
+
## Links
|
|
155
|
+
|
|
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
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
// Nordic Data CLI — every Norwegian company as one API
|
|
4
|
+
// MIT License — Nordic Data <support@nordicdata.cloud>
|
|
5
|
+
//
|
|
6
|
+
// Endpoints used (verified live, 2026-05):
|
|
7
|
+
// GET /_/look?q=<query> Public name/orgnr search, anonymous
|
|
8
|
+
// GET /_/look/:orgnr Public full snapshot, 4 free per IP/24h
|
|
9
|
+
// GET /companies/:orgnr* Authenticated tier (X-API-Key)
|
|
10
|
+
|
|
11
|
+
const API_BASE = process.env.NORDIC_DATA_API || 'https://api.nordicdata.cloud';
|
|
12
|
+
const API_KEY = process.env.NORDIC_DATA_KEY || '';
|
|
13
|
+
|
|
14
|
+
const COLORS = {
|
|
15
|
+
reset: '\x1b[0m',
|
|
16
|
+
dim: '\x1b[2m',
|
|
17
|
+
bold: '\x1b[1m',
|
|
18
|
+
cyan: '\x1b[36m',
|
|
19
|
+
green: '\x1b[32m',
|
|
20
|
+
red: '\x1b[31m',
|
|
21
|
+
yellow: '\x1b[33m',
|
|
22
|
+
orange: '\x1b[38;5;208m',
|
|
23
|
+
};
|
|
24
|
+
const isTTY = process.stdout.isTTY;
|
|
25
|
+
const c = (color, text) => (isTTY ? `${COLORS[color]}${text}${COLORS.reset}` : text);
|
|
26
|
+
|
|
27
|
+
const HELP = `${c('bold', 'nordic-data')} ${c('dim', '— every Norwegian company as one API')}
|
|
28
|
+
|
|
29
|
+
${c('bold', 'USAGE')}
|
|
30
|
+
nordic-data <command> [args]
|
|
31
|
+
|
|
32
|
+
${c('bold', 'COMMANDS')}
|
|
33
|
+
search <query> Search companies by name or org number
|
|
34
|
+
lookup <orgnr> Full snapshot of one company
|
|
35
|
+
contacts <orgnr> Emails, phones, and named executives
|
|
36
|
+
board <orgnr> Board + leadership
|
|
37
|
+
finances <orgnr> Latest financial summary
|
|
38
|
+
procurement <orgnr> Public-sector contract aggregates (Doffin)
|
|
39
|
+
grants <orgnr> EU R&D grants (Horizon, EIC)
|
|
40
|
+
sanctions <orgnr> Sanctions screening (EU/UN/OFAC) hits
|
|
41
|
+
shareholders <orgnr> Aksjonærregisteret aggregates
|
|
42
|
+
mcp Show MCP setup snippet for Claude Desktop / Cursor
|
|
43
|
+
signup Open the free-tier signup page in your browser
|
|
44
|
+
--help, -h Show this help
|
|
45
|
+
--version, -v Show version
|
|
46
|
+
|
|
47
|
+
${c('bold', 'FLAGS')}
|
|
48
|
+
--json Output raw JSON instead of formatted
|
|
49
|
+
--key <api-key> API key (or set NORDIC_DATA_KEY env var)
|
|
50
|
+
--no-color Disable ANSI colors
|
|
51
|
+
|
|
52
|
+
${c('bold', 'EXAMPLES')}
|
|
53
|
+
${c('dim', '# Look up Equinor (no key — uses the public widget tier)')}
|
|
54
|
+
nordic-data search equinor
|
|
55
|
+
nordic-data lookup 923609016
|
|
56
|
+
nordic-data contacts 923609016
|
|
57
|
+
|
|
58
|
+
${c('dim', '# Use your API key for higher limits')}
|
|
59
|
+
export NORDIC_DATA_KEY=nrd_live_...
|
|
60
|
+
nordic-data lookup 923609016 --json | jq
|
|
61
|
+
|
|
62
|
+
${c('dim', '# Show MCP config for Claude Desktop')}
|
|
63
|
+
nordic-data mcp
|
|
64
|
+
|
|
65
|
+
${c('bold', 'FREE TIER')}
|
|
66
|
+
${c('orange', '5,000 requests per month, no card.')} Get a key at
|
|
67
|
+
${c('cyan', 'https://nordicdata.cloud/?signup=free')}
|
|
68
|
+
${c('dim', 'Without a key, this CLI uses the public widget tier (4 lookups/IP/24h).')}
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
const args = process.argv.slice(2);
|
|
72
|
+
let useJson = false;
|
|
73
|
+
let useColor = isTTY;
|
|
74
|
+
const cleanArgs = [];
|
|
75
|
+
for (let i = 0; i < args.length; i++) {
|
|
76
|
+
const a = args[i];
|
|
77
|
+
if (a === '--json') useJson = true;
|
|
78
|
+
else if (a === '--no-color') useColor = false;
|
|
79
|
+
else if (a === '--key') { process.env.NORDIC_DATA_KEY = args[++i] || ''; }
|
|
80
|
+
else if (a === '--help' || a === '-h') { console.log(HELP); process.exit(0); }
|
|
81
|
+
else if (a === '--version' || a === '-v') {
|
|
82
|
+
const { version } = require('../package.json');
|
|
83
|
+
console.log(version);
|
|
84
|
+
process.exit(0);
|
|
85
|
+
} else cleanArgs.push(a);
|
|
86
|
+
}
|
|
87
|
+
if (cleanArgs.length === 0) { console.log(HELP); process.exit(0); }
|
|
88
|
+
|
|
89
|
+
const [cmd, ...rest] = cleanArgs;
|
|
90
|
+
|
|
91
|
+
// ── API helpers ────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
async function publicRequest(path) {
|
|
94
|
+
const headers = {
|
|
95
|
+
'User-Agent': `nordic-data-cli/${require('../package.json').version}`,
|
|
96
|
+
Origin: 'https://nordicdata.cloud',
|
|
97
|
+
};
|
|
98
|
+
const key = API_KEY || process.env.NORDIC_DATA_KEY;
|
|
99
|
+
if (key) headers['X-API-Key'] = key;
|
|
100
|
+
|
|
101
|
+
const res = await fetch(`${API_BASE}${path}`, { headers });
|
|
102
|
+
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()}`);
|
|
107
|
+
}
|
|
108
|
+
return res.json();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function die(msg) {
|
|
112
|
+
console.error(c('red', '✗ ') + msg);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function pretty(label, value) {
|
|
117
|
+
if (value == null || value === '' || value === 0) return;
|
|
118
|
+
const v = typeof value === 'string' ? value : JSON.stringify(value);
|
|
119
|
+
console.log(` ${c('dim', label.padEnd(16))} ${v}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function header(title) {
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log(c('bold', title));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Normalise nested response paths used across commands.
|
|
128
|
+
function id(snapshot) { return snapshot.identity || {}; }
|
|
129
|
+
function pd(snapshot) { return (snapshot.public_details && snapshot.public_details.contact_details) || {}; }
|
|
130
|
+
function offs(snapshot) { return (snapshot.public_details && snapshot.public_details.top_officers) || []; }
|
|
131
|
+
|
|
132
|
+
// ── Commands ───────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
async function search(query) {
|
|
135
|
+
if (!query) die('Usage: nordic-data search <query>');
|
|
136
|
+
const data = await publicRequest(`/_/look?q=${encodeURIComponent(query)}`);
|
|
137
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
138
|
+
const rs = data.results || [];
|
|
139
|
+
if (rs.length === 0) return console.log(c('dim', 'No results.'));
|
|
140
|
+
header(`${rs.length} result(s) for "${query}"`);
|
|
141
|
+
for (const r of rs.slice(0, 25)) {
|
|
142
|
+
const city = (r.business_address && r.business_address.city) || '';
|
|
143
|
+
const status = r.status === 'active' ? '' : c('dim', ` · ${r.status}`);
|
|
144
|
+
console.log(` ${c('orange', r.orgnr)} ${r.name}${city ? c('dim', ` · ${city}`) : ''}${status}`);
|
|
145
|
+
}
|
|
146
|
+
if (rs.length > 25) console.log(c('dim', ` ...and ${rs.length - 25} more`));
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log(c('dim', 'nordic-data lookup <orgnr> to see the full snapshot.'));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function lookup(orgnr) {
|
|
152
|
+
if (!orgnr) die('Usage: nordic-data lookup <orgnr>');
|
|
153
|
+
const snap = await publicRequest(`/_/look/${orgnr}`);
|
|
154
|
+
if (useJson) return console.log(JSON.stringify(snap, null, 2));
|
|
155
|
+
|
|
156
|
+
const i = id(snap);
|
|
157
|
+
const ba = i.business_address || {};
|
|
158
|
+
header(`${i.name || 'Unknown'} ${c('dim', `(${snap.orgnr})`)}`);
|
|
159
|
+
pretty('Status', i.status);
|
|
160
|
+
if (snap.status && snap.status.is_bankrupt) pretty('Bankruptcy', c('red', 'Active konkurs'));
|
|
161
|
+
pretty('Founded', i.registered);
|
|
162
|
+
pretty('Legal form', i.legal_form && `${i.legal_form.code} (${i.legal_form.description})`);
|
|
163
|
+
if (i.nace && i.nace[0]) pretty('NACE', `${i.nace[0].code} ${i.nace[0].description}`);
|
|
164
|
+
pretty('Address', ba.street);
|
|
165
|
+
pretty('City', `${ba.postal_code || ''} ${ba.city || ''}`.trim());
|
|
166
|
+
pretty('Employees', i.employees);
|
|
167
|
+
pretty('VAT reg.', i.in_vat_registry != null ? (i.in_vat_registry ? 'yes' : 'no') : null);
|
|
168
|
+
pretty('Website', i.website);
|
|
169
|
+
|
|
170
|
+
const contact = pd(snap);
|
|
171
|
+
const phones = contact.phones || [];
|
|
172
|
+
const emails = contact.emails || [];
|
|
173
|
+
if (phones.length) pretty('Phone', phones[0]);
|
|
174
|
+
if (emails.length) pretty('Email', emails[0]);
|
|
175
|
+
|
|
176
|
+
const nc = contact.named_contacts || [];
|
|
177
|
+
if (nc.length) {
|
|
178
|
+
header('Key personnel');
|
|
179
|
+
for (const p of nc.slice(0, 10)) {
|
|
180
|
+
const extras = [p.email, p.phone].filter(Boolean).join(' · ');
|
|
181
|
+
console.log(` ${c('orange', (p.role || '').slice(0, 32).padEnd(32))} ${c('bold', p.name)}${extras ? c('dim', ` · ${extras}`) : ''}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const sanc = snap.sanctions || {};
|
|
186
|
+
if (sanc.company_hits || sanc.officer_hits) {
|
|
187
|
+
header('Sanctions');
|
|
188
|
+
if (sanc.company_hits) console.log(c('red', ` ● Company hits: ${sanc.company_hits}`));
|
|
189
|
+
if (sanc.officer_hits) console.log(c('yellow', ` ● Officer hits: ${sanc.officer_hits}`));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log('');
|
|
193
|
+
console.log(c('dim', 'Open in browser: ') + c('cyan', `https://nordicdata.cloud/company/${snap.orgnr}`));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function contacts(orgnr) {
|
|
197
|
+
if (!orgnr) die('Usage: nordic-data contacts <orgnr>');
|
|
198
|
+
const snap = await publicRequest(`/_/look/${orgnr}`);
|
|
199
|
+
if (useJson) return console.log(JSON.stringify(snap.public_details && snap.public_details.contact_details, null, 2));
|
|
200
|
+
const contact = pd(snap);
|
|
201
|
+
header(`Contacts for ${id(snap).name || orgnr}`);
|
|
202
|
+
const labels = contact.labels || {};
|
|
203
|
+
if ((contact.emails || []).length) {
|
|
204
|
+
console.log(c('bold', '\n Emails:'));
|
|
205
|
+
for (const e of contact.emails) {
|
|
206
|
+
const label = labels[`email:${e}`] || '';
|
|
207
|
+
console.log(` ${e}${label ? c('dim', ` · ${label}`) : ''}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if ((contact.phones || []).length) {
|
|
211
|
+
console.log(c('bold', '\n Phones:'));
|
|
212
|
+
for (const p of contact.phones) {
|
|
213
|
+
const label = labels[`phone:${p}`] || '';
|
|
214
|
+
console.log(` ${p}${label ? c('dim', ` · ${label}`) : ''}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if ((contact.named_contacts || []).length) {
|
|
218
|
+
console.log(c('bold', '\n Named contacts:'));
|
|
219
|
+
for (const n of contact.named_contacts) {
|
|
220
|
+
console.log(` ${c('orange', n.role || '')} ${c('bold', n.name)}${n.email ? c('dim', ` · ${n.email}`) : ''}${n.phone ? c('dim', ` · ${n.phone}`) : ''}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function board(orgnr) {
|
|
226
|
+
if (!orgnr) die('Usage: nordic-data board <orgnr>');
|
|
227
|
+
const snap = await publicRequest(`/_/look/${orgnr}`);
|
|
228
|
+
const officers = offs(snap);
|
|
229
|
+
if (useJson) return console.log(JSON.stringify(officers, null, 2));
|
|
230
|
+
header(`Officers for ${id(snap).name || orgnr}`);
|
|
231
|
+
if (!officers.length) return console.log(c('dim', ' No officers in public snapshot.'));
|
|
232
|
+
for (const o of officers) {
|
|
233
|
+
const cat = o.category === 'styre' ? c('cyan', '[styre] ') : o.category === 'ledelse' ? c('orange', '[ledelse] ') : c('dim', `[${o.category}]`.padEnd(12));
|
|
234
|
+
console.log(` ${cat} ${(o.role_description || '').padEnd(22)} ${c('bold', o.name)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function finances(orgnr) {
|
|
239
|
+
if (!orgnr) die('Usage: nordic-data finances <orgnr>');
|
|
240
|
+
const snap = await publicRequest(`/_/look/${orgnr}`);
|
|
241
|
+
const a = snap.accounts || {};
|
|
242
|
+
if (useJson) return console.log(JSON.stringify(a, null, 2));
|
|
243
|
+
header(`Financials ${a.fiscal_year ? `(FY${a.fiscal_year})` : ''} for ${id(snap).name || orgnr}`);
|
|
244
|
+
const is_ = a.income_statement || {};
|
|
245
|
+
const bs = a.balance_sheet || {};
|
|
246
|
+
const r = a.ratios || {};
|
|
247
|
+
pretty('Revenue', fmt(is_.revenue, a.currency));
|
|
248
|
+
pretty('Operating profit', fmt(is_.operating_profit, a.currency));
|
|
249
|
+
pretty('Net result', fmt(is_.net_result, a.currency));
|
|
250
|
+
pretty('Total assets', fmt(bs.total_assets, a.currency));
|
|
251
|
+
pretty('Equity', fmt(bs.equity, a.currency));
|
|
252
|
+
pretty('Equity ratio', r.equity_ratio != null && (r.equity_ratio * 100).toFixed(1) + '%');
|
|
253
|
+
pretty('Net margin', r.net_margin != null && (r.net_margin * 100).toFixed(1) + '%');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function procurement(orgnr) {
|
|
257
|
+
if (!orgnr) die('Usage: nordic-data procurement <orgnr>');
|
|
258
|
+
const snap = await publicRequest(`/_/look/${orgnr}`);
|
|
259
|
+
const p = snap.procurement || {};
|
|
260
|
+
if (useJson) return console.log(JSON.stringify(p, null, 2));
|
|
261
|
+
header(`Doffin procurement aggregates for ${id(snap).name || orgnr}`);
|
|
262
|
+
pretty('Tenders as buyer', `${p.tenders_as_buyer_24m || 0} (24m)`);
|
|
263
|
+
pretty('Contracts won', `${p.contracts_won_24m || 0} (24m)`);
|
|
264
|
+
pretty('Contract value', p.contract_wins_value_24m && `NOK ${p.contract_wins_value_24m.toLocaleString('nb-NO')} (24m)`);
|
|
265
|
+
const top = (snap.public_details && snap.public_details.top_contract_wins) || [];
|
|
266
|
+
if (top.length) {
|
|
267
|
+
console.log(c('bold', '\n Top contract wins:'));
|
|
268
|
+
for (const w of top.slice(0, 10)) {
|
|
269
|
+
console.log(` ${c('orange', w.awarded_at || '')} ${w.buyer || ''} ${c('dim', '·')} ${w.value ? 'NOK ' + Number(w.value).toLocaleString('nb-NO') : ''}`);
|
|
270
|
+
if (w.title) console.log(` ${c('dim', w.title)}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function grants(orgnr) {
|
|
276
|
+
if (!orgnr) die('Usage: nordic-data grants <orgnr>');
|
|
277
|
+
const snap = await publicRequest(`/_/look/${orgnr}`);
|
|
278
|
+
const g = snap.eu_funding || {};
|
|
279
|
+
if (useJson) return console.log(JSON.stringify(g, null, 2));
|
|
280
|
+
header(`EU funding for ${id(snap).name || orgnr}`);
|
|
281
|
+
pretty('Horizon grants', g.horizon_grants);
|
|
282
|
+
pretty('Coordinator grants', g.coordinator_grants);
|
|
283
|
+
pretty('Total EC contrib', g.total_ec_contribution_eur && `EUR ${Math.round(g.total_ec_contribution_eur).toLocaleString('nb-NO')}`);
|
|
284
|
+
const top = (snap.public_details && snap.public_details.top_eu_grants) || [];
|
|
285
|
+
if (top.length) {
|
|
286
|
+
console.log(c('bold', '\n Top grants:'));
|
|
287
|
+
for (const t of top.slice(0, 10)) {
|
|
288
|
+
console.log(` ${c('orange', t.programme || '')} ${(t.topic || '').slice(0, 60)}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function sanctions(orgnr) {
|
|
294
|
+
if (!orgnr) die('Usage: nordic-data sanctions <orgnr>');
|
|
295
|
+
const snap = await publicRequest(`/_/look/${orgnr}`);
|
|
296
|
+
const s = snap.sanctions || {};
|
|
297
|
+
if (useJson) return console.log(JSON.stringify(s, null, 2));
|
|
298
|
+
header(`Sanctions screening for ${id(snap).name || orgnr}`);
|
|
299
|
+
if (!s.company_hits && !s.officer_hits) {
|
|
300
|
+
console.log(c('green', ' ✓ No sanctions hits.'));
|
|
301
|
+
pretty('Officers screened', s.officers_screened);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (s.company_hits) console.log(c('red', ` ● Company hits: ${s.company_hits}`));
|
|
305
|
+
if (s.officer_hits) console.log(c('yellow', ` ● Officer hits: ${s.officer_hits} (of ${s.officers_screened || '?'} screened)`));
|
|
306
|
+
if (s.company_top_match) pretty('Top match', s.company_top_match);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function shareholders(orgnr) {
|
|
310
|
+
if (!orgnr) die('Usage: nordic-data shareholders <orgnr>');
|
|
311
|
+
const snap = await publicRequest(`/_/look/${orgnr}`);
|
|
312
|
+
const sh = snap.shareholders || {};
|
|
313
|
+
if (useJson) return console.log(JSON.stringify(sh, null, 2));
|
|
314
|
+
header(`Aksjonærregisteret summary for ${id(snap).name || orgnr}`);
|
|
315
|
+
pretty('Fiscal year', sh.fiscal_year);
|
|
316
|
+
pretty('Shareholders', sh.count);
|
|
317
|
+
pretty('Total shares', sh.total_shares && sh.total_shares.toLocaleString('nb-NO'));
|
|
318
|
+
console.log(c('dim', '\n Full UBO chain available via authenticated /companies/:orgnr/ownership.'));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function mcp() {
|
|
322
|
+
console.log(`${c('bold', 'MCP setup for Claude Desktop / Cursor')}\n`);
|
|
323
|
+
console.log(c('dim', '# Add to your MCP client config (e.g. claude_desktop_config.json):'));
|
|
324
|
+
const snippet = {
|
|
325
|
+
mcpServers: {
|
|
326
|
+
'nordic-data': {
|
|
327
|
+
url: 'https://api.nordicdata.cloud/mcp',
|
|
328
|
+
headers: { Authorization: 'Bearer YOUR_NORDIC_DATA_KEY' },
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
console.log(JSON.stringify(snippet, null, 2));
|
|
333
|
+
console.log('');
|
|
334
|
+
console.log(c('dim', 'Get a free key (5,000 req/mo) at ') + c('cyan', 'https://nordicdata.cloud/?signup=free'));
|
|
335
|
+
console.log(c('dim', 'Listed on Smithery: ') + c('cyan', 'https://smithery.ai/servers/sofia-jameson-20/Nordic-Data'));
|
|
336
|
+
console.log(c('dim', 'Listed on mcp.so: ') + c('cyan', 'https://mcp.so/server/nordic-data'));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function signup() {
|
|
340
|
+
const url = 'https://nordicdata.cloud/?signup=free';
|
|
341
|
+
console.log(`Opening ${c('cyan', url)} in your browser...`);
|
|
342
|
+
const { exec } = require('child_process');
|
|
343
|
+
const cmds = { darwin: `open "${url}"`, win32: `start "" "${url}"`, linux: `xdg-open "${url}"` };
|
|
344
|
+
const c2 = cmds[process.platform];
|
|
345
|
+
if (c2) exec(c2);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function fmt(n, currency) {
|
|
349
|
+
if (n == null) return null;
|
|
350
|
+
const cur = currency || 'NOK';
|
|
351
|
+
if (Math.abs(n) >= 1e9) return `${cur} ${(n / 1e9).toFixed(2)}B`;
|
|
352
|
+
if (Math.abs(n) >= 1e6) return `${cur} ${(n / 1e6).toFixed(1)}M`;
|
|
353
|
+
return `${cur} ${n.toLocaleString('nb-NO')}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── Dispatch ───────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
(async () => {
|
|
359
|
+
try {
|
|
360
|
+
switch (cmd) {
|
|
361
|
+
case 'search': await search(rest.join(' ')); break;
|
|
362
|
+
case 'lookup': await lookup(rest[0]); break;
|
|
363
|
+
case 'contacts': await contacts(rest[0]); break;
|
|
364
|
+
case 'board': await board(rest[0]); break;
|
|
365
|
+
case 'finances': await finances(rest[0]); break;
|
|
366
|
+
case 'procurement': await procurement(rest[0]); break;
|
|
367
|
+
case 'grants': await grants(rest[0]); break;
|
|
368
|
+
case 'sanctions': await sanctions(rest[0]); break;
|
|
369
|
+
case 'shareholders': await shareholders(rest[0]); break;
|
|
370
|
+
case 'mcp': mcp(); break;
|
|
371
|
+
case 'signup': signup(); break;
|
|
372
|
+
default:
|
|
373
|
+
die(`Unknown command: ${cmd}\n\nRun ${c('bold', 'nordic-data --help')} for usage.`);
|
|
374
|
+
}
|
|
375
|
+
} catch (err) {
|
|
376
|
+
die(err.message || String(err));
|
|
377
|
+
}
|
|
378
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
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.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"nordic-data": "./bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"norway",
|
|
15
|
+
"norwegian",
|
|
16
|
+
"company",
|
|
17
|
+
"data",
|
|
18
|
+
"brreg",
|
|
19
|
+
"bronnoysund",
|
|
20
|
+
"aksjonaerregisteret",
|
|
21
|
+
"doffin",
|
|
22
|
+
"kyb",
|
|
23
|
+
"aml",
|
|
24
|
+
"sanctions",
|
|
25
|
+
"cli",
|
|
26
|
+
"mcp",
|
|
27
|
+
"nordic-data"
|
|
28
|
+
],
|
|
29
|
+
"homepage": "https://nordicdata.cloud",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/Aengus406/nordic-data-cli/issues",
|
|
32
|
+
"email": "support@nordicdata.cloud"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/Aengus406/nordic-data-cli.git"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"author": "Nordic Data <support@nordicdata.cloud> (https://nordicdata.cloud)",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "node --test test/*.test.mjs",
|
|
45
|
+
"prepublishOnly": "node bin/cli.js --version"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {}
|
|
48
|
+
}
|