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/README.md +3 -133
- package/bin/edgar-lib.cjs +118 -0
- package/bin/edgar.cjs +6 -0
- package/package.json +20 -34
- package/LICENSE +0 -21
- package/dist/cli.d.ts +0 -10
- package/dist/cli.js +0 -332
- package/dist/commands/facts.d.ts +0 -8
- package/dist/commands/facts.js +0 -125
- package/dist/commands/filings.d.ts +0 -14
- package/dist/commands/filings.js +0 -198
- package/dist/commands/research.d.ts +0 -33
- package/dist/commands/research.js +0 -698
- package/dist/commands/resolve.d.ts +0 -2
- package/dist/commands/resolve.js +0 -7
- package/dist/core/config.d.ts +0 -23
- package/dist/core/config.js +0 -51
- package/dist/core/envelope.d.ts +0 -28
- package/dist/core/envelope.js +0 -37
- package/dist/core/errors.d.ts +0 -18
- package/dist/core/errors.js +0 -37
- package/dist/core/output-shape.d.ts +0 -10
- package/dist/core/output-shape.js +0 -61
- package/dist/core/runtime.d.ts +0 -10
- package/dist/core/runtime.js +0 -1
- package/dist/sec/client.d.ts +0 -17
- package/dist/sec/client.js +0 -154
- package/dist/sec/endpoints.d.ts +0 -10
- package/dist/sec/endpoints.js +0 -19
- package/dist/sec/normalizers.d.ts +0 -5
- package/dist/sec/normalizers.js +0 -44
- package/dist/sec/ticker-map.d.ts +0 -16
- package/dist/sec/ticker-map.js +0 -57
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
|
-
}
|
package/dist/commands/facts.d.ts
DELETED
|
@@ -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>;
|
package/dist/commands/facts.js
DELETED
|
@@ -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>;
|
package/dist/commands/filings.js
DELETED
|
@@ -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
|
-
}
|