portable-agent-layer 0.110.0 → 0.111.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/assets/skills/research-entity/SKILL.md +6 -0
- package/package.json +1 -1
- package/src/cli/knowledge-research.ts +6 -1
- package/src/hooks/handlers/entity-extraction.ts +2 -0
- package/src/hooks/lib/company-register-router.ts +24 -0
- package/src/hooks/lib/company-register.ts +25 -0
- package/src/hooks/lib/entity-extraction.ts +1 -0
- package/src/hooks/lib/entity-research.ts +88 -3
- package/src/hooks/lib/hungarian-registry.ts +194 -0
|
@@ -23,6 +23,7 @@ Research the company or person named in $ARGUMENTS and show a sourced profile. T
|
|
|
23
23
|
1. Work out from $ARGUMENTS and the conversation:
|
|
24
24
|
- the exact name;
|
|
25
25
|
- whether it is a company or a person;
|
|
26
|
+
- for a company, its registered name with the legal form, not a brand or short name. The legal form is what routes it to its country's company register;
|
|
26
27
|
- for a person, where they work;
|
|
27
28
|
- one sentence of what the user said about them.
|
|
28
29
|
If you cannot tell a company from a person, ask.
|
|
@@ -34,6 +35,11 @@ Research the company or person named in $ARGUMENTS and show a sourced profile. T
|
|
|
34
35
|
```bash
|
|
35
36
|
pal cli knowledge research "<name>" --person --organization "<employer>" --context "<what the user said>"
|
|
36
37
|
```
|
|
38
|
+
When the legal form places the company in a country PAL has a register for, the command first reads that register directly, because web search does not index it. An exact registered-name match fills in the registration number, tax number, seat, main activity, managing directors and status, and the profile is kept even when the web finds nothing else. A brand name, a similar name, or a company from a country without a register goes through web search alone.
|
|
39
|
+
|
|
40
|
+
| Country | Legal forms | Register read |
|
|
41
|
+
| ------- | ----------- | ------------- |
|
|
42
|
+
| Hungary | Kft., Zrt., Nyrt., Bt., Kkt., or spelled out | ceginfo.hu for the registration number, nemzeticegtar.hu for the record |
|
|
37
43
|
It takes one to several minutes. Add `--queue` when the user wants the profile saved; it then waits in the review queue.
|
|
38
44
|
3. If the command exits 1, tell the user the reason it printed. Usual reasons:
|
|
39
45
|
- the model found only namesakes (`match: unsure`): ask for something that tells them apart, such as an employer, a city or a website, and run it again with that in `--context` or `--organization`;
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* the active agent, print the profile, and optionally queue it for review.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { lookupCompanyRegister } from "../hooks/lib/company-register-router";
|
|
6
7
|
import { loadKnownEntities, loadNameIndex } from "../hooks/lib/entity-cards";
|
|
7
8
|
import { queueForReview } from "../hooks/lib/entity-extraction";
|
|
8
9
|
import { lookupName } from "../hooks/lib/entity-names";
|
|
@@ -46,7 +47,11 @@ function targetFrom(name: string, values: ResearchValues): ResearchTarget {
|
|
|
46
47
|
export async function researchFromCli(
|
|
47
48
|
name: string | undefined,
|
|
48
49
|
values: ResearchValues,
|
|
49
|
-
deps: ResearchDeps = {
|
|
50
|
+
deps: ResearchDeps = {
|
|
51
|
+
principal: identity().principal.name,
|
|
52
|
+
infer: inference,
|
|
53
|
+
registry: lookupCompanyRegister,
|
|
54
|
+
}
|
|
50
55
|
): Promise<number> {
|
|
51
56
|
if (!name?.trim()) throw new UsageError("name the person or company to research");
|
|
52
57
|
const target = targetFrom(name.trim(), values);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { ingestEntities } from "../../tools/knowledge/ingest";
|
|
9
|
+
import { lookupCompanyRegister } from "../lib/company-register-router";
|
|
9
10
|
import { excludedNames, loadKnownEntities, loadNameIndex } from "../lib/entity-cards";
|
|
10
11
|
import {
|
|
11
12
|
extractionSchema,
|
|
@@ -69,6 +70,7 @@ export async function extractSessionEntities(
|
|
|
69
70
|
await researchNewEntities(newResearchTargets(plan.ingest, written), {
|
|
70
71
|
principal: id.principal.name,
|
|
71
72
|
infer: inference,
|
|
73
|
+
registry: lookupCompanyRegister,
|
|
72
74
|
});
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official company registers, routed by country. A company is looked up only in the
|
|
3
|
+
* register of the country its name places it in; any other name goes to web search
|
|
4
|
+
* alone. To add a country, implement CompanyRegister in its own module and add it to
|
|
5
|
+
* REGISTERS.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CompanyRegister, Fetch, RegisterEntry } from "./company-register";
|
|
9
|
+
import { hungarianRegister } from "./hungarian-registry";
|
|
10
|
+
|
|
11
|
+
const REGISTERS: CompanyRegister[] = [hungarianRegister];
|
|
12
|
+
|
|
13
|
+
/** Null when no register recognizes the name, and when its register has no exact match. */
|
|
14
|
+
export async function lookupCompanyRegister(
|
|
15
|
+
name: string,
|
|
16
|
+
fetchImpl: Fetch = fetch,
|
|
17
|
+
registers: CompanyRegister[] = REGISTERS
|
|
18
|
+
): Promise<RegisterEntry | null> {
|
|
19
|
+
const wanted = name.trim();
|
|
20
|
+
const register = registers.find((r) => r.recognizes(wanted));
|
|
21
|
+
if (!register) return null;
|
|
22
|
+
const company = await register.lookup(wanted, fetchImpl);
|
|
23
|
+
return company && { ...company, country: register.country };
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** What a country's company register implements to join the routing table in company-register-router.ts. */
|
|
2
|
+
|
|
3
|
+
export interface RegisteredCompany {
|
|
4
|
+
name: string;
|
|
5
|
+
fullName: string;
|
|
6
|
+
number: string;
|
|
7
|
+
taxNumber: string;
|
|
8
|
+
seat: string;
|
|
9
|
+
status: string;
|
|
10
|
+
activity: string;
|
|
11
|
+
founded: string;
|
|
12
|
+
managers: string[];
|
|
13
|
+
url: string;
|
|
14
|
+
sources: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type RegisterEntry = RegisteredCompany & { country: string };
|
|
18
|
+
|
|
19
|
+
export type Fetch = (input: string, init?: RequestInit) => Promise<Response>;
|
|
20
|
+
|
|
21
|
+
export interface CompanyRegister {
|
|
22
|
+
country: string;
|
|
23
|
+
recognizes: (name: string) => boolean;
|
|
24
|
+
lookup: (name: string, fetchImpl: Fetch) => Promise<RegisteredCompany | null>;
|
|
25
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { IngestInput, IngestResult } from "../../tools/knowledge/ingest";
|
|
8
|
+
import type { RegisterEntry } from "./company-register";
|
|
8
9
|
import {
|
|
9
10
|
closedObject,
|
|
10
11
|
type ExtractedEntity,
|
|
@@ -27,6 +28,7 @@ export interface ResearchDeps {
|
|
|
27
28
|
principal: string;
|
|
28
29
|
infer: typeof inference;
|
|
29
30
|
today?: string;
|
|
31
|
+
registry?: (name: string) => Promise<RegisterEntry | null>;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const MAX_TARGETS = 3;
|
|
@@ -83,6 +85,7 @@ export function researchSchema() {
|
|
|
83
85
|
taxNumber: text,
|
|
84
86
|
seat: text,
|
|
85
87
|
status: text,
|
|
88
|
+
activity: text,
|
|
86
89
|
managers: list(text),
|
|
87
90
|
url: text,
|
|
88
91
|
}),
|
|
@@ -96,7 +99,7 @@ const CHECKLIST = {
|
|
|
96
99
|
"- website: the official website.",
|
|
97
100
|
"- linkedin: the LinkedIn company page.",
|
|
98
101
|
"- socials: other official accounts (X, Facebook, Instagram, YouTube, GitHub).",
|
|
99
|
-
"- registry: its entry in the official company register of its country: registered name, registration number, tax number, registered seat, status (active, in liquidation…), managing directors, and the URL you read it on. For a Hungarian company (Kft., Zrt., Bt., Nyrt., a .hu site) that is the cégjegyzék: e-cegjegyzek.hu, or public mirrors such as ceginformacio.hu, nemzeticegtar.hu or opten.hu.",
|
|
102
|
+
"- registry: its entry in the official company register of its country: registered name, registration number, tax number, registered seat, status (active, in liquidation…), main activity, managing directors, and the URL you read it on. For a Hungarian company (Kft., Zrt., Bt., Nyrt., a .hu site) that is the cégjegyzék: e-cegjegyzek.hu, or public mirrors such as ceginformacio.hu, nemzeticegtar.hu or opten.hu.",
|
|
100
103
|
"- news: up to five news items or public statements from the last 12 months, newest first.",
|
|
101
104
|
"- summary: two or three sentences on what the company does, its size and where it is based.",
|
|
102
105
|
],
|
|
@@ -124,13 +127,30 @@ export function researchSystem(principal: string, kind: ResearchTarget["kind"]):
|
|
|
124
127
|
].join("\n");
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
function
|
|
130
|
+
function registryFacts(entry: RegisterEntry): string {
|
|
131
|
+
return [
|
|
132
|
+
"The company register, already read: use it to tell this company from namesakes and do not search the register again.",
|
|
133
|
+
`registered name: ${entry.fullName || entry.name}`,
|
|
134
|
+
`registration number: ${entry.number}`,
|
|
135
|
+
entry.seat && `seat: ${entry.seat}`,
|
|
136
|
+
entry.activity && `main activity: ${entry.activity}`,
|
|
137
|
+
]
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function researchUser(
|
|
143
|
+
target: ResearchTarget,
|
|
144
|
+
today: string,
|
|
145
|
+
registry: RegisterEntry | null
|
|
146
|
+
): string {
|
|
128
147
|
return [
|
|
129
148
|
`Today is ${today}.`,
|
|
130
149
|
`${target.kind}: ${target.name}`,
|
|
131
150
|
target.organization && `organization: ${target.organization}`,
|
|
132
151
|
target.fact &&
|
|
133
152
|
`what ${target.kind === "person" ? "was said about them" : "was said about it"}: ${target.fact}`,
|
|
153
|
+
registry && registryFacts(registry),
|
|
134
154
|
]
|
|
135
155
|
.filter(Boolean)
|
|
136
156
|
.join("\n");
|
|
@@ -166,13 +186,76 @@ export type ResearchOutcome =
|
|
|
166
186
|
| { found: true; profile: ResearchProfile }
|
|
167
187
|
| { found: false; reason: string };
|
|
168
188
|
|
|
189
|
+
async function registryEntry(
|
|
190
|
+
target: ResearchTarget,
|
|
191
|
+
deps: ResearchDeps
|
|
192
|
+
): Promise<RegisterEntry | null> {
|
|
193
|
+
if (target.kind !== "company" || !deps.registry) return null;
|
|
194
|
+
try {
|
|
195
|
+
return await deps.registry(target.name);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
logError(`entity-research:registry:${target.name}`, err);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function registryOnlyProfile(entry: RegisterEntry): ResearchProfile {
|
|
203
|
+
const founded = entry.founded ? `, founded ${entry.founded}` : "";
|
|
204
|
+
const what = entry.activity || `company registered in ${entry.country}`;
|
|
205
|
+
return {
|
|
206
|
+
match: "sure",
|
|
207
|
+
summary: `${entry.name}: ${what}. Seat: ${entry.seat}${founded}.`,
|
|
208
|
+
role: "",
|
|
209
|
+
organization: "",
|
|
210
|
+
website: "",
|
|
211
|
+
linkedin: "",
|
|
212
|
+
socials: [],
|
|
213
|
+
registry: registryOf(entry),
|
|
214
|
+
news: [],
|
|
215
|
+
sources: [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function registryOf(entry: RegisterEntry): ResearchProfile["registry"] {
|
|
220
|
+
return {
|
|
221
|
+
name: entry.fullName || entry.name,
|
|
222
|
+
number: entry.number,
|
|
223
|
+
taxNumber: entry.taxNumber,
|
|
224
|
+
seat: entry.seat,
|
|
225
|
+
status: entry.status,
|
|
226
|
+
activity: entry.activity,
|
|
227
|
+
managers: entry.managers,
|
|
228
|
+
url: entry.url,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** What the register says replaces what the model read, and an exact register match is
|
|
233
|
+
* enough to keep the profile even when the web turned up nothing. */
|
|
234
|
+
function withRegistry(
|
|
235
|
+
profile: ResearchProfile | null,
|
|
236
|
+
entry: RegisterEntry
|
|
237
|
+
): ResearchProfile {
|
|
238
|
+
const base =
|
|
239
|
+
profile?.match === "sure" ? withWebUrlsOnly(profile) : registryOnlyProfile(entry);
|
|
240
|
+
return {
|
|
241
|
+
...base,
|
|
242
|
+
registry: registryOf(entry),
|
|
243
|
+
sources: [...new Set([...entry.sources, ...base.sources])],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
169
247
|
export async function researchEntity(
|
|
170
248
|
target: ResearchTarget,
|
|
171
249
|
deps: ResearchDeps
|
|
172
250
|
): Promise<ResearchOutcome> {
|
|
251
|
+
const registry = await registryEntry(target, deps);
|
|
173
252
|
const result = await deps.infer({
|
|
174
253
|
system: researchSystem(deps.principal, target.kind),
|
|
175
|
-
user: researchUser(
|
|
254
|
+
user: researchUser(
|
|
255
|
+
target,
|
|
256
|
+
deps.today ?? new Date().toISOString().slice(0, 10),
|
|
257
|
+
registry
|
|
258
|
+
),
|
|
176
259
|
tier: "medium",
|
|
177
260
|
timeout: RESEARCH_TIMEOUT_MS,
|
|
178
261
|
jsonSchema: researchSchema(),
|
|
@@ -180,6 +263,7 @@ export async function researchEntity(
|
|
|
180
263
|
web: true,
|
|
181
264
|
});
|
|
182
265
|
const profile = result.success ? parseProfile(result.output) : null;
|
|
266
|
+
if (registry) return { found: true, profile: withRegistry(profile, registry) };
|
|
183
267
|
if (!profile) return { found: false, reason: result.error ?? "no usable reply" };
|
|
184
268
|
if (profile.match !== "sure")
|
|
185
269
|
return { found: false, reason: `match: ${profile.match}. ${profile.summary}`.trim() };
|
|
@@ -243,6 +327,7 @@ function registryLines(r: ResearchProfile["registry"]): string[] {
|
|
|
243
327
|
["Tax number", r.taxNumber],
|
|
244
328
|
["Seat", r.seat],
|
|
245
329
|
["Status", r.status],
|
|
330
|
+
["Main activity", r.activity],
|
|
246
331
|
["Managing directors", r.managers.join(", ")],
|
|
247
332
|
["Registry entry", r.url],
|
|
248
333
|
];
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Hungarian company register, read directly. Web search does not index it, so a
|
|
3
|
+
* young or small Kft. is invisible to a search-only lookup. Only a name ending in a
|
|
4
|
+
* Hungarian legal form is recognized: ceginfo.hu resolves the exact name to its
|
|
5
|
+
* registration number, nemzeticegtar.hu shows the record for free.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CompanyRegister, Fetch, RegisteredCompany } from "./company-register";
|
|
9
|
+
|
|
10
|
+
interface Page {
|
|
11
|
+
url: string;
|
|
12
|
+
html: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CEGINFO = "https://ceginfo.hu/";
|
|
16
|
+
const NEMZETICEGTAR = "https://www.nemzeticegtar.hu/";
|
|
17
|
+
const USER_AGENT = "Mozilla/5.0 (compatible; PAL knowledge research)";
|
|
18
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
19
|
+
const MAX_REDIRECTS = 5;
|
|
20
|
+
const REDIRECTS = new Set([301, 302, 303, 307, 308]);
|
|
21
|
+
const SEARCH_TOKEN_RE = /name="ceginfo_csrf_token" value="([^"]+)"/;
|
|
22
|
+
const OFFICERS_RE = /Cégjegyzésre jogosultak<\/h3>([\s\S]*?)<\/ul>/;
|
|
23
|
+
const PROCEEDINGS_RE = /<p[^>]*>([^<]*(?:felszámolási|végelszámolási)[^<]*)<\/p>/;
|
|
24
|
+
|
|
25
|
+
const LONG_FORMS: Array<[RegExp, string]> = [
|
|
26
|
+
[/korlátolt felelősségű társaság$/i, "kft"],
|
|
27
|
+
[/zártkörűen működő részvénytársaság$/i, "zrt"],
|
|
28
|
+
[/nyilvánosan működő részvénytársaság$/i, "nyrt"],
|
|
29
|
+
[/betéti társaság$/i, "bt"],
|
|
30
|
+
[/közkereseti társaság$/i, "kkt"],
|
|
31
|
+
];
|
|
32
|
+
const SHORT_FORM_RE = /(?:^|\s)(?:kft|zrt|nyrt|bt|kkt|rt)\.?$/i;
|
|
33
|
+
|
|
34
|
+
function isHungarianCompanyName(name: string): boolean {
|
|
35
|
+
return SHORT_FORM_RE.test(name) || LONG_FORMS.some(([re]) => re.test(name));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function comparable(name: string): string {
|
|
39
|
+
const short = LONG_FORMS.reduce((n, [re, form]) => n.replace(re, form), name);
|
|
40
|
+
return short.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ENTITIES: Record<string, string> = { amp: "&", quot: '"', nbsp: " " };
|
|
44
|
+
|
|
45
|
+
function decoded(html: string): string {
|
|
46
|
+
return html.replace(/&(#x?[0-9a-f]+|\w+);/gi, (whole, code: string) => {
|
|
47
|
+
if (code.startsWith("#x"))
|
|
48
|
+
return String.fromCodePoint(Number.parseInt(code.slice(2), 16));
|
|
49
|
+
if (code.startsWith("#")) return String.fromCodePoint(Number(code.slice(1)));
|
|
50
|
+
return ENTITIES[code] ?? whole;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function plainText(html: string): string {
|
|
55
|
+
return decoded(html.replace(/<[^>]+>/g, ""))
|
|
56
|
+
.replace(/\s+/g, " ")
|
|
57
|
+
.trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function remember(response: Response, jar: Map<string, string>): void {
|
|
61
|
+
for (const cookie of response.headers.getSetCookie()) {
|
|
62
|
+
const [pair] = cookie.split(";");
|
|
63
|
+
const at = pair.indexOf("=");
|
|
64
|
+
jar.set(pair.slice(0, at), pair.slice(at + 1));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One browsing session: cookies carried across requests and redirects followed by hand,
|
|
69
|
+
* because a search result lives in the session the search was posted from. */
|
|
70
|
+
function session(fetchImpl: Fetch) {
|
|
71
|
+
const jar = new Map<string, string>();
|
|
72
|
+
return async (url: string, form?: URLSearchParams): Promise<Page> => {
|
|
73
|
+
let current = url;
|
|
74
|
+
let body = form;
|
|
75
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
76
|
+
const response = await fetchImpl(current, {
|
|
77
|
+
method: body ? "POST" : "GET",
|
|
78
|
+
body,
|
|
79
|
+
redirect: "manual",
|
|
80
|
+
headers: {
|
|
81
|
+
"user-agent": USER_AGENT,
|
|
82
|
+
cookie: [...jar].map(([k, v]) => `${k}=${v}`).join("; "),
|
|
83
|
+
},
|
|
84
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
85
|
+
});
|
|
86
|
+
remember(response, jar);
|
|
87
|
+
const location = response.headers.get("location");
|
|
88
|
+
if (location && REDIRECTS.has(response.status)) {
|
|
89
|
+
current = new URL(location, current).href;
|
|
90
|
+
body = undefined;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!response.ok) throw new Error(`${current}: HTTP ${response.status}`);
|
|
94
|
+
return { url: current, html: await response.text() };
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`${url}: too many redirects`);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface Hit {
|
|
101
|
+
name: string;
|
|
102
|
+
number: string;
|
|
103
|
+
slug: string;
|
|
104
|
+
url: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function searchHits(html: string): Hit[] {
|
|
108
|
+
const results = html.matchAll(
|
|
109
|
+
/<a href="([^"]*\/ceg-adatlap\/([a-z0-9-]+)-(\d{10})\.html)"[^>]*>[\s\S]*?<h2[^>]*>([\s\S]*?)<\/h2>/g
|
|
110
|
+
);
|
|
111
|
+
return Array.from(results, ([, url, slug, number, title]) => ({
|
|
112
|
+
name: plainText(title.replace(/<small[\s\S]*?<\/small>/g, "")),
|
|
113
|
+
number,
|
|
114
|
+
slug,
|
|
115
|
+
url: url.replace(/(?<!:)\/\//g, "/"),
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function findRegistered(
|
|
120
|
+
name: string,
|
|
121
|
+
browse: ReturnType<typeof session>
|
|
122
|
+
): Promise<Hit | null> {
|
|
123
|
+
const home = await browse(CEGINFO);
|
|
124
|
+
const token = SEARCH_TOKEN_RE.exec(home.html)?.[1];
|
|
125
|
+
if (!token) throw new Error("ceginfo.hu: the search form has no token");
|
|
126
|
+
const results = await browse(
|
|
127
|
+
`${CEGINFO}cegkereso/rapid`,
|
|
128
|
+
new URLSearchParams({ ceginfo_csrf_token: token, rapid: name })
|
|
129
|
+
);
|
|
130
|
+
const exact = searchHits(results.html).filter(
|
|
131
|
+
(hit) => comparable(hit.name) === comparable(name)
|
|
132
|
+
);
|
|
133
|
+
return exact.length === 1 ? exact[0] : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function field(html: string, label: string): string {
|
|
137
|
+
const value = new RegExp(
|
|
138
|
+
String.raw`<h6[^>]*>\s*${label}\s*</h6>\s*<p[^>]*>([\s\S]*?)</p>`,
|
|
139
|
+
"i"
|
|
140
|
+
).exec(html)?.[1];
|
|
141
|
+
return value ? plainText(value) : "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Names only: the register also prints each officer's mother's name and home address. */
|
|
145
|
+
function managers(html: string): string[] {
|
|
146
|
+
const block = OFFICERS_RE.exec(html)?.[1] ?? "";
|
|
147
|
+
return Array.from(block.matchAll(/<strong[^>]*>([\s\S]*?)<\/strong>/g), ([, n]) =>
|
|
148
|
+
plainText(n)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function proceedings(html: string): string {
|
|
153
|
+
const sentence = PROCEEDINGS_RE.exec(html)?.[1];
|
|
154
|
+
return sentence ? plainText(sentence) : "";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formattedNumber(digits: string): string {
|
|
158
|
+
return `${digits.slice(0, 2)}-${digits.slice(2, 4)}-${digits.slice(4)}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function entryFrom(hit: Hit, record: Page): RegisteredCompany {
|
|
162
|
+
const html = record.html;
|
|
163
|
+
return {
|
|
164
|
+
name: hit.name,
|
|
165
|
+
fullName: field(html, "Teljes név"),
|
|
166
|
+
number: formattedNumber(hit.number),
|
|
167
|
+
taxNumber: field(html, "Adószám"),
|
|
168
|
+
seat: field(html, "székhely"),
|
|
169
|
+
status: proceedings(html),
|
|
170
|
+
activity: field(html, "Főtevékenység"),
|
|
171
|
+
founded: field(html, "Alapítás éve"),
|
|
172
|
+
managers: managers(html),
|
|
173
|
+
url: record.url,
|
|
174
|
+
sources: [hit.url, record.url],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Null for anything but one exact match. */
|
|
179
|
+
async function lookupHungarianCompany(
|
|
180
|
+
name: string,
|
|
181
|
+
fetchImpl: Fetch
|
|
182
|
+
): Promise<RegisteredCompany | null> {
|
|
183
|
+
const browse = session(fetchImpl);
|
|
184
|
+
const hit = await findRegistered(name, browse);
|
|
185
|
+
if (!hit) return null;
|
|
186
|
+
const record = await browse(`${NEMZETICEGTAR}${hit.slug}-c${hit.number}.html`);
|
|
187
|
+
return entryFrom(hit, record);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const hungarianRegister: CompanyRegister = {
|
|
191
|
+
country: "Hungary",
|
|
192
|
+
recognizes: isHungarianCompanyName,
|
|
193
|
+
lookup: lookupHungarianCompany,
|
|
194
|
+
};
|