@teemtape/api-client 0.0.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.
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Shared data contracts for the teemtape API.
3
+ *
4
+ * These types are the single source of truth used by every client (CLI, web,
5
+ * and later React Native) and will be implemented by the Cloudflare Worker in
6
+ * milestone M0. See docs/architecture.md and docs/roadmap.md.
7
+ */
8
+ /** Where a note came from. Drives the "agent" badge in the web UI. */
9
+ type NoteSource = "web" | "cli";
10
+ /** A delayed stock quote (delayed ~1 minute on purpose — not a trading tool). */
11
+ interface Quote {
12
+ symbol: string;
13
+ name: string;
14
+ /** Last (delayed) price. */
15
+ price: number;
16
+ /** Absolute change vs previous close. */
17
+ change: number;
18
+ /** Percentage change vs previous close. */
19
+ pct: number;
20
+ /** ISO timestamp of the (delayed) quote. */
21
+ asOf: string;
22
+ }
23
+ interface QuotesResponse {
24
+ quotes: Quote[];
25
+ /** How many seconds the data is intentionally delayed by. */
26
+ delayedSeconds: number;
27
+ /** Upstream data source, e.g. "polygon" | "massive" | "mock". */
28
+ source: string;
29
+ }
30
+ /** An anonymous note attached to a (watchlist, symbol). */
31
+ interface Note {
32
+ id: string;
33
+ symbol: string;
34
+ /** Short anonymous handle, e.g. "anon-6f1ed0" or "agent-cli". */
35
+ author: string;
36
+ source: NoteSource;
37
+ body: string;
38
+ /** ISO timestamp. */
39
+ createdAt: string;
40
+ }
41
+ interface NotesResponse {
42
+ symbol: string;
43
+ notes: Note[];
44
+ }
45
+ /** A watchlist identified by an anonymous MD5 token (lives in the share URL). */
46
+ interface Watchlist {
47
+ token: string;
48
+ symbols: string[];
49
+ createdAt: string;
50
+ }
51
+ interface CreateNoteInput {
52
+ symbol: string;
53
+ body: string;
54
+ source: NoteSource;
55
+ }
56
+ /** A row in the SEC symbols reference catalog. */
57
+ interface SymbolEntry {
58
+ ticker: string;
59
+ cikStr: number;
60
+ title: string;
61
+ }
62
+ interface SymbolsListResponse {
63
+ symbols: SymbolEntry[];
64
+ /** Zero-based row offset (e.g. 0, 100, 200). */
65
+ offset: number;
66
+ /** Page size (max 100). */
67
+ limit: number;
68
+ /** Total rows matching the current filters. */
69
+ total: number;
70
+ /** Sort order applied: ticker or title. */
71
+ sort: "ticker" | "title";
72
+ }
73
+
74
+ interface ApiClientOptions {
75
+ /** Base URL of the Worker API, e.g. http://localhost:8787 */
76
+ baseUrl: string;
77
+ /** Optional default watchlist token used by watchlist/note calls. */
78
+ token?: string;
79
+ /** Injectable fetch (defaults to global fetch); handy for tests. */
80
+ fetch?: typeof fetch;
81
+ }
82
+ /** Thrown for any non-2xx API response. */
83
+ declare class ApiError extends Error {
84
+ readonly status: number;
85
+ readonly body: unknown;
86
+ constructor(status: number, message: string, body: unknown);
87
+ }
88
+ /**
89
+ * Minimal typed client for the teemtape Worker API. The same contract is reused
90
+ * by the CLI now and the web/native apps later (see docs/cli-options.md).
91
+ */
92
+ declare class TeemtapeClient {
93
+ private readonly baseUrl;
94
+ private readonly token?;
95
+ private readonly fetchImpl;
96
+ constructor(options: ApiClientOptions);
97
+ /** Delayed quotes for the given symbols. */
98
+ getQuotes(symbols: string[]): Promise<QuotesResponse>;
99
+ /** Paginated SEC symbol catalog with optional search and sort. */
100
+ listSymbols(params?: {
101
+ offset?: number;
102
+ limit?: number;
103
+ sort?: "ticker" | "title";
104
+ q?: string;
105
+ symbol?: string;
106
+ name?: string;
107
+ }): Promise<SymbolsListResponse>;
108
+ /** Create a new anonymous watchlist and return its MD5 token. */
109
+ createWatchlist(): Promise<Watchlist>;
110
+ /** Fetch a watchlist (symbols + metadata) by token. */
111
+ getWatchlist(token?: string): Promise<Watchlist>;
112
+ /** Add a symbol to a watchlist. */
113
+ addSymbol(symbol: string, token?: string): Promise<Watchlist>;
114
+ /** Notes for a symbol on a watchlist. */
115
+ getNotes(symbol: string, token?: string): Promise<NotesResponse>;
116
+ /** Post an anonymous note to a symbol. */
117
+ addNote(input: CreateNoteInput, token?: string): Promise<Note>;
118
+ private requireToken;
119
+ private request;
120
+ }
121
+
122
+ export { type ApiClientOptions, ApiError, type CreateNoteInput, type Note, type NoteSource, type NotesResponse, type Quote, type QuotesResponse, type SymbolEntry, type SymbolsListResponse, TeemtapeClient, type Watchlist };
package/dist/index.js ADDED
@@ -0,0 +1,104 @@
1
+ // src/client.ts
2
+ var ApiError = class extends Error {
3
+ status;
4
+ body;
5
+ constructor(status, message, body) {
6
+ super(message);
7
+ this.name = "ApiError";
8
+ this.status = status;
9
+ this.body = body;
10
+ }
11
+ };
12
+ var TeemtapeClient = class {
13
+ baseUrl;
14
+ token;
15
+ fetchImpl;
16
+ constructor(options) {
17
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
18
+ this.token = options.token;
19
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
20
+ if (typeof this.fetchImpl !== "function") {
21
+ throw new Error("No fetch implementation available (need Node 18+ or pass options.fetch).");
22
+ }
23
+ }
24
+ /** Delayed quotes for the given symbols. */
25
+ async getQuotes(symbols) {
26
+ const query = encodeURIComponent(symbols.join(","));
27
+ return this.request(`/api/quotes?symbols=${query}`);
28
+ }
29
+ /** Paginated SEC symbol catalog with optional search and sort. */
30
+ async listSymbols(params = {}) {
31
+ const search = new URLSearchParams();
32
+ if (params.offset !== void 0) search.set("offset", String(params.offset));
33
+ if (params.limit !== void 0) search.set("limit", String(params.limit));
34
+ if (params.sort) search.set("sort", params.sort);
35
+ if (params.q) search.set("q", params.q);
36
+ if (params.symbol) search.set("symbol", params.symbol);
37
+ if (params.name) search.set("name", params.name);
38
+ const qs = search.toString();
39
+ return this.request(`/api/symbols${qs ? `?${qs}` : ""}`);
40
+ }
41
+ /** Create a new anonymous watchlist and return its MD5 token. */
42
+ async createWatchlist() {
43
+ return this.request(`/api/watchlists`, { method: "POST" });
44
+ }
45
+ /** Fetch a watchlist (symbols + metadata) by token. */
46
+ async getWatchlist(token = this.requireToken()) {
47
+ return this.request(`/api/w/${token}`);
48
+ }
49
+ /** Add a symbol to a watchlist. */
50
+ async addSymbol(symbol, token = this.requireToken()) {
51
+ return this.request(`/api/w/${token}/symbols`, {
52
+ method: "POST",
53
+ body: JSON.stringify({ symbol: symbol.toUpperCase() })
54
+ });
55
+ }
56
+ /** Notes for a symbol on a watchlist. */
57
+ async getNotes(symbol, token = this.requireToken()) {
58
+ const query = encodeURIComponent(symbol.toUpperCase());
59
+ return this.request(`/api/w/${token}/notes?symbol=${query}`);
60
+ }
61
+ /** Post an anonymous note to a symbol. */
62
+ async addNote(input, token = this.requireToken()) {
63
+ return this.request(`/api/w/${token}/notes`, {
64
+ method: "POST",
65
+ body: JSON.stringify({ ...input, symbol: input.symbol.toUpperCase() })
66
+ });
67
+ }
68
+ requireToken() {
69
+ if (!this.token) {
70
+ throw new Error("A watchlist token is required. Pass --token or set TEEMTAPE_TOKEN.");
71
+ }
72
+ return this.token;
73
+ }
74
+ async request(path, init = {}) {
75
+ const headers = new Headers(init.headers);
76
+ if (init.body && !headers.has("content-type")) {
77
+ headers.set("content-type", "application/json");
78
+ }
79
+ headers.set("accept", "application/json");
80
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, { ...init, headers });
81
+ const text = await res.text();
82
+ const data = text ? safeJsonParse(text) : void 0;
83
+ if (!res.ok) {
84
+ const message = isRecord(data) && typeof data.error === "string" && data.error || `Request to ${path} failed with ${res.status}`;
85
+ throw new ApiError(res.status, message, data);
86
+ }
87
+ return data;
88
+ }
89
+ };
90
+ function safeJsonParse(text) {
91
+ try {
92
+ return JSON.parse(text);
93
+ } catch {
94
+ return text;
95
+ }
96
+ }
97
+ function isRecord(value) {
98
+ return typeof value === "object" && value !== null;
99
+ }
100
+ export {
101
+ ApiError,
102
+ TeemtapeClient
103
+ };
104
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import type {\n CreateNoteInput,\n Note,\n NotesResponse,\n QuotesResponse,\n SymbolsListResponse,\n Watchlist,\n} from \"./types.js\";\n\nexport interface ApiClientOptions {\n /** Base URL of the Worker API, e.g. http://localhost:8787 */\n baseUrl: string;\n /** Optional default watchlist token used by watchlist/note calls. */\n token?: string;\n /** Injectable fetch (defaults to global fetch); handy for tests. */\n fetch?: typeof fetch;\n}\n\n/** Thrown for any non-2xx API response. */\nexport class ApiError extends Error {\n readonly status: number;\n readonly body: unknown;\n\n constructor(status: number, message: string, body: unknown) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.body = body;\n }\n}\n\n/**\n * Minimal typed client for the teemtape Worker API. The same contract is reused\n * by the CLI now and the web/native apps later (see docs/cli-options.md).\n */\nexport class TeemtapeClient {\n private readonly baseUrl: string;\n private readonly token?: string;\n private readonly fetchImpl: typeof fetch;\n\n constructor(options: ApiClientOptions) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, \"\");\n this.token = options.token;\n this.fetchImpl = options.fetch ?? globalThis.fetch;\n if (typeof this.fetchImpl !== \"function\") {\n throw new Error(\"No fetch implementation available (need Node 18+ or pass options.fetch).\");\n }\n }\n\n /** Delayed quotes for the given symbols. */\n async getQuotes(symbols: string[]): Promise<QuotesResponse> {\n const query = encodeURIComponent(symbols.join(\",\"));\n return this.request<QuotesResponse>(`/api/quotes?symbols=${query}`);\n }\n\n /** Paginated SEC symbol catalog with optional search and sort. */\n async listSymbols(params: {\n offset?: number;\n limit?: number;\n sort?: \"ticker\" | \"title\";\n q?: string;\n symbol?: string;\n name?: string;\n } = {}): Promise<SymbolsListResponse> {\n const search = new URLSearchParams();\n if (params.offset !== undefined) search.set(\"offset\", String(params.offset));\n if (params.limit !== undefined) search.set(\"limit\", String(params.limit));\n if (params.sort) search.set(\"sort\", params.sort);\n if (params.q) search.set(\"q\", params.q);\n if (params.symbol) search.set(\"symbol\", params.symbol);\n if (params.name) search.set(\"name\", params.name);\n const qs = search.toString();\n return this.request<SymbolsListResponse>(`/api/symbols${qs ? `?${qs}` : \"\"}`);\n }\n\n /** Create a new anonymous watchlist and return its MD5 token. */\n async createWatchlist(): Promise<Watchlist> {\n return this.request<Watchlist>(`/api/watchlists`, { method: \"POST\" });\n }\n\n /** Fetch a watchlist (symbols + metadata) by token. */\n async getWatchlist(token = this.requireToken()): Promise<Watchlist> {\n return this.request<Watchlist>(`/api/w/${token}`);\n }\n\n /** Add a symbol to a watchlist. */\n async addSymbol(symbol: string, token = this.requireToken()): Promise<Watchlist> {\n return this.request<Watchlist>(`/api/w/${token}/symbols`, {\n method: \"POST\",\n body: JSON.stringify({ symbol: symbol.toUpperCase() }),\n });\n }\n\n /** Notes for a symbol on a watchlist. */\n async getNotes(symbol: string, token = this.requireToken()): Promise<NotesResponse> {\n const query = encodeURIComponent(symbol.toUpperCase());\n return this.request<NotesResponse>(`/api/w/${token}/notes?symbol=${query}`);\n }\n\n /** Post an anonymous note to a symbol. */\n async addNote(input: CreateNoteInput, token = this.requireToken()): Promise<Note> {\n return this.request<Note>(`/api/w/${token}/notes`, {\n method: \"POST\",\n body: JSON.stringify({ ...input, symbol: input.symbol.toUpperCase() }),\n });\n }\n\n private requireToken(): string {\n if (!this.token) {\n throw new Error(\"A watchlist token is required. Pass --token or set TEEMTAPE_TOKEN.\");\n }\n return this.token;\n }\n\n private async request<T>(path: string, init: RequestInit = {}): Promise<T> {\n const headers = new Headers(init.headers);\n if (init.body && !headers.has(\"content-type\")) {\n headers.set(\"content-type\", \"application/json\");\n }\n headers.set(\"accept\", \"application/json\");\n\n const res = await this.fetchImpl(`${this.baseUrl}${path}`, { ...init, headers });\n const text = await res.text();\n const data = text ? safeJsonParse(text) : undefined;\n\n if (!res.ok) {\n const message =\n (isRecord(data) && typeof data.error === \"string\" && data.error) ||\n `Request to ${path} failed with ${res.status}`;\n throw new ApiError(res.status, message, data);\n }\n return data as T;\n }\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@teemtape/api-client",
3
+ "version": "0.0.1",
4
+ "description": "Shared typed client + types for the teemtape Worker API.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "test": "node --test"
22
+ },
23
+ "devDependencies": {
24
+ "tsup": "^8.0.0",
25
+ "typescript": "^5.4.0"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ }
30
+ }