edgar-cli 0.1.4 → 0.2.2

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/dist/cli.js DELETED
@@ -1,332 +0,0 @@
1
- #!/usr/bin/env node
2
- import { realpathSync } from 'node:fs';
3
- import { Command, CommanderError } from 'commander';
4
- import { fileURLToPath } from 'node:url';
5
- import { runFactsGet } from './commands/facts.js';
6
- import { runFilingsGet, runFilingsList } from './commands/filings.js';
7
- import { parseResearchProfile, runResearchAsk, runResearchAskById, runResearchSync } from './commands/research.js';
8
- import { runResolve } from './commands/resolve.js';
9
- import { buildRuntimeOptions, parseDateString, parseNonNegativeInt, parsePositiveInt, requireUserAgent } from './core/config.js';
10
- import { failureEnvelope, successEnvelope } from './core/envelope.js';
11
- import { CLIError, ErrorCode, EXIT_CODE_MAP, isCLIError } from './core/errors.js';
12
- import { shapeData } from './core/output-shape.js';
13
- import { SecClient } from './sec/client.js';
14
- class CLIAbortError extends Error {
15
- exitCode;
16
- constructor(exitCode) {
17
- super(`CLI exited with code ${exitCode}`);
18
- this.exitCode = exitCode;
19
- this.name = 'CLIAbortError';
20
- }
21
- }
22
- function defaultIo() {
23
- return {
24
- stdout: (message) => process.stdout.write(message),
25
- stderr: (message) => process.stderr.write(message),
26
- env: process.env
27
- };
28
- }
29
- function humanPrint(io, data) {
30
- io.stdout(`${JSON.stringify(data, null, 2)}\n`);
31
- }
32
- function emitSuccess(params) {
33
- const { context, io, command, result } = params;
34
- if (context.runtime.humanMode) {
35
- humanPrint(io, result.data);
36
- return;
37
- }
38
- const shaped = shapeData({
39
- data: result.data,
40
- fields: context.runtime.fields,
41
- limit: context.runtime.limit
42
- });
43
- const metaUpdates = {
44
- ...(result.metaUpdates ?? {}),
45
- ...shaped.metaUpdates
46
- };
47
- const envelope = successEnvelope({
48
- command,
49
- data: shaped.data,
50
- view: context.runtime.view,
51
- metaUpdates
52
- });
53
- io.stdout(`${JSON.stringify(envelope)}\n`);
54
- }
55
- function emitError(params) {
56
- const { command, err, runtimeView, humanMode, io } = params;
57
- if (humanMode) {
58
- io.stderr(`${err.code} ${err.message}\n`);
59
- return err.exitCode;
60
- }
61
- const envelope = failureEnvelope({
62
- command,
63
- code: err.code,
64
- message: err.message,
65
- retriable: err.retriable,
66
- view: runtimeView
67
- });
68
- io.stdout(`${JSON.stringify(envelope)}\n`);
69
- return err.exitCode;
70
- }
71
- function toCliError(err) {
72
- if (isCLIError(err)) {
73
- return err;
74
- }
75
- return new CLIError(ErrorCode.INTERNAL_ERROR, err.message || 'Unexpected error');
76
- }
77
- async function executeCommand(command, commandObj, io, handler, options) {
78
- const globalOptions = commandObj.optsWithGlobals();
79
- const runtime = buildRuntimeOptions({
80
- json: globalOptions.json,
81
- human: globalOptions.human,
82
- view: globalOptions.view,
83
- fields: globalOptions.fields,
84
- limit: globalOptions.limit,
85
- verbose: globalOptions.verbose,
86
- userAgent: globalOptions.userAgent
87
- }, io.env);
88
- try {
89
- const requiresSecIdentity = options?.requiresSecIdentity ?? true;
90
- const userAgent = requiresSecIdentity
91
- ? requireUserAgent(runtime.userAgent)
92
- : runtime.userAgent ?? 'edgar-cli local research';
93
- const secClient = new SecClient({
94
- userAgent,
95
- verbose: runtime.verbose,
96
- logger: (message) => io.stderr(`[debug] ${message}\n`)
97
- });
98
- const context = {
99
- runtime,
100
- secClient
101
- };
102
- const result = await handler(context);
103
- emitSuccess({ command, result, context, io });
104
- }
105
- catch (error) {
106
- const cliError = toCliError(error);
107
- const exitCode = emitError({
108
- command,
109
- err: cliError,
110
- runtimeView: runtime.view,
111
- humanMode: runtime.humanMode,
112
- io
113
- });
114
- throw new CLIAbortError(exitCode);
115
- }
116
- }
117
- export function buildProgram(io) {
118
- const program = new Command();
119
- program
120
- .name('edgar')
121
- .description('Agent-friendly SEC EDGAR CLI')
122
- .option('--json', 'Emit JSON envelope output (default)')
123
- .option('--human', 'Emit human-readable output')
124
- .option('--view <view>', 'Output view mode (summary|full)', 'summary')
125
- .option('--fields <fields>', 'Select specific response fields in JSON mode')
126
- .option('--limit <n>', 'Limit output rows in JSON mode')
127
- .option('--verbose', 'Enable verbose debug logs')
128
- .option('--user-agent <value>', 'SEC identity (required for network commands), e.g. "Name email@domain.com"')
129
- .showHelpAfterError(true)
130
- .exitOverride()
131
- .addHelpText('after', '\nSEC identity is required for network commands.\nSet --user-agent or EDGAR_USER_AGENT.')
132
- .configureOutput({
133
- writeOut: (message) => io.stdout(message),
134
- writeErr: (message) => io.stderr(message)
135
- });
136
- program
137
- .command('resolve')
138
- .description('Resolve ticker/CIK to canonical SEC identity fields')
139
- .argument('<id>', 'Ticker (AAPL) or CIK (320193 / 0000320193)')
140
- .action(async function actionResolve(id) {
141
- await executeCommand('resolve', this, io, async (context) => runResolve(id, context));
142
- });
143
- const filings = program.command('filings').description('Query filing metadata and filing documents');
144
- filings
145
- .command('list')
146
- .requiredOption('--id <id>', 'Ticker or CIK')
147
- .option('--form <form>', 'SEC form type, e.g. 10-K')
148
- .option('--from <yyyy-mm-dd>', 'Lower filing-date bound')
149
- .option('--to <yyyy-mm-dd>', 'Upper filing-date bound')
150
- .option('--query-limit <n>', 'Limit rows before envelope shaping')
151
- .option('--offset <n>', 'Offset rows before query-limit slicing', '0')
152
- .action(async function actionFilingsList(options) {
153
- const from = options.from ? parseDateString(options.from, '--from') : undefined;
154
- const to = options.to ? parseDateString(options.to, '--to') : undefined;
155
- const queryLimit = options.queryLimit === undefined
156
- ? undefined
157
- : parsePositiveInt(options.queryLimit, '--query-limit');
158
- const offset = parseNonNegativeInt(options.offset, '--offset');
159
- await executeCommand('filings list', this, io, async (context) => runFilingsList({
160
- id: options.id,
161
- form: options.form,
162
- from,
163
- to,
164
- queryLimit,
165
- offset
166
- }, context));
167
- });
168
- filings
169
- .command('get')
170
- .requiredOption('--id <id>', 'Ticker or CIK')
171
- .requiredOption('--accession <accession>', 'Accession number: XXXXXXXXXX-XX-XXXXXX')
172
- .option('--format <format>', 'url|html|text|markdown', 'url')
173
- .action(async function actionFilingsGet(options) {
174
- const format = options.format;
175
- if (!['url', 'html', 'text', 'markdown'].includes(format)) {
176
- throw new CLIAbortError(emitError({
177
- command: 'filings get',
178
- err: new CLIError(ErrorCode.VALIDATION_ERROR, '--format must be one of url|html|text|markdown'),
179
- runtimeView: 'summary',
180
- humanMode: false,
181
- io
182
- }));
183
- }
184
- await executeCommand('filings get', this, io, async (context) => runFilingsGet({
185
- id: options.id,
186
- accession: options.accession,
187
- format: format
188
- }, context));
189
- });
190
- const facts = program.command('facts').description('Query SEC company facts (XBRL)');
191
- facts
192
- .command('get')
193
- .requiredOption('--id <id>', 'Ticker or CIK')
194
- .option('--taxonomy <taxonomy>', 'us-gaap|dei')
195
- .option('--concept <concept>', 'Concept name, e.g. Revenues')
196
- .option('--unit <unit>', 'Unit key, e.g. USD')
197
- .option('--latest', 'Return only latest point per unit')
198
- .action(async function actionFactsGet(options) {
199
- const taxonomyValue = options.taxonomy;
200
- if (taxonomyValue && !['us-gaap', 'dei'].includes(taxonomyValue)) {
201
- throw new CLIAbortError(emitError({
202
- command: 'facts get',
203
- err: new CLIError(ErrorCode.VALIDATION_ERROR, '--taxonomy must be us-gaap or dei'),
204
- runtimeView: 'summary',
205
- humanMode: false,
206
- io
207
- }));
208
- }
209
- await executeCommand('facts get', this, io, async (context) => runFactsGet({
210
- id: options.id,
211
- taxonomy: taxonomyValue,
212
- concept: options.concept,
213
- unit: options.unit,
214
- latest: Boolean(options.latest)
215
- }, context));
216
- });
217
- const research = program
218
- .command('research')
219
- .description('Run deterministic research workflows over explicit docs or cached filing profiles');
220
- research
221
- .command('sync')
222
- .description('Cache a deterministic research corpus for a company/profile')
223
- .requiredOption('--id <id>', 'Ticker or CIK')
224
- .option('--profile <profile>', 'core|events|financials', 'core')
225
- .option('--cache-dir <path>', 'Override cache directory')
226
- .option('--refresh', 'Force refetch even when cached docs exist')
227
- .action(async function actionResearchSync(options) {
228
- const profile = parseResearchProfile(options.profile);
229
- await executeCommand('research sync', this, io, async (context) => runResearchSync({
230
- id: options.id,
231
- profile,
232
- cacheDir: options.cacheDir,
233
- refresh: Boolean(options.refresh)
234
- }, context), { requiresSecIdentity: true });
235
- });
236
- research
237
- .command('ask')
238
- .description('Query explicitly provided local docs, or a cached company profile corpus when --id is used')
239
- .argument('<query>', 'Natural language query')
240
- .option('--id <id>', 'Ticker or CIK for cached/profile-based research')
241
- .option('--profile <profile>', 'core|events|financials (used with --id)', 'core')
242
- .option('--form <form>', 'SEC form filter for scoped filing selection with --id, e.g. 10-Q')
243
- .option('--latest <n>', 'With --id, limit to latest N filings after filters')
244
- .option('--cache-dir <path>', 'Override cache directory')
245
- .option('--refresh', 'With --id, force refetch of filings before querying')
246
- .option('--doc <path>', 'Path to a local document (repeatable)', collectValues, [])
247
- .option('--manifest <path>', 'Path to JSON manifest: either ["doc1", ...] or {"docs": ["doc1", ...]}')
248
- .option('--top-k <n>', 'Maximum number of chunks to return', '8')
249
- .option('--chunk-lines <n>', 'Number of lines per retrieval chunk', '40')
250
- .option('--chunk-overlap <n>', 'Line overlap between retrieval chunks', '10')
251
- .action(async function actionResearchAsk(query, options) {
252
- const topK = parsePositiveInt(options.topK, '--top-k');
253
- const chunkLines = parsePositiveInt(options.chunkLines, '--chunk-lines');
254
- const chunkOverlap = parseNonNegativeInt(options.chunkOverlap, '--chunk-overlap');
255
- const latest = options.latest === undefined
256
- ? undefined
257
- : parsePositiveInt(options.latest, '--latest');
258
- if (!options.id && (options.form || latest !== undefined)) {
259
- throw new CLIAbortError(emitError({
260
- command: 'research ask',
261
- err: new CLIError(ErrorCode.VALIDATION_ERROR, '--form and --latest require --id'),
262
- runtimeView: 'summary',
263
- humanMode: false,
264
- io
265
- }));
266
- }
267
- const requiresSecIdentity = Boolean(options.id);
268
- const profile = parseResearchProfile(options.profile);
269
- await executeCommand('research ask', this, io, async (context) => options.id
270
- ? runResearchAskById({
271
- id: options.id,
272
- query,
273
- profile,
274
- scope: {
275
- form: options.form,
276
- latest
277
- },
278
- cacheDir: options.cacheDir,
279
- refresh: Boolean(options.refresh),
280
- topK,
281
- chunkLines,
282
- chunkOverlap
283
- }, context)
284
- : runResearchAsk({
285
- query,
286
- docs: options.doc ?? [],
287
- manifestPath: options.manifest,
288
- topK,
289
- chunkLines,
290
- chunkOverlap
291
- }, context), { requiresSecIdentity });
292
- });
293
- return program;
294
- }
295
- function collectValues(value, previous) {
296
- return [...previous, value];
297
- }
298
- export async function runCli(argv, io = defaultIo()) {
299
- const program = buildProgram(io);
300
- try {
301
- await program.parseAsync(argv, { from: 'user' });
302
- return 0;
303
- }
304
- catch (error) {
305
- if (error instanceof CLIAbortError) {
306
- return error.exitCode;
307
- }
308
- if (error instanceof CommanderError) {
309
- return error.exitCode;
310
- }
311
- const cliError = toCliError(error);
312
- io.stderr(`${cliError.code} ${cliError.message}\n`);
313
- return EXIT_CODE_MAP[cliError.code] ?? 10;
314
- }
315
- }
316
- function isDirectExecution() {
317
- const argvPath = process.argv[1];
318
- if (!argvPath) {
319
- return false;
320
- }
321
- try {
322
- return realpathSync(argvPath) === realpathSync(fileURLToPath(import.meta.url));
323
- }
324
- catch {
325
- return false;
326
- }
327
- }
328
- if (isDirectExecution()) {
329
- runCli(process.argv.slice(2)).then((exitCode) => {
330
- process.exit(exitCode);
331
- });
332
- }
@@ -1,8 +0,0 @@
1
- import { CommandContext, CommandResult } from '../core/runtime.js';
2
- export declare function runFactsGet(params: {
3
- id: string;
4
- taxonomy?: 'us-gaap' | 'dei';
5
- concept?: string;
6
- unit?: string;
7
- latest?: boolean;
8
- }, context: CommandContext): Promise<CommandResult>;
@@ -1,125 +0,0 @@
1
- import { CLIError, ErrorCode } from '../core/errors.js';
2
- import { companyFactsUrl } from '../sec/endpoints.js';
3
- import { resolveEntity } from '../sec/ticker-map.js';
4
- function buildConceptSummary(taxonomyFacts) {
5
- return Object.entries(taxonomyFacts)
6
- .map(([concept, payload]) => {
7
- const units = Object.keys(payload.units ?? {});
8
- return {
9
- concept,
10
- label: payload.label ?? null,
11
- unit_count: units.length,
12
- units
13
- };
14
- })
15
- .sort((a, b) => a.concept.localeCompare(b.concept));
16
- }
17
- function pickLatest(points) {
18
- if (points.length === 0) {
19
- return null;
20
- }
21
- const sorted = [...points].sort((a, b) => {
22
- const aKey = a.filed ?? a.end ?? '';
23
- const bKey = b.filed ?? b.end ?? '';
24
- return bKey.localeCompare(aKey);
25
- });
26
- return sorted[0] ?? null;
27
- }
28
- function selectTaxonomy(allFacts, concept, taxonomy) {
29
- if (taxonomy) {
30
- if (!allFacts[taxonomy]) {
31
- throw new CLIError(ErrorCode.NOT_FOUND, `Taxonomy ${taxonomy} not found`);
32
- }
33
- return taxonomy;
34
- }
35
- const preferred = ['us-gaap', 'dei'];
36
- for (const tax of preferred) {
37
- if (allFacts[tax]?.[concept]) {
38
- return tax;
39
- }
40
- }
41
- const anyTaxonomy = Object.keys(allFacts).find((tax) => Boolean(allFacts[tax]?.[concept]));
42
- if (anyTaxonomy) {
43
- return anyTaxonomy;
44
- }
45
- throw new CLIError(ErrorCode.NOT_FOUND, `Concept ${concept} not found in company facts`);
46
- }
47
- export async function runFactsGet(params, context) {
48
- const entity = await resolveEntity(params.id, context.secClient, { strictMapMatch: false });
49
- const payload = await context.secClient.fetchSecJson(companyFactsUrl(entity.cik));
50
- const allFacts = payload.facts ?? {};
51
- if (!params.concept) {
52
- if (params.taxonomy) {
53
- const taxonomyFacts = allFacts[params.taxonomy];
54
- if (!taxonomyFacts) {
55
- throw new CLIError(ErrorCode.NOT_FOUND, `Taxonomy ${params.taxonomy} not found`);
56
- }
57
- return {
58
- data: {
59
- cik: entity.cik,
60
- entityName: payload.entityName,
61
- taxonomy: params.taxonomy,
62
- concept_count: Object.keys(taxonomyFacts).length,
63
- concepts: buildConceptSummary(taxonomyFacts)
64
- }
65
- };
66
- }
67
- const taxonomySummary = Object.fromEntries(Object.entries(allFacts).map(([taxonomy, taxonomyFacts]) => [
68
- taxonomy,
69
- {
70
- concept_count: Object.keys(taxonomyFacts).length
71
- }
72
- ]));
73
- return {
74
- data: {
75
- cik: entity.cik,
76
- entityName: payload.entityName,
77
- taxonomies: taxonomySummary
78
- }
79
- };
80
- }
81
- const concept = params.concept;
82
- const taxonomy = selectTaxonomy(allFacts, concept, params.taxonomy);
83
- const conceptData = allFacts[taxonomy]?.[concept];
84
- if (!conceptData) {
85
- throw new CLIError(ErrorCode.NOT_FOUND, `Concept ${concept} not found in taxonomy ${taxonomy}`);
86
- }
87
- const rawUnits = conceptData.units ?? {};
88
- let selectedUnits;
89
- if (params.unit) {
90
- if (!rawUnits[params.unit]) {
91
- throw new CLIError(ErrorCode.NOT_FOUND, `Unit ${params.unit} not found for ${taxonomy}:${concept}`);
92
- }
93
- selectedUnits = {
94
- [params.unit]: rawUnits[params.unit]
95
- };
96
- }
97
- else {
98
- selectedUnits = rawUnits;
99
- }
100
- if (params.latest) {
101
- const latestByUnit = Object.fromEntries(Object.entries(selectedUnits).map(([unitName, points]) => [unitName, pickLatest(points)]));
102
- return {
103
- data: {
104
- cik: entity.cik,
105
- entityName: payload.entityName,
106
- taxonomy,
107
- concept,
108
- label: conceptData.label ?? null,
109
- description: conceptData.description ?? null,
110
- latest: latestByUnit
111
- }
112
- };
113
- }
114
- return {
115
- data: {
116
- cik: entity.cik,
117
- entityName: payload.entityName,
118
- taxonomy,
119
- concept,
120
- label: conceptData.label ?? null,
121
- description: conceptData.description ?? null,
122
- units: selectedUnits
123
- }
124
- };
125
- }
@@ -1,14 +0,0 @@
1
- import { CommandContext, CommandResult } from '../core/runtime.js';
2
- export declare function runFilingsList(params: {
3
- id: string;
4
- form?: string;
5
- from?: string;
6
- to?: string;
7
- queryLimit?: number;
8
- offset?: number;
9
- }, context: CommandContext): Promise<CommandResult>;
10
- export declare function runFilingsGet(params: {
11
- id: string;
12
- accession: string;
13
- format: 'url' | 'html' | 'text' | 'markdown';
14
- }, context: CommandContext): Promise<CommandResult>;
@@ -1,198 +0,0 @@
1
- import TurndownService from 'turndown';
2
- import { gfm } from '@joplin/turndown-plugin-gfm';
3
- import { CLIError, ErrorCode } from '../core/errors.js';
4
- import { filingDocumentUrl, submissionsUrl } from '../sec/endpoints.js';
5
- import { dateInRange, normalizeAccession } from '../sec/normalizers.js';
6
- import { resolveEntity } from '../sec/ticker-map.js';
7
- function buildMarkdownConverter() {
8
- const service = new TurndownService({
9
- headingStyle: 'atx',
10
- hr: '---',
11
- bulletListMarker: '-',
12
- codeBlockStyle: 'fenced',
13
- fence: '```',
14
- emDelimiter: '*',
15
- strongDelimiter: '**',
16
- linkStyle: 'inlined'
17
- });
18
- service.use(gfm);
19
- service.remove(['script', 'style', 'noscript', 'iframe', 'canvas']);
20
- return service;
21
- }
22
- const markdownConverter = buildMarkdownConverter();
23
- function stripInlineXbrlHeaders(content) {
24
- return content
25
- .replace(/<ix:header[\s\S]*?<\/ix:header>/gi, '')
26
- .replace(/<ix:hidden[\s\S]*?<\/ix:hidden>/gi, '')
27
- .replace(/<ix:resources[\s\S]*?<\/ix:resources>/gi, '');
28
- }
29
- function splitMarkdownTableCells(line) {
30
- const trimmed = line.trim();
31
- const withoutLeadingPipe = trimmed.startsWith('|') ? trimmed.slice(1) : trimmed;
32
- const withoutTrailingPipe = withoutLeadingPipe.endsWith('|')
33
- ? withoutLeadingPipe.slice(0, -1)
34
- : withoutLeadingPipe;
35
- return withoutTrailingPipe.split('|').map((cell) => cell.trim());
36
- }
37
- function isMarkdownTableSeparatorLine(line) {
38
- const cells = splitMarkdownTableCells(line);
39
- if (cells.length === 0) {
40
- return false;
41
- }
42
- return cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s+/g, '')));
43
- }
44
- function collapseLayoutTables(markdown) {
45
- const lines = markdown.split('\n');
46
- const output = [];
47
- for (let idx = 0; idx < lines.length; idx += 1) {
48
- const line = lines[idx];
49
- if (!line.trimStart().startsWith('|')) {
50
- output.push(line);
51
- continue;
52
- }
53
- const tableBlock = [line];
54
- while (idx + 1 < lines.length && lines[idx + 1].trimStart().startsWith('|')) {
55
- idx += 1;
56
- tableBlock.push(lines[idx]);
57
- }
58
- const hasSeparator = tableBlock.some(isMarkdownTableSeparatorLine);
59
- if (!hasSeparator) {
60
- output.push(...tableBlock);
61
- continue;
62
- }
63
- const dataRows = tableBlock.filter((row) => !isMarkdownTableSeparatorLine(row));
64
- const nonEmptyCellCounts = dataRows.map((row) => splitMarkdownTableCells(row).filter((cell) => cell.length > 0).length);
65
- const maxNonEmptyCells = Math.max(...nonEmptyCellCounts, 0);
66
- const avgNonEmptyCells = nonEmptyCellCounts.reduce((sum, count) => sum + count, 0) /
67
- Math.max(nonEmptyCellCounts.length, 1);
68
- const isLayoutTable = maxNonEmptyCells <= 1 || avgNonEmptyCells <= 1.2;
69
- if (!isLayoutTable) {
70
- output.push(...tableBlock);
71
- continue;
72
- }
73
- const flattenedRows = dataRows
74
- .map((row) => splitMarkdownTableCells(row).filter((cell) => cell.length > 0).join(' '))
75
- .map((row) => row.replace(/\s+/g, ' ').trim())
76
- .filter((row) => row.length > 0);
77
- if (flattenedRows.length > 0) {
78
- output.push(...flattenedRows, '');
79
- }
80
- }
81
- return output.join('\n').replace(/\n{3,}/g, '\n\n').trim();
82
- }
83
- function zipRecentFilings(cik, recent) {
84
- if (!recent) {
85
- return [];
86
- }
87
- const accessionNumbers = recent.accessionNumber ?? [];
88
- const forms = recent.form ?? [];
89
- const filingDates = recent.filingDate ?? [];
90
- const reportDates = recent.reportDate ?? [];
91
- const primaryDocuments = recent.primaryDocument ?? [];
92
- const rowCount = accessionNumbers.length;
93
- const rows = [];
94
- for (let idx = 0; idx < rowCount; idx += 1) {
95
- const accession = accessionNumbers[idx];
96
- if (!accession) {
97
- continue;
98
- }
99
- const primaryDocument = primaryDocuments[idx] ?? null;
100
- const filingUrl = primaryDocument && primaryDocument.length > 0
101
- ? filingDocumentUrl({
102
- cik,
103
- accession,
104
- primaryDocument
105
- })
106
- : null;
107
- rows.push({
108
- accession,
109
- form: forms[idx] ?? null,
110
- filingDate: filingDates[idx] ?? null,
111
- reportDate: reportDates[idx] ?? null,
112
- primaryDocument,
113
- filingUrl
114
- });
115
- }
116
- return rows;
117
- }
118
- function extractMarkdownFromHtml(content) {
119
- const sanitizedHtml = stripInlineXbrlHeaders(content);
120
- const markdown = markdownConverter
121
- .turndown(sanitizedHtml)
122
- .replace(/\u00a0/g, ' ')
123
- .replace(/\r/g, '')
124
- .replace(/[ \t]+\n/g, '\n')
125
- .replace(/\n[ \t]+/g, '\n')
126
- .replace(/\n{3,}/g, '\n\n')
127
- .trim();
128
- return collapseLayoutTables(markdown);
129
- }
130
- export async function runFilingsList(params, context) {
131
- const entity = await resolveEntity(params.id, context.secClient, { strictMapMatch: false });
132
- const submissions = await context.secClient.fetchSecJson(submissionsUrl(entity.cik));
133
- const rows = zipRecentFilings(entity.cik, submissions.filings?.recent);
134
- const normalizedForm = params.form?.toUpperCase();
135
- const filteredRows = rows.filter((row) => {
136
- if (normalizedForm && (row.form ?? '').toUpperCase() !== normalizedForm) {
137
- return false;
138
- }
139
- if (!row.filingDate) {
140
- return !params.from && !params.to;
141
- }
142
- return dateInRange(row.filingDate, params.from, params.to);
143
- });
144
- const offset = params.offset ?? 0;
145
- const queryLimit = params.queryLimit ?? filteredRows.length;
146
- const pagedRows = filteredRows.slice(offset, offset + queryLimit);
147
- return {
148
- data: pagedRows,
149
- metaUpdates: {
150
- query_total_count: filteredRows.length,
151
- query_returned_count: pagedRows.length,
152
- query_truncated: offset + pagedRows.length < filteredRows.length,
153
- query_offset: offset
154
- }
155
- };
156
- }
157
- export async function runFilingsGet(params, context) {
158
- const accession = normalizeAccession(params.accession);
159
- const entity = await resolveEntity(params.id, context.secClient, { strictMapMatch: false });
160
- const submissions = await context.secClient.fetchSecJson(submissionsUrl(entity.cik));
161
- const rows = zipRecentFilings(entity.cik, submissions.filings?.recent);
162
- const match = rows.find((row) => row.accession === accession);
163
- if (!match) {
164
- throw new CLIError(ErrorCode.NOT_FOUND, `Accession ${accession} not found in recent submissions for ${params.id}`);
165
- }
166
- if (!match.primaryDocument || !match.filingUrl) {
167
- throw new CLIError(ErrorCode.NOT_FOUND, `No primary document found for accession ${accession}`);
168
- }
169
- if (params.format === 'url') {
170
- return {
171
- data: {
172
- accession: match.accession,
173
- form: match.form,
174
- filingDate: match.filingDate,
175
- reportDate: match.reportDate,
176
- primaryDocument: match.primaryDocument,
177
- url: match.filingUrl
178
- }
179
- };
180
- }
181
- const content = await context.secClient.fetchSecText(match.filingUrl);
182
- if (params.format === 'html') {
183
- return {
184
- data: {
185
- accession: match.accession,
186
- url: match.filingUrl,
187
- content
188
- }
189
- };
190
- }
191
- return {
192
- data: {
193
- accession: match.accession,
194
- url: match.filingUrl,
195
- content: extractMarkdownFromHtml(content)
196
- }
197
- };
198
- }