Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ secapi config profiles
Two binaries are installed: the preferred `secapi` and the compatibility alias `omni-sec`.

```bash
secapi --version # prints the bare package version, e.g. 1.2.0
secapi --version # prints the bare package version, e.g. 1.3.0
secapi --help # short task-oriented help for common workflows
secapi help all # full command inventory
secapi examples # local starter workflows for humans and agents
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@secapi/cli",
"version": "1.2.0",
"version": "1.3.0",
"description": "SEC API CLI for SEC data shaped for investors and agents",
"type": "module",
"bin": {
Expand Down
200 changes: 200 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ beforeAll(() => {
headers: { ...responseHeaders, "content-type": "text/csv; charset=utf-8" },
})
}
if (url.pathname.endsWith("/export")) {
return new Response("## Special Situation Brief\n\nSource-cited test brief.\n", {
headers: { ...responseHeaders, "content-type": "text/markdown; charset=utf-8" },
})
}
if (url.pathname === "/v1/me" && request.headers.get("x-api-key") === "secapi_live_DOCTOR_ECHO_SECRET") {
return Response.json({
object: "error",
Expand Down Expand Up @@ -634,6 +639,7 @@ describe("CLI version and search commands", () => {
const result = await runCli(["--version"])

expect(result.status).toBe(0)
expect(packageJson.version).toBe("1.3.0")
expect(result.stdout.trim()).toBe(packageJson.version)
expect(result.stdout).not.toContain("SEC API CLI")
expect(requests).toHaveLength(0)
Expand Down Expand Up @@ -2715,6 +2721,200 @@ describe("CLI bridge telegram validation (Phase 12)", () => {
})
})

describe("CLI Special Situations commands", () => {
const auth = { SECAPI_API_KEY: "secapi_live_ENV_BACKED_AUTH" }

test("situations list calls the public REST endpoint with filters and response mode", async () => {
const result = await runCli([
"situations", "list",
"--types", "merger,tender_offer",
"--statuses", "announced",
"--tickers", "AAPL,MSFT",
"--forms", "SC 13D,DEFM14A",
"--enrich", "false",
"--limit", "10",
"--response-mode", "agent",
], { env: auth })

expect(result.status).toBe(0)
expect(JSON.parse(result.stdout)).toEqual({ ok: true })
expect(requests).toHaveLength(1)
expect(requests[0]).toMatchObject({
method: "GET",
path: "/v1/situations",
})
expect(requests[0]?.headers["x-api-key"]).toBe("secapi_live_ENV_BACKED_AUTH")
expect(requests[0]?.searchParams).toMatchObject({
types: "merger,tender_offer",
statuses: "announced",
tickers: "AAPL,MSFT",
forms: "SC 13D,DEFM14A",
enrich: "false",
limit: "10",
response_mode: "agent",
})
assertNoSecretLeak(result.stdout, result.stderr)
})

test("situations by-form forwards every public filter", async () => {
const result = await runCli([
"situations", "by-form",
"--form", "SC 13D",
"--subtypes", "stake_disclosure,proxy_contest",
"--statuses", "announced,pending",
"--tickers", "AAPL,MSFT",
"--sectors", "technology,health_care",
"--market-cap", "mid,large",
"--country", "US",
"--announced-from", "2026-01-01",
"--announced-to", "2026-06-30",
"--updated-from", "2026-07-01",
"--enrich", "false",
"--cursor", "10",
"--limit", "25",
"--response-mode", "agent",
], { env: auth })

expect(result.status).toBe(0)
expect(requests).toHaveLength(1)
expect(requests[0]).toMatchObject({
method: "GET",
path: "/v1/situations/by-form/SC%2013D",
searchParams: {
subtypes: "stake_disclosure,proxy_contest",
statuses: "announced,pending",
tickers: "AAPL,MSFT",
sectors: "technology,health_care",
market_cap: "mid,large",
country: "US",
announced_from: "2026-01-01",
announced_to: "2026-06-30",
updated_from: "2026-07-01",
enrich: "false",
cursor: "10",
limit: "25",
response_mode: "agent",
},
})
})

test("situations export prints markdown from the public REST endpoint", async () => {
const result = await runCli(["situations", "export", "--situation-id", "sit_abc123"], { env: auth })

expect(result.status).toBe(0)
expect(result.stdout).toContain("## Special Situation Brief")
expect(result.stderr).toBe("")
expect(requests[0]?.path).toBe("/v1/situations/sit_abc123/export")
assertNoSecretLeak(result.stdout, result.stderr)
})

test("situations archive and underwriting commands use their public API routes", async () => {
const situationId = "sit_0123456789abcdef0123"

const archive = await runCli(["situations", "issues", "--limit", "12"], { env: auth })
const issue = await runCli(["situations", "issue", "--issue", "42"], { env: auth })
const underwrite = await runCli(["situations", "underwrite", "--situation-id", situationId], { env: auth })

expect(archive.status).toBe(0)
expect(issue.status).toBe(0)
expect(underwrite.status).toBe(0)
expect(requests).toHaveLength(3)
expect(requests.map((request) => request.path)).toEqual([
"/v1/situations/issues",
"/v1/situations/issues/42",
`/v1/situations/${situationId}/underwriting-pack`,
])
expect(requests.every((request) => request.headers["x-api-key"] === "secapi_live_ENV_BACKED_AUTH")).toBe(true)
})

test("situations watch dry-run previews a public monitor request without calling the API", async () => {
const result = await runCli([
"situations", "watch",
"--tickers", "AAPL,MSFT",
"--types", "merger",
"--use-org-webhooks",
"--dry-run",
], { env: auth })

expect(result.status).toBe(0)
expect(requests).toHaveLength(0)
const preview = JSON.parse(result.stdout)
expect(preview).toMatchObject({
object: "secapi_cli_dry_run",
command: "secapi situations watch",
mutates: true,
callsApi: false,
request: {
method: "POST",
path: "/v1/monitors",
},
})
expect(preview.request.body).toEqual({
name: "Special Situations watch",
query: "situations.watch",
searchMode: "situation",
filters: {
types: ["merger"],
tickers: ["AAPL", "MSFT"],
},
delivery: { type: "webhook", config: { organizationEventFanout: true } },
})
assertNoSecretLeak(result.stdout, result.stderr)
})

test("situations watch rejects invalid public contract values before fetching", async () => {
const invalidCommands = [
["situations", "watch", "--situation-id", "sit_notcanonical", "--use-org-webhooks"],
["situations", "watch", "--types", "not_a_situation", "--use-org-webhooks"],
["situations", "watch", "--subtypes", "not_a_subtype", "--use-org-webhooks"],
["situations", "watch", "--statuses", "active", "--use-org-webhooks"],
["situations", "watch", "--types", "merger", "--email", " "],
["situations", "watch", "--types", "merger", "--email", "you@example.com", "--use-org-webhooks"],
]

for (const command of invalidCommands) {
const result = await runCli(command, { env: auth })
expect(result.status).not.toBe(0)
expect(requests).toHaveLength(0)
}
})

test("situations watch accepts a canonical situation id", async () => {
const situationId = "sit_0123456789abcdef0123"
const result = await runCli([
"situations", "watch",
"--situation-id", situationId,
"--email", "you@example.com",
], { env: auth })

expect(result.status).toBe(0)
expect(requests).toHaveLength(1)
expect(JSON.parse(requests[0]?.body ?? "{}")).toMatchObject({
searchMode: "situation",
filters: { situationIds: [situationId] },
delivery: { type: "email", config: { to: "you@example.com" } },
})
})

test("agent-context registers situations commands with accurate mutation and output metadata", async () => {
const result = await runCli(["agent-context"])

expect(result.status).toBe(0)
expect(requests).toHaveLength(0)
const payload = JSON.parse(result.stdout)
const situations = payload.commandGroups.find((group: { group: string }) => group.group === "situations")
expect(situations?.commands).toContain("secapi situations list [--types <types>] [--statuses <statuses>] [--tickers <tickers>] [--forms <forms>] [--enrich true|false] [--limit <n>]")
const watch = situations?.details.find((detail: { command: string }) => detail.command === "secapi situations watch")
expect(watch).toMatchObject({
auth: "api_key",
mutates: true,
requiredFlags: ["--situation-ids|--types|--tickers|--sectors|--statuses|--subtypes", "--email|--use-org-webhooks"],
})
const exported = situations?.details.find((detail: { command: string }) => detail.command === "secapi situations export")
expect(exported).toMatchObject({ output: "json_or_markdown" })
})
})

describe("CLI monitors and news (Phase 10)", () => {
const auth = { SECAPI_API_KEY: "secapi_live_ENV_BACKED_AUTH" }

Expand Down
Loading