edgar-cli 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ian Finlay
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,103 @@
1
+ # edgar-cli
2
+
3
+ Agent-friendly SEC EDGAR CLI for filings and company facts.
4
+
5
+ ## Features
6
+
7
+ - `npx`-friendly Node/TypeScript package (no Python runtime needed)
8
+ - JSON envelope output by default for stable automation
9
+ - Strict SEC identity enforcement (`--user-agent` or `EDGAR_USER_AGENT`)
10
+ - Core commands:
11
+ - `resolve`
12
+ - `filings list`
13
+ - `filings get`
14
+ - `facts get`
15
+
16
+ ## Install / Run
17
+
18
+ ```bash
19
+ npx edgar-cli --help
20
+ ```
21
+
22
+ Local development:
23
+
24
+ ```bash
25
+ npm install
26
+ npm run build
27
+ node dist/cli.js --help
28
+ ```
29
+
30
+ ## SEC Identity Requirement
31
+
32
+ SEC endpoints require declared automated access identity.
33
+
34
+ Use either:
35
+
36
+ ```bash
37
+ export EDGAR_USER_AGENT="Your Name your.email@example.com"
38
+ ```
39
+
40
+ Or pass per command:
41
+
42
+ ```bash
43
+ npx edgar-cli --user-agent "Your Name your.email@example.com" resolve AAPL
44
+ ```
45
+
46
+ If identity is missing, commands fail with `IDENTITY_REQUIRED`.
47
+
48
+ ## Examples
49
+
50
+ ```bash
51
+ # Resolve ticker -> canonical SEC identity mapping
52
+ npx edgar-cli --user-agent "Your Name your.email@example.com" resolve AAPL
53
+
54
+ # List recent 10-K filings
55
+ npx edgar-cli --user-agent "Your Name your.email@example.com" filings list --id AAPL --form 10-K --query-limit 5
56
+
57
+ # Get filing document URL by accession
58
+ npx edgar-cli --user-agent "Your Name your.email@example.com" filings get --id AAPL --accession 0000320193-26-000006 --format url
59
+
60
+ # Get concept data (latest per unit)
61
+ npx edgar-cli --user-agent "Your Name your.email@example.com" facts get --id AAPL --taxonomy us-gaap --concept Revenues --latest
62
+ ```
63
+
64
+ ## Output Contract (default)
65
+
66
+ All JSON-mode commands emit:
67
+
68
+ ```json
69
+ {
70
+ "ok": true,
71
+ "command": "resolve",
72
+ "provider": "sec",
73
+ "data": {},
74
+ "error": null,
75
+ "meta": {
76
+ "timestamp": "2026-02-11T00:00:00Z",
77
+ "output_schema": "v1",
78
+ "view": "summary"
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## Compliance Notes
84
+
85
+ - This CLI targets SEC-hosted endpoints only in V0.
86
+ - Respect SEC fair-access guidance and use a valid identity in your user-agent.
87
+
88
+ References:
89
+
90
+ - [SEC Developer](https://www.sec.gov/developer)
91
+ - [SEC Webmaster FAQ: code support](https://www.sec.gov/os/webmaster-faq#code-support)
92
+
93
+ ## Security
94
+
95
+ See [`SECURITY.md`](SECURITY.md) for vulnerability reporting guidance.
96
+
97
+ ## Development
98
+
99
+ ```bash
100
+ npm run typecheck
101
+ npm run test
102
+ npm run build
103
+ ```
package/dist/cli.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ interface CliIo {
4
+ stdout: (message: string) => void;
5
+ stderr: (message: string) => void;
6
+ env: NodeJS.ProcessEnv;
7
+ }
8
+ export declare function buildProgram(io: CliIo): Command;
9
+ export declare function runCli(argv: string[], io?: CliIo): Promise<number>;
10
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+ import { Command, CommanderError } from 'commander';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { runFactsGet } from './commands/facts.js';
5
+ import { runFilingsGet, runFilingsList } from './commands/filings.js';
6
+ import { runResolve } from './commands/resolve.js';
7
+ import { buildRuntimeOptions, parseDateString, parseNonNegativeInt, parsePositiveInt, requireUserAgent } from './core/config.js';
8
+ import { failureEnvelope, successEnvelope } from './core/envelope.js';
9
+ import { CLIError, ErrorCode, EXIT_CODE_MAP, isCLIError } from './core/errors.js';
10
+ import { shapeData } from './core/output-shape.js';
11
+ import { SecClient } from './sec/client.js';
12
+ class CLIAbortError extends Error {
13
+ exitCode;
14
+ constructor(exitCode) {
15
+ super(`CLI exited with code ${exitCode}`);
16
+ this.exitCode = exitCode;
17
+ this.name = 'CLIAbortError';
18
+ }
19
+ }
20
+ function defaultIo() {
21
+ return {
22
+ stdout: (message) => process.stdout.write(message),
23
+ stderr: (message) => process.stderr.write(message),
24
+ env: process.env
25
+ };
26
+ }
27
+ function humanPrint(io, data) {
28
+ io.stdout(`${JSON.stringify(data, null, 2)}\n`);
29
+ }
30
+ function emitSuccess(params) {
31
+ const { context, io, command, result } = params;
32
+ if (context.runtime.humanMode) {
33
+ humanPrint(io, result.data);
34
+ return;
35
+ }
36
+ const shaped = shapeData({
37
+ data: result.data,
38
+ fields: context.runtime.fields,
39
+ limit: context.runtime.limit
40
+ });
41
+ const metaUpdates = {
42
+ ...(result.metaUpdates ?? {}),
43
+ ...shaped.metaUpdates
44
+ };
45
+ const envelope = successEnvelope({
46
+ command,
47
+ data: shaped.data,
48
+ view: context.runtime.view,
49
+ metaUpdates
50
+ });
51
+ io.stdout(`${JSON.stringify(envelope)}\n`);
52
+ }
53
+ function emitError(params) {
54
+ const { command, err, runtimeView, humanMode, io } = params;
55
+ if (humanMode) {
56
+ io.stderr(`${err.code} ${err.message}\n`);
57
+ return err.exitCode;
58
+ }
59
+ const envelope = failureEnvelope({
60
+ command,
61
+ code: err.code,
62
+ message: err.message,
63
+ retriable: err.retriable,
64
+ view: runtimeView
65
+ });
66
+ io.stdout(`${JSON.stringify(envelope)}\n`);
67
+ return err.exitCode;
68
+ }
69
+ function toCliError(err) {
70
+ if (isCLIError(err)) {
71
+ return err;
72
+ }
73
+ return new CLIError(ErrorCode.INTERNAL_ERROR, err.message || 'Unexpected error');
74
+ }
75
+ async function executeCommand(command, commandObj, io, handler) {
76
+ const globalOptions = commandObj.optsWithGlobals();
77
+ const runtime = buildRuntimeOptions({
78
+ json: globalOptions.json,
79
+ human: globalOptions.human,
80
+ view: globalOptions.view,
81
+ fields: globalOptions.fields,
82
+ limit: globalOptions.limit,
83
+ verbose: globalOptions.verbose,
84
+ userAgent: globalOptions.userAgent
85
+ }, io.env);
86
+ try {
87
+ const userAgent = requireUserAgent(runtime.userAgent);
88
+ const secClient = new SecClient({
89
+ userAgent,
90
+ verbose: runtime.verbose,
91
+ logger: (message) => io.stderr(`[debug] ${message}\n`)
92
+ });
93
+ const context = {
94
+ runtime,
95
+ secClient
96
+ };
97
+ const result = await handler(context);
98
+ emitSuccess({ command, result, context, io });
99
+ }
100
+ catch (error) {
101
+ const cliError = toCliError(error);
102
+ const exitCode = emitError({
103
+ command,
104
+ err: cliError,
105
+ runtimeView: runtime.view,
106
+ humanMode: runtime.humanMode,
107
+ io
108
+ });
109
+ throw new CLIAbortError(exitCode);
110
+ }
111
+ }
112
+ export function buildProgram(io) {
113
+ const program = new Command();
114
+ program
115
+ .name('edgar')
116
+ .description('Agent-friendly SEC EDGAR CLI')
117
+ .option('--json', 'Emit JSON envelope output (default)')
118
+ .option('--human', 'Emit human-readable output')
119
+ .option('--view <view>', 'Output view mode (summary|full)', 'summary')
120
+ .option('--fields <fields>', 'Select specific response fields in JSON mode')
121
+ .option('--limit <n>', 'Limit output rows in JSON mode')
122
+ .option('--verbose', 'Enable verbose debug logs')
123
+ .option('--user-agent <value>', 'SEC identity (required for network commands), e.g. "Name email@domain.com"')
124
+ .showHelpAfterError(true)
125
+ .exitOverride()
126
+ .addHelpText('after', '\nSEC identity is required for network commands.\nSet --user-agent or EDGAR_USER_AGENT.')
127
+ .configureOutput({
128
+ writeOut: (message) => io.stdout(message),
129
+ writeErr: (message) => io.stderr(message)
130
+ });
131
+ program
132
+ .command('resolve')
133
+ .description('Resolve ticker/CIK to canonical SEC identity fields')
134
+ .argument('<id>', 'Ticker (AAPL) or CIK (320193 / 0000320193)')
135
+ .action(async function actionResolve(id) {
136
+ await executeCommand('resolve', this, io, async (context) => runResolve(id, context));
137
+ });
138
+ const filings = program.command('filings').description('Query filing metadata and filing documents');
139
+ filings
140
+ .command('list')
141
+ .requiredOption('--id <id>', 'Ticker or CIK')
142
+ .option('--form <form>', 'SEC form type, e.g. 10-K')
143
+ .option('--from <yyyy-mm-dd>', 'Lower filing-date bound')
144
+ .option('--to <yyyy-mm-dd>', 'Upper filing-date bound')
145
+ .option('--query-limit <n>', 'Limit rows before envelope shaping')
146
+ .option('--offset <n>', 'Offset rows before query-limit slicing', '0')
147
+ .action(async function actionFilingsList(options) {
148
+ const from = options.from ? parseDateString(options.from, '--from') : undefined;
149
+ const to = options.to ? parseDateString(options.to, '--to') : undefined;
150
+ const queryLimit = options.queryLimit === undefined
151
+ ? undefined
152
+ : parsePositiveInt(options.queryLimit, '--query-limit');
153
+ const offset = parseNonNegativeInt(options.offset, '--offset');
154
+ await executeCommand('filings list', this, io, async (context) => runFilingsList({
155
+ id: options.id,
156
+ form: options.form,
157
+ from,
158
+ to,
159
+ queryLimit,
160
+ offset
161
+ }, context));
162
+ });
163
+ filings
164
+ .command('get')
165
+ .requiredOption('--id <id>', 'Ticker or CIK')
166
+ .requiredOption('--accession <accession>', 'Accession number: XXXXXXXXXX-XX-XXXXXX')
167
+ .option('--format <format>', 'url|html|text', 'url')
168
+ .action(async function actionFilingsGet(options) {
169
+ const format = options.format;
170
+ if (!['url', 'html', 'text'].includes(format)) {
171
+ throw new CLIAbortError(emitError({
172
+ command: 'filings get',
173
+ err: new CLIError(ErrorCode.VALIDATION_ERROR, '--format must be one of url|html|text'),
174
+ runtimeView: 'summary',
175
+ humanMode: false,
176
+ io
177
+ }));
178
+ }
179
+ await executeCommand('filings get', this, io, async (context) => runFilingsGet({
180
+ id: options.id,
181
+ accession: options.accession,
182
+ format: format
183
+ }, context));
184
+ });
185
+ const facts = program.command('facts').description('Query SEC company facts (XBRL)');
186
+ facts
187
+ .command('get')
188
+ .requiredOption('--id <id>', 'Ticker or CIK')
189
+ .option('--taxonomy <taxonomy>', 'us-gaap|dei')
190
+ .option('--concept <concept>', 'Concept name, e.g. Revenues')
191
+ .option('--unit <unit>', 'Unit key, e.g. USD')
192
+ .option('--latest', 'Return only latest point per unit')
193
+ .action(async function actionFactsGet(options) {
194
+ const taxonomyValue = options.taxonomy;
195
+ if (taxonomyValue && !['us-gaap', 'dei'].includes(taxonomyValue)) {
196
+ throw new CLIAbortError(emitError({
197
+ command: 'facts get',
198
+ err: new CLIError(ErrorCode.VALIDATION_ERROR, '--taxonomy must be us-gaap or dei'),
199
+ runtimeView: 'summary',
200
+ humanMode: false,
201
+ io
202
+ }));
203
+ }
204
+ await executeCommand('facts get', this, io, async (context) => runFactsGet({
205
+ id: options.id,
206
+ taxonomy: taxonomyValue,
207
+ concept: options.concept,
208
+ unit: options.unit,
209
+ latest: Boolean(options.latest)
210
+ }, context));
211
+ });
212
+ return program;
213
+ }
214
+ export async function runCli(argv, io = defaultIo()) {
215
+ const program = buildProgram(io);
216
+ try {
217
+ await program.parseAsync(argv, { from: 'user' });
218
+ return 0;
219
+ }
220
+ catch (error) {
221
+ if (error instanceof CLIAbortError) {
222
+ return error.exitCode;
223
+ }
224
+ if (error instanceof CommanderError) {
225
+ return error.exitCode;
226
+ }
227
+ const cliError = toCliError(error);
228
+ io.stderr(`${cliError.code} ${cliError.message}\n`);
229
+ return EXIT_CODE_MAP[cliError.code] ?? 10;
230
+ }
231
+ }
232
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
233
+ runCli(process.argv.slice(2)).then((exitCode) => {
234
+ process.exit(exitCode);
235
+ });
236
+ }
@@ -0,0 +1,8 @@
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>;
@@ -0,0 +1,125 @@
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
+ }
@@ -0,0 +1,14 @@
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';
14
+ }, context: CommandContext): Promise<CommandResult>;
@@ -0,0 +1,113 @@
1
+ import * as cheerio from 'cheerio';
2
+ import { CLIError, ErrorCode } from '../core/errors.js';
3
+ import { filingDocumentUrl, submissionsUrl } from '../sec/endpoints.js';
4
+ import { dateInRange, normalizeAccession } from '../sec/normalizers.js';
5
+ import { resolveEntity } from '../sec/ticker-map.js';
6
+ function zipRecentFilings(cik, recent) {
7
+ if (!recent) {
8
+ return [];
9
+ }
10
+ const accessionNumbers = recent.accessionNumber ?? [];
11
+ const forms = recent.form ?? [];
12
+ const filingDates = recent.filingDate ?? [];
13
+ const reportDates = recent.reportDate ?? [];
14
+ const primaryDocuments = recent.primaryDocument ?? [];
15
+ const rowCount = accessionNumbers.length;
16
+ const rows = [];
17
+ for (let idx = 0; idx < rowCount; idx += 1) {
18
+ const accession = accessionNumbers[idx];
19
+ if (!accession) {
20
+ continue;
21
+ }
22
+ const primaryDocument = primaryDocuments[idx] ?? null;
23
+ const filingUrl = primaryDocument && primaryDocument.length > 0
24
+ ? filingDocumentUrl({
25
+ cik,
26
+ accession,
27
+ primaryDocument
28
+ })
29
+ : null;
30
+ rows.push({
31
+ accession,
32
+ form: forms[idx] ?? null,
33
+ filingDate: filingDates[idx] ?? null,
34
+ reportDate: reportDates[idx] ?? null,
35
+ primaryDocument,
36
+ filingUrl
37
+ });
38
+ }
39
+ return rows;
40
+ }
41
+ function extractTextFromHtml(content) {
42
+ const $ = cheerio.load(content);
43
+ return $.text().replace(/\s+/g, ' ').trim();
44
+ }
45
+ export async function runFilingsList(params, context) {
46
+ const entity = await resolveEntity(params.id, context.secClient, { strictMapMatch: false });
47
+ const submissions = await context.secClient.fetchSecJson(submissionsUrl(entity.cik));
48
+ const rows = zipRecentFilings(entity.cik, submissions.filings?.recent);
49
+ const normalizedForm = params.form?.toUpperCase();
50
+ const filteredRows = rows.filter((row) => {
51
+ if (normalizedForm && (row.form ?? '').toUpperCase() !== normalizedForm) {
52
+ return false;
53
+ }
54
+ if (!row.filingDate) {
55
+ return !params.from && !params.to;
56
+ }
57
+ return dateInRange(row.filingDate, params.from, params.to);
58
+ });
59
+ const offset = params.offset ?? 0;
60
+ const queryLimit = params.queryLimit ?? filteredRows.length;
61
+ const pagedRows = filteredRows.slice(offset, offset + queryLimit);
62
+ return {
63
+ data: pagedRows,
64
+ metaUpdates: {
65
+ query_total_count: filteredRows.length,
66
+ query_returned_count: pagedRows.length,
67
+ query_truncated: offset + pagedRows.length < filteredRows.length,
68
+ query_offset: offset
69
+ }
70
+ };
71
+ }
72
+ export async function runFilingsGet(params, context) {
73
+ const accession = normalizeAccession(params.accession);
74
+ const entity = await resolveEntity(params.id, context.secClient, { strictMapMatch: false });
75
+ const submissions = await context.secClient.fetchSecJson(submissionsUrl(entity.cik));
76
+ const rows = zipRecentFilings(entity.cik, submissions.filings?.recent);
77
+ const match = rows.find((row) => row.accession === accession);
78
+ if (!match) {
79
+ throw new CLIError(ErrorCode.NOT_FOUND, `Accession ${accession} not found in recent submissions for ${params.id}`);
80
+ }
81
+ if (!match.primaryDocument || !match.filingUrl) {
82
+ throw new CLIError(ErrorCode.NOT_FOUND, `No primary document found for accession ${accession}`);
83
+ }
84
+ if (params.format === 'url') {
85
+ return {
86
+ data: {
87
+ accession: match.accession,
88
+ form: match.form,
89
+ filingDate: match.filingDate,
90
+ reportDate: match.reportDate,
91
+ primaryDocument: match.primaryDocument,
92
+ url: match.filingUrl
93
+ }
94
+ };
95
+ }
96
+ const content = await context.secClient.fetchSecText(match.filingUrl);
97
+ if (params.format === 'html') {
98
+ return {
99
+ data: {
100
+ accession: match.accession,
101
+ url: match.filingUrl,
102
+ content
103
+ }
104
+ };
105
+ }
106
+ return {
107
+ data: {
108
+ accession: match.accession,
109
+ url: match.filingUrl,
110
+ content: extractTextFromHtml(content)
111
+ }
112
+ };
113
+ }
@@ -0,0 +1,2 @@
1
+ import { CommandContext, CommandResult } from '../core/runtime.js';
2
+ export declare function runResolve(id: string, context: CommandContext): Promise<CommandResult>;
@@ -0,0 +1,7 @@
1
+ import { resolveEntity } from '../sec/ticker-map.js';
2
+ export async function runResolve(id, context) {
3
+ const entity = await resolveEntity(id, context.secClient, { strictMapMatch: true });
4
+ return {
5
+ data: entity
6
+ };
7
+ }
@@ -0,0 +1,23 @@
1
+ export interface RuntimeOptions {
2
+ jsonMode: boolean;
3
+ humanMode: boolean;
4
+ view: 'summary' | 'full';
5
+ fields?: string[];
6
+ limit?: number;
7
+ verbose: boolean;
8
+ userAgent?: string;
9
+ }
10
+ export interface RuntimeInput {
11
+ json?: boolean;
12
+ human?: boolean;
13
+ view?: string;
14
+ fields?: string;
15
+ limit?: string | number;
16
+ verbose?: boolean;
17
+ userAgent?: string;
18
+ }
19
+ export declare function buildRuntimeOptions(input: RuntimeInput, env: NodeJS.ProcessEnv): RuntimeOptions;
20
+ export declare function requireUserAgent(userAgent: string | undefined): string;
21
+ export declare function parsePositiveInt(value: string, argName: string): number;
22
+ export declare function parseNonNegativeInt(value: string, argName: string): number;
23
+ export declare function parseDateString(value: string, argName: string): string;
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod';
2
+ import { CLIError, ErrorCode } from './errors.js';
3
+ import { parseFields } from './output-shape.js';
4
+ const positiveIntSchema = z.coerce.number().int().min(1);
5
+ const nonNegativeIntSchema = z.coerce.number().int().min(0);
6
+ const dateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
7
+ export function buildRuntimeOptions(input, env) {
8
+ const humanMode = Boolean(input.human);
9
+ const jsonMode = !humanMode;
10
+ const view = input.view === 'full' ? 'full' : 'summary';
11
+ const parsedFields = parseFields(input.fields);
12
+ const parsedLimit = input.limit === undefined || input.limit === null
13
+ ? undefined
14
+ : parsePositiveInt(String(input.limit), '--limit');
15
+ return {
16
+ jsonMode,
17
+ humanMode,
18
+ view,
19
+ fields: parsedFields,
20
+ limit: parsedLimit,
21
+ verbose: Boolean(input.verbose),
22
+ userAgent: input.userAgent?.trim() || env.EDGAR_USER_AGENT?.trim() || undefined
23
+ };
24
+ }
25
+ export function requireUserAgent(userAgent) {
26
+ if (userAgent && userAgent.trim().length > 0) {
27
+ return userAgent.trim();
28
+ }
29
+ throw new CLIError(ErrorCode.IDENTITY_REQUIRED, 'Missing SEC identity. Set --user-agent "Name email@domain.com" or EDGAR_USER_AGENT.');
30
+ }
31
+ export function parsePositiveInt(value, argName) {
32
+ const parsed = positiveIntSchema.safeParse(value);
33
+ if (!parsed.success) {
34
+ throw new CLIError(ErrorCode.VALIDATION_ERROR, `${argName} must be a positive integer`);
35
+ }
36
+ return parsed.data;
37
+ }
38
+ export function parseNonNegativeInt(value, argName) {
39
+ const parsed = nonNegativeIntSchema.safeParse(value);
40
+ if (!parsed.success) {
41
+ throw new CLIError(ErrorCode.VALIDATION_ERROR, `${argName} must be a non-negative integer`);
42
+ }
43
+ return parsed.data;
44
+ }
45
+ export function parseDateString(value, argName) {
46
+ const parsed = dateSchema.safeParse(value);
47
+ if (!parsed.success) {
48
+ throw new CLIError(ErrorCode.VALIDATION_ERROR, `${argName} must use YYYY-MM-DD`);
49
+ }
50
+ return parsed.data;
51
+ }
@@ -0,0 +1,28 @@
1
+ import { ErrorCode } from './errors.js';
2
+ export interface BrokerError {
3
+ code: ErrorCode;
4
+ message: string;
5
+ retriable: boolean;
6
+ }
7
+ export interface OutputEnvelope {
8
+ ok: boolean;
9
+ command: string;
10
+ provider: 'sec';
11
+ data: unknown;
12
+ error: BrokerError | null;
13
+ meta: Record<string, unknown>;
14
+ }
15
+ export declare function successEnvelope(params: {
16
+ command: string;
17
+ data: unknown;
18
+ view: string;
19
+ metaUpdates?: Record<string, unknown>;
20
+ }): OutputEnvelope;
21
+ export declare function failureEnvelope(params: {
22
+ command: string;
23
+ code: ErrorCode;
24
+ message: string;
25
+ retriable?: boolean;
26
+ view: string;
27
+ metaUpdates?: Record<string, unknown>;
28
+ }): OutputEnvelope;
@@ -0,0 +1,37 @@
1
+ function nowIso() {
2
+ return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
3
+ }
4
+ export function successEnvelope(params) {
5
+ return {
6
+ ok: true,
7
+ command: params.command,
8
+ provider: 'sec',
9
+ data: params.data,
10
+ error: null,
11
+ meta: {
12
+ timestamp: nowIso(),
13
+ output_schema: 'v1',
14
+ view: params.view,
15
+ ...(params.metaUpdates ?? {})
16
+ }
17
+ };
18
+ }
19
+ export function failureEnvelope(params) {
20
+ return {
21
+ ok: false,
22
+ command: params.command,
23
+ provider: 'sec',
24
+ data: null,
25
+ error: {
26
+ code: params.code,
27
+ message: params.message,
28
+ retriable: params.retriable ?? false
29
+ },
30
+ meta: {
31
+ timestamp: nowIso(),
32
+ output_schema: 'v1',
33
+ view: params.view,
34
+ ...(params.metaUpdates ?? {})
35
+ }
36
+ };
37
+ }
@@ -0,0 +1,17 @@
1
+ export declare enum ErrorCode {
2
+ VALIDATION_ERROR = "VALIDATION_ERROR",
3
+ IDENTITY_REQUIRED = "IDENTITY_REQUIRED",
4
+ RATE_LIMITED = "RATE_LIMITED",
5
+ NOT_FOUND = "NOT_FOUND",
6
+ NETWORK_ERROR = "NETWORK_ERROR",
7
+ PARSE_ERROR = "PARSE_ERROR",
8
+ INTERNAL_ERROR = "INTERNAL_ERROR"
9
+ }
10
+ export declare const EXIT_CODE_MAP: Record<ErrorCode, number>;
11
+ export declare class CLIError extends Error {
12
+ readonly code: ErrorCode;
13
+ readonly retriable: boolean;
14
+ constructor(code: ErrorCode, message: string, retriable?: boolean);
15
+ get exitCode(): number;
16
+ }
17
+ export declare function isCLIError(err: unknown): err is CLIError;
@@ -0,0 +1,35 @@
1
+ export var ErrorCode;
2
+ (function (ErrorCode) {
3
+ ErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
4
+ ErrorCode["IDENTITY_REQUIRED"] = "IDENTITY_REQUIRED";
5
+ ErrorCode["RATE_LIMITED"] = "RATE_LIMITED";
6
+ ErrorCode["NOT_FOUND"] = "NOT_FOUND";
7
+ ErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
8
+ ErrorCode["PARSE_ERROR"] = "PARSE_ERROR";
9
+ ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
10
+ })(ErrorCode || (ErrorCode = {}));
11
+ export const EXIT_CODE_MAP = {
12
+ [ErrorCode.VALIDATION_ERROR]: 2,
13
+ [ErrorCode.IDENTITY_REQUIRED]: 3,
14
+ [ErrorCode.RATE_LIMITED]: 4,
15
+ [ErrorCode.NOT_FOUND]: 5,
16
+ [ErrorCode.NETWORK_ERROR]: 6,
17
+ [ErrorCode.PARSE_ERROR]: 7,
18
+ [ErrorCode.INTERNAL_ERROR]: 10
19
+ };
20
+ export class CLIError extends Error {
21
+ code;
22
+ retriable;
23
+ constructor(code, message, retriable = false) {
24
+ super(message);
25
+ this.code = code;
26
+ this.retriable = retriable;
27
+ this.name = 'CLIError';
28
+ }
29
+ get exitCode() {
30
+ return EXIT_CODE_MAP[this.code] ?? 10;
31
+ }
32
+ }
33
+ export function isCLIError(err) {
34
+ return err instanceof CLIError;
35
+ }
@@ -0,0 +1,10 @@
1
+ export interface ShapedData {
2
+ data: unknown;
3
+ metaUpdates: Record<string, unknown>;
4
+ }
5
+ export declare function parseFields(raw: string | undefined): string[] | undefined;
6
+ export declare function shapeData(params: {
7
+ data: unknown;
8
+ fields?: string[];
9
+ limit?: number;
10
+ }): ShapedData;
@@ -0,0 +1,61 @@
1
+ import { CLIError, ErrorCode } from './errors.js';
2
+ export function parseFields(raw) {
3
+ if (!raw) {
4
+ return undefined;
5
+ }
6
+ const fields = raw
7
+ .split(',')
8
+ .map((field) => field.trim())
9
+ .filter((field) => field.length > 0);
10
+ if (fields.length === 0) {
11
+ throw new CLIError(ErrorCode.VALIDATION_ERROR, '--fields requires at least one field');
12
+ }
13
+ return [...new Set(fields)];
14
+ }
15
+ function projectObject(source, fields) {
16
+ const projected = {};
17
+ for (const field of fields) {
18
+ projected[field] = source[field];
19
+ }
20
+ return projected;
21
+ }
22
+ function applyFields(data, fields) {
23
+ if (!fields) {
24
+ return data;
25
+ }
26
+ if (Array.isArray(data)) {
27
+ if (data.length === 0) {
28
+ return data;
29
+ }
30
+ if (!data.every((item) => item && typeof item === 'object' && !Array.isArray(item))) {
31
+ throw new CLIError(ErrorCode.VALIDATION_ERROR, '--fields can only be applied to object results or lists of objects');
32
+ }
33
+ return data.map((item) => projectObject(item, fields));
34
+ }
35
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
36
+ return projectObject(data, fields);
37
+ }
38
+ throw new CLIError(ErrorCode.VALIDATION_ERROR, '--fields can only be applied to object results or lists of objects');
39
+ }
40
+ export function shapeData(params) {
41
+ const fieldShaped = applyFields(params.data, params.fields);
42
+ const metaUpdates = {};
43
+ if (Array.isArray(fieldShaped) && typeof params.limit === 'number') {
44
+ if (params.limit < 1) {
45
+ throw new CLIError(ErrorCode.VALIDATION_ERROR, '--limit must be at least 1');
46
+ }
47
+ const totalCount = fieldShaped.length;
48
+ const data = fieldShaped.slice(0, params.limit);
49
+ metaUpdates.total_count = totalCount;
50
+ metaUpdates.returned_count = data.length;
51
+ metaUpdates.truncated = data.length < totalCount;
52
+ return {
53
+ data,
54
+ metaUpdates
55
+ };
56
+ }
57
+ return {
58
+ data: fieldShaped,
59
+ metaUpdates
60
+ };
61
+ }
@@ -0,0 +1,10 @@
1
+ import { RuntimeOptions } from './config.js';
2
+ import { SecClient } from '../sec/client.js';
3
+ export interface CommandContext {
4
+ runtime: RuntimeOptions;
5
+ secClient: SecClient;
6
+ }
7
+ export interface CommandResult {
8
+ data: unknown;
9
+ metaUpdates?: Record<string, unknown>;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ export interface SecClientOptions {
2
+ userAgent: string;
3
+ verbose?: boolean;
4
+ fetchImpl?: typeof fetch;
5
+ logger?: (message: string) => void;
6
+ }
7
+ export declare class SecClient {
8
+ private readonly userAgent;
9
+ private readonly verbose;
10
+ private readonly fetchImpl;
11
+ private readonly logger;
12
+ constructor(options: SecClientOptions);
13
+ fetchSecJson<T>(url: string): Promise<T>;
14
+ fetchSecText(url: string): Promise<string>;
15
+ private log;
16
+ private request;
17
+ }
@@ -0,0 +1,154 @@
1
+ import pLimit from 'p-limit';
2
+ import { CLIError, ErrorCode } from '../core/errors.js';
3
+ const REQUEST_INTERVAL_MS = 125;
4
+ const MAX_ATTEMPTS = 4;
5
+ const paceGate = pLimit(1);
6
+ let nextAllowedAt = 0;
7
+ const sleep = async (ms) => {
8
+ if (ms <= 0) {
9
+ return;
10
+ }
11
+ await new Promise((resolve) => setTimeout(resolve, ms));
12
+ };
13
+ const jitter = () => Math.floor(Math.random() * 120);
14
+ function retryAfterMs(headerValue) {
15
+ if (!headerValue) {
16
+ return null;
17
+ }
18
+ const seconds = Number.parseInt(headerValue, 10);
19
+ if (!Number.isNaN(seconds)) {
20
+ return Math.max(0, seconds * 1000);
21
+ }
22
+ const retryAt = Date.parse(headerValue);
23
+ if (Number.isNaN(retryAt)) {
24
+ return null;
25
+ }
26
+ return Math.max(0, retryAt - Date.now());
27
+ }
28
+ async function paceRequests() {
29
+ await paceGate(async () => {
30
+ const now = Date.now();
31
+ const delay = Math.max(0, nextAllowedAt - now);
32
+ if (delay > 0) {
33
+ await sleep(delay);
34
+ }
35
+ nextAllowedAt = Math.max(nextAllowedAt, Date.now()) + REQUEST_INTERVAL_MS;
36
+ });
37
+ }
38
+ function isUndeclaredAutomationBody(value) {
39
+ const lowered = value.toLowerCase();
40
+ return (lowered.includes('undeclared automated tool') ||
41
+ lowered.includes('please declare your traffic') ||
42
+ lowered.includes('acceptable policy'));
43
+ }
44
+ function isRetriableNetworkError(err) {
45
+ return err instanceof TypeError;
46
+ }
47
+ function toNetworkError(url, message, retriable = false) {
48
+ return new CLIError(ErrorCode.NETWORK_ERROR, `SEC request failed for ${url}: ${message}`, retriable);
49
+ }
50
+ function toRateLimitedError(url) {
51
+ return new CLIError(ErrorCode.RATE_LIMITED, `SEC rate limit reached for ${url}`, true);
52
+ }
53
+ export class SecClient {
54
+ userAgent;
55
+ verbose;
56
+ fetchImpl;
57
+ logger;
58
+ constructor(options) {
59
+ this.userAgent = options.userAgent;
60
+ this.verbose = options.verbose ?? false;
61
+ this.fetchImpl = options.fetchImpl ?? fetch;
62
+ this.logger = options.logger ?? (() => undefined);
63
+ }
64
+ async fetchSecJson(url) {
65
+ const raw = await this.request(url, 'json');
66
+ try {
67
+ return JSON.parse(raw);
68
+ }
69
+ catch (error) {
70
+ throw new CLIError(ErrorCode.PARSE_ERROR, `Unable to parse SEC JSON response from ${url}: ${error.message}`);
71
+ }
72
+ }
73
+ async fetchSecText(url) {
74
+ return this.request(url, 'text');
75
+ }
76
+ log(message) {
77
+ if (!this.verbose) {
78
+ return;
79
+ }
80
+ this.logger(message);
81
+ }
82
+ async request(url, kind) {
83
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
84
+ try {
85
+ await paceRequests();
86
+ this.log(`GET ${url} (attempt ${attempt}/${MAX_ATTEMPTS})`);
87
+ const response = await this.fetchImpl(url, {
88
+ method: 'GET',
89
+ headers: {
90
+ 'User-Agent': this.userAgent,
91
+ 'Accept-Encoding': 'identity'
92
+ }
93
+ });
94
+ if (response.status === 403) {
95
+ const body = await response.text();
96
+ if (isUndeclaredAutomationBody(body)) {
97
+ throw new CLIError(ErrorCode.IDENTITY_REQUIRED, 'SEC rejected request as undeclared automation. Use a valid --user-agent or EDGAR_USER_AGENT.');
98
+ }
99
+ throw toNetworkError(url, '403 Forbidden');
100
+ }
101
+ if (response.status === 404) {
102
+ throw new CLIError(ErrorCode.NOT_FOUND, `SEC resource not found at ${url}`);
103
+ }
104
+ if (response.status === 429) {
105
+ if (attempt < MAX_ATTEMPTS) {
106
+ const headerDelay = retryAfterMs(response.headers.get('retry-after'));
107
+ const delay = headerDelay ?? 250 * 2 ** (attempt - 1) + jitter();
108
+ this.log(`429 received, waiting ${delay}ms before retry`);
109
+ await sleep(delay);
110
+ continue;
111
+ }
112
+ throw toRateLimitedError(url);
113
+ }
114
+ if (response.status === 503) {
115
+ if (attempt < MAX_ATTEMPTS) {
116
+ const headerDelay = retryAfterMs(response.headers.get('retry-after'));
117
+ const delay = headerDelay ?? 250 * 2 ** (attempt - 1) + jitter();
118
+ this.log(`503 received, waiting ${delay}ms before retry`);
119
+ await sleep(delay);
120
+ continue;
121
+ }
122
+ throw toNetworkError(url, '503 Service Unavailable', true);
123
+ }
124
+ if (!response.ok) {
125
+ if (response.status >= 500 && attempt < MAX_ATTEMPTS) {
126
+ const delay = 250 * 2 ** (attempt - 1) + jitter();
127
+ this.log(`HTTP ${response.status}, waiting ${delay}ms before retry`);
128
+ await sleep(delay);
129
+ continue;
130
+ }
131
+ throw toNetworkError(url, `HTTP ${response.status}`);
132
+ }
133
+ const body = await response.text();
134
+ if (kind === 'json' && body.trim().length === 0) {
135
+ throw new CLIError(ErrorCode.PARSE_ERROR, `SEC returned empty JSON response from ${url}`);
136
+ }
137
+ return body;
138
+ }
139
+ catch (error) {
140
+ if (error instanceof CLIError) {
141
+ throw error;
142
+ }
143
+ if (attempt < MAX_ATTEMPTS && isRetriableNetworkError(error)) {
144
+ const delay = 250 * 2 ** (attempt - 1) + jitter();
145
+ this.log(`Transient network error, waiting ${delay}ms before retry`);
146
+ await sleep(delay);
147
+ continue;
148
+ }
149
+ throw toNetworkError(url, error.message, isRetriableNetworkError(error));
150
+ }
151
+ }
152
+ throw toNetworkError(url, 'Request failed after retries');
153
+ }
154
+ }
@@ -0,0 +1,10 @@
1
+ export declare const SEC_DATA_HOST = "https://data.sec.gov";
2
+ export declare const SEC_WWW_HOST = "https://www.sec.gov";
3
+ export declare function submissionsUrl(cik: string): string;
4
+ export declare function companyFactsUrl(cik: string): string;
5
+ export declare function tickerMapUrl(): string;
6
+ export declare function filingDocumentUrl(params: {
7
+ cik: string;
8
+ accession: string;
9
+ primaryDocument: string;
10
+ }): string;
@@ -0,0 +1,19 @@
1
+ import { normalizeAccession, normalizeCik } from './normalizers.js';
2
+ export const SEC_DATA_HOST = 'https://data.sec.gov';
3
+ export const SEC_WWW_HOST = 'https://www.sec.gov';
4
+ export function submissionsUrl(cik) {
5
+ const cik10 = normalizeCik(cik);
6
+ return `${SEC_DATA_HOST}/submissions/CIK${cik10}.json`;
7
+ }
8
+ export function companyFactsUrl(cik) {
9
+ const cik10 = normalizeCik(cik);
10
+ return `${SEC_DATA_HOST}/api/xbrl/companyfacts/CIK${cik10}.json`;
11
+ }
12
+ export function tickerMapUrl() {
13
+ return `${SEC_WWW_HOST}/files/company_tickers.json`;
14
+ }
15
+ export function filingDocumentUrl(params) {
16
+ const cikNumeric = String(Number.parseInt(normalizeCik(params.cik), 10));
17
+ const accessionNoDash = normalizeAccession(params.accession).replace(/-/g, '');
18
+ return `${SEC_WWW_HOST}/Archives/edgar/data/${cikNumeric}/${accessionNoDash}/${params.primaryDocument}`;
19
+ }
@@ -0,0 +1,5 @@
1
+ export declare function isLikelyCik(value: string): boolean;
2
+ export declare function normalizeCik(value: string): string;
3
+ export declare function normalizeTicker(value: string): string;
4
+ export declare function normalizeAccession(value: string): string;
5
+ export declare function dateInRange(value: string, from?: string, to?: string): boolean;
@@ -0,0 +1,44 @@
1
+ import { z } from 'zod';
2
+ import { CLIError, ErrorCode } from '../core/errors.js';
3
+ const cikSchema = z.string().regex(/^\d{1,10}$/);
4
+ const tickerSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9.-]{0,14}$/);
5
+ const accessionSchema = z.string().regex(/^\d{10}-\d{2}-\d{6}$/);
6
+ export function isLikelyCik(value) {
7
+ return /^\d+$/.test(value.trim());
8
+ }
9
+ export function normalizeCik(value) {
10
+ const trimmed = value.trim();
11
+ const parsed = cikSchema.safeParse(trimmed);
12
+ if (!parsed.success) {
13
+ throw new CLIError(ErrorCode.VALIDATION_ERROR, `Invalid CIK: ${value}`);
14
+ }
15
+ return parsed.data.padStart(10, '0');
16
+ }
17
+ export function normalizeTicker(value) {
18
+ const trimmed = value.trim();
19
+ const parsed = tickerSchema.safeParse(trimmed);
20
+ if (!parsed.success) {
21
+ throw new CLIError(ErrorCode.VALIDATION_ERROR, `Invalid ticker: ${value}`);
22
+ }
23
+ return parsed.data.toUpperCase();
24
+ }
25
+ export function normalizeAccession(value) {
26
+ const trimmed = value.trim();
27
+ const parsed = accessionSchema.safeParse(trimmed);
28
+ if (!parsed.success) {
29
+ throw new CLIError(ErrorCode.VALIDATION_ERROR, '--accession must match XXXXXXXXXX-XX-XXXXXX');
30
+ }
31
+ return parsed.data;
32
+ }
33
+ export function dateInRange(value, from, to) {
34
+ if (!from && !to) {
35
+ return true;
36
+ }
37
+ if (from && value < from) {
38
+ return false;
39
+ }
40
+ if (to && value > to) {
41
+ return false;
42
+ }
43
+ return true;
44
+ }
@@ -0,0 +1,16 @@
1
+ import { SecClient } from './client.js';
2
+ export interface TickerRecord {
3
+ cik_str: number;
4
+ ticker: string;
5
+ title: string;
6
+ }
7
+ export interface ResolvedEntity {
8
+ input: string;
9
+ cik: string;
10
+ cik_numeric: number;
11
+ ticker: string | null;
12
+ title: string | null;
13
+ }
14
+ export declare function resolveEntity(id: string, client: SecClient, options?: {
15
+ strictMapMatch?: boolean;
16
+ }): Promise<ResolvedEntity>;
@@ -0,0 +1,57 @@
1
+ import { CLIError, ErrorCode } from '../core/errors.js';
2
+ import { tickerMapUrl } from './endpoints.js';
3
+ import { isLikelyCik, normalizeCik, normalizeTicker } from './normalizers.js';
4
+ let cachedMap = null;
5
+ let cachedAtMs = 0;
6
+ const CACHE_TTL_MS = 15 * 60 * 1000;
7
+ async function getTickerMap(client) {
8
+ const now = Date.now();
9
+ if (cachedMap && now - cachedAtMs < CACHE_TTL_MS) {
10
+ return cachedMap;
11
+ }
12
+ const payload = await client.fetchSecJson(tickerMapUrl());
13
+ const records = Object.values(payload)
14
+ .filter((record) => record && typeof record === 'object')
15
+ .filter((record) => typeof record.cik_str === 'number' && typeof record.ticker === 'string');
16
+ cachedMap = records;
17
+ cachedAtMs = now;
18
+ return records;
19
+ }
20
+ function findByTicker(records, ticker) {
21
+ return records.find((record) => record.ticker.toUpperCase() === ticker);
22
+ }
23
+ function findByCik(records, cik10) {
24
+ const cikNumeric = Number.parseInt(cik10, 10);
25
+ return records.find((record) => record.cik_str === cikNumeric);
26
+ }
27
+ export async function resolveEntity(id, client, options) {
28
+ const strictMapMatch = options?.strictMapMatch ?? false;
29
+ const records = await getTickerMap(client);
30
+ if (isLikelyCik(id)) {
31
+ const cik = normalizeCik(id);
32
+ const cikNumeric = Number.parseInt(cik, 10);
33
+ const match = findByCik(records, cik);
34
+ if (!match && strictMapMatch) {
35
+ throw new CLIError(ErrorCode.NOT_FOUND, `No SEC ticker-map record found for CIK ${cik}`);
36
+ }
37
+ return {
38
+ input: id,
39
+ cik,
40
+ cik_numeric: cikNumeric,
41
+ ticker: match?.ticker ?? null,
42
+ title: match?.title ?? null
43
+ };
44
+ }
45
+ const ticker = normalizeTicker(id);
46
+ const match = findByTicker(records, ticker);
47
+ if (!match) {
48
+ throw new CLIError(ErrorCode.NOT_FOUND, `No SEC ticker-map record found for ticker ${ticker}`);
49
+ }
50
+ return {
51
+ input: id,
52
+ cik: String(match.cik_str).padStart(10, '0'),
53
+ cik_numeric: match.cik_str,
54
+ ticker: match.ticker,
55
+ title: match.title
56
+ };
57
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "edgar-cli",
3
+ "version": "0.1.1",
4
+ "description": "Agent-friendly SEC EDGAR CLI",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "bin": {
11
+ "edgar": "dist/cli.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/finlayi/edgar-cli.git"
24
+ },
25
+ "homepage": "https://github.com/finlayi/edgar-cli#readme",
26
+ "bugs": {
27
+ "url": "https://github.com/finlayi/edgar-cli/issues"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json",
31
+ "dev": "tsx src/cli.ts",
32
+ "test": "vitest run",
33
+ "lint": "eslint . --ext .ts",
34
+ "typecheck": "tsc --noEmit"
35
+ },
36
+ "keywords": [
37
+ "edgar",
38
+ "sec",
39
+ "cli",
40
+ "npx",
41
+ "filings"
42
+ ],
43
+ "dependencies": {
44
+ "cheerio": "^1.1.2",
45
+ "commander": "^14.0.1",
46
+ "p-limit": "^7.1.1",
47
+ "zod": "^4.1.5"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^22.13.9",
51
+ "@typescript-eslint/eslint-plugin": "^8.44.0",
52
+ "@typescript-eslint/parser": "^8.44.0",
53
+ "eslint": "^8.57.1",
54
+ "nock": "^14.0.10",
55
+ "prettier": "^3.6.2",
56
+ "tsx": "^4.20.5",
57
+ "typescript": "^5.9.2",
58
+ "vitest": "^3.2.4"
59
+ }
60
+ }