@sourcerly/sdk 0.0.0-stage → 0.1.0

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 Garcia Patodo, LLC (Sourcerly)
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 CHANGED
@@ -1,3 +1,18 @@
1
- # Temporary Holding Version
1
+ # @sourcerly/sdk (TypeScript / JavaScript)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ```sh
4
+ npm install @sourcerly/sdk
5
+ ```
6
+
7
+ Node 18 or newer. This is an ES module package: use `import`, or `const { Sourcerly } = await import("@sourcerly/sdk")` from CommonJS (`require` does not work).
8
+
9
+ ```ts
10
+ import { Sourcerly } from "@sourcerly/sdk";
11
+
12
+ const client = new Sourcerly({ apiKey: process.env.SOURCERLY_API_KEY! });
13
+ const response = await client.ask({ input: "Can I cancel? Will I get a refund?" });
14
+ for (const r of response.results) console.log(r.status, r.answer ?? r.message);
15
+ ```
16
+
17
+ `client.ask({ questions: [{ id: "refund", question: "..." }] })` sends explicit questions, `client.stream(...)` yields each result as soon as it is ready.
18
+ Retries reuse one `Idempotency-Key`, so a retried request is answered and counted once. No dependencies (uses `fetch`). Tests: `npm test`.
@@ -0,0 +1,96 @@
1
+ export type EvidenceStatus = "supported" | "partial" | "insufficient" | "conflicting" | "not_evaluated";
2
+ export interface Citation {
3
+ sourceId: string | null;
4
+ sourceName: string;
5
+ page: number | null;
6
+ reference: string | null;
7
+ }
8
+ export interface Result {
9
+ id: string;
10
+ question: string;
11
+ status: EvidenceStatus;
12
+ answerable: boolean;
13
+ answer: string | null;
14
+ message: string | null;
15
+ language: string | null;
16
+ citations: Citation[];
17
+ }
18
+ export interface AskResponse {
19
+ id: string;
20
+ object: "sourcerly.answer";
21
+ status: "completed" | "partial";
22
+ livemode: boolean;
23
+ createdAt: string;
24
+ language: string | null;
25
+ conversationId: string | null;
26
+ results: Result[];
27
+ summary: {
28
+ questions: number;
29
+ supported: number;
30
+ partial: number;
31
+ insufficient: number;
32
+ conflicting: number;
33
+ notEvaluated: number;
34
+ };
35
+ usage: {
36
+ questions: number;
37
+ };
38
+ }
39
+ export type Question = string | {
40
+ id?: string;
41
+ question: string;
42
+ };
43
+ export interface AskOptions {
44
+ idempotencyKey?: string;
45
+ citations?: boolean;
46
+ conversationId?: string;
47
+ }
48
+ export type AskInput = ({
49
+ input: string;
50
+ } | {
51
+ questions: Question[];
52
+ }) & AskOptions;
53
+ export interface StreamEvent {
54
+ event: string;
55
+ data: any;
56
+ }
57
+ export interface SourcerlyOptions {
58
+ apiKey: string;
59
+ baseUrl?: string; /** Streaming is served by the Sourcerly backend (the public API does not stream). */
60
+ streamBaseUrl?: string;
61
+ timeoutMs?: number;
62
+ maxRetries?: number;
63
+ fetch?: typeof fetch;
64
+ }
65
+ /** The API refused or failed the request. `status` is the HTTP status, `code` the machine-readable code (for example BAD_REQUEST). */
66
+ export declare class SourcerlyError extends Error {
67
+ readonly status?: number | undefined;
68
+ readonly code?: string | undefined;
69
+ readonly retryAfter?: number | undefined;
70
+ constructor(message: string, status?: number | undefined, code?: string | undefined, retryAfter?: number | undefined);
71
+ }
72
+ /** 401 or 403: the key is missing, invalid, revoked, expired, or its channel is disabled. */
73
+ export declare class SourcerlyAuthError extends SourcerlyError {
74
+ }
75
+ /** 429 RATE_LIMITED: too many requests. `retryAfter` is how many seconds to wait. */
76
+ export declare class SourcerlyRateLimitError extends SourcerlyError {
77
+ }
78
+ /** 429 QUESTION_LIMIT_EXCEEDED: the plan's monthly questions are used up. Retrying will not help until the next period or an upgrade. */
79
+ export declare class SourcerlyQuotaError extends SourcerlyError {
80
+ }
81
+ export declare class Sourcerly {
82
+ private readonly apiKey;
83
+ private readonly baseUrl;
84
+ private readonly streamBaseUrl;
85
+ private readonly timeoutMs;
86
+ private readonly maxRetries;
87
+ private readonly fetchImpl;
88
+ constructor(options: SourcerlyOptions);
89
+ /** Free text (`input`) that may hold several questions, or explicit `questions` (strings, or {id, question} to keep your own ids). */
90
+ ask(request: AskInput | string): Promise<AskResponse>;
91
+ /** Each event as it arrives. Every `question.completed` is a finished, cited, evidence-checked result. */
92
+ stream(request: AskInput | string): AsyncGenerator<StreamEvent>;
93
+ private body;
94
+ private post;
95
+ private error;
96
+ }
@@ -0,0 +1,134 @@
1
+ // Small client for POST /v1/ask on the Sourcerly public API. No dependencies: it uses the platform's fetch (Node 18+, browsers' server-side runtimes).
2
+ //
3
+ // const client = new Sourcerly({ apiKey: process.env.SOURCERLY_API_KEY! });
4
+ // const response = await client.ask({ input: "Can I cancel? Will I get a refund?" });
5
+ // for (const r of response.results) console.log(r.status, r.answer ?? r.message);
6
+ /** The API refused or failed the request. `status` is the HTTP status, `code` the machine-readable code (for example BAD_REQUEST). */
7
+ export class SourcerlyError extends Error {
8
+ status;
9
+ code;
10
+ retryAfter;
11
+ constructor(message, status, code, retryAfter) {
12
+ super(message);
13
+ this.status = status;
14
+ this.code = code;
15
+ this.retryAfter = retryAfter;
16
+ this.name = new.target.name;
17
+ }
18
+ }
19
+ /** 401 or 403: the key is missing, invalid, revoked, expired, or its channel is disabled. */
20
+ export class SourcerlyAuthError extends SourcerlyError {
21
+ }
22
+ /** 429 RATE_LIMITED: too many requests. `retryAfter` is how many seconds to wait. */
23
+ export class SourcerlyRateLimitError extends SourcerlyError {
24
+ }
25
+ /** 429 QUESTION_LIMIT_EXCEEDED: the plan's monthly questions are used up. Retrying will not help until the next period or an upgrade. */
26
+ export class SourcerlyQuotaError extends SourcerlyError {
27
+ }
28
+ const DEFAULT_BASE_URL = "https://public-api.sourcerly.io";
29
+ const DEFAULT_STREAM_BASE_URL = "https://api.sourcerly.io/msp-sourcerly";
30
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
31
+ export class Sourcerly {
32
+ apiKey;
33
+ baseUrl;
34
+ streamBaseUrl;
35
+ timeoutMs;
36
+ maxRetries;
37
+ fetchImpl;
38
+ constructor(options) {
39
+ if (!options.apiKey)
40
+ throw new Error("apiKey is required");
41
+ this.apiKey = options.apiKey;
42
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
43
+ this.streamBaseUrl = (options.streamBaseUrl ?? DEFAULT_STREAM_BASE_URL).replace(/\/+$/, "");
44
+ this.timeoutMs = options.timeoutMs ?? 60_000;
45
+ this.maxRetries = options.maxRetries ?? 2;
46
+ this.fetchImpl = options.fetch ?? fetch;
47
+ }
48
+ /** Free text (`input`) that may hold several questions, or explicit `questions` (strings, or {id, question} to keep your own ids). */
49
+ async ask(request) {
50
+ const body = this.body(typeof request === "string" ? { input: request } : request);
51
+ // One key for every attempt: a retry after a timeout is the same logical request, answered and counted once.
52
+ const key = (typeof request === "string" ? undefined : request.idempotencyKey) ?? crypto.randomUUID();
53
+ for (let attempt = 0;; attempt++) {
54
+ try {
55
+ const response = await this.post("/v1/ask", body, key, "application/json");
56
+ if (response.ok)
57
+ return (await response.json());
58
+ const error = await this.error(response);
59
+ const retryable = [409, 429, 500, 502, 503, 504].includes(response.status) && !(error instanceof SourcerlyQuotaError);
60
+ if (!retryable || attempt >= this.maxRetries)
61
+ throw error;
62
+ await sleep((error.retryAfter ?? Math.min(2 ** attempt * 0.5, 8)) * 1000);
63
+ }
64
+ catch (e) {
65
+ if (e instanceof SourcerlyError)
66
+ throw e;
67
+ if (attempt >= this.maxRetries)
68
+ throw new SourcerlyError(`Could not reach Sourcerly: ${e.message}`);
69
+ await sleep(Math.min(2 ** attempt * 0.5, 8) * 1000);
70
+ }
71
+ }
72
+ }
73
+ /** Each event as it arrives. Every `question.completed` is a finished, cited, evidence-checked result. */
74
+ async *stream(request) {
75
+ const r = typeof request === "string" ? { input: request } : request;
76
+ const response = await this.post("/api/v1/ask/stream", this.body(r), r.idempotencyKey ?? crypto.randomUUID(), "text/event-stream", this.streamBaseUrl);
77
+ if (!response.ok)
78
+ throw await this.error(response);
79
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
80
+ let buffer = "";
81
+ for (;;) {
82
+ const { value, done } = await reader.read();
83
+ if (done)
84
+ return;
85
+ buffer += value;
86
+ let end;
87
+ while ((end = buffer.search(/\r?\n\r?\n/)) >= 0) {
88
+ const block = buffer.slice(0, end);
89
+ buffer = buffer.slice(end).replace(/^\r?\n\r?\n/, "");
90
+ let event = "", data = "";
91
+ for (const line of block.split(/\r?\n/)) {
92
+ if (line.startsWith("event:"))
93
+ event = line.slice(6).trim();
94
+ else if (line.startsWith("data:"))
95
+ data += line.slice(5).trimStart();
96
+ }
97
+ if (event)
98
+ yield { event, data: data ? JSON.parse(data) : null };
99
+ }
100
+ }
101
+ }
102
+ body(r) {
103
+ const body = "input" in r
104
+ ? { input: r.input }
105
+ : { questions: r.questions.map((q) => (typeof q === "string" ? { question: q } : q)) };
106
+ if (r.citations === false)
107
+ body.response = { citations: false };
108
+ if (r.conversationId)
109
+ body.conversationId = r.conversationId;
110
+ return JSON.stringify(body);
111
+ }
112
+ post(path, body, idempotencyKey, accept, base = this.baseUrl) {
113
+ return this.fetchImpl(base + path, {
114
+ method: "POST", body, signal: AbortSignal.timeout(this.timeoutMs),
115
+ headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json", Accept: accept, "Idempotency-Key": idempotencyKey },
116
+ });
117
+ }
118
+ async error(response) {
119
+ let payload = {};
120
+ try {
121
+ payload = await response.json();
122
+ }
123
+ catch { /* not JSON */ }
124
+ const message = payload.message ?? response.statusText ?? "Request failed", code = payload.code;
125
+ const header = response.headers.get("Retry-After"), retryAfter = header ? Number(header) : undefined;
126
+ if (response.status === 401 || response.status === 403)
127
+ return new SourcerlyAuthError(message, response.status, code);
128
+ if (response.status === 429 && code === "QUESTION_LIMIT_EXCEEDED")
129
+ return new SourcerlyQuotaError(message, response.status, code);
130
+ if (response.status === 429)
131
+ return new SourcerlyRateLimitError(message, response.status, code, retryAfter);
132
+ return new SourcerlyError(message, response.status, code, retryAfter);
133
+ }
134
+ }
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "@sourcerly/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "TypeScript client for the Sourcerly API: grounded, cited answers from your own knowledge.",
5
+ "keywords": [
6
+ "sourcerly",
7
+ "rag",
8
+ "knowledge",
9
+ "answers",
10
+ "citations",
11
+ "sdk"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://gitlab.com/sourcerly/apis/sourcerly-sdk.git",
17
+ "directory": "typescript"
18
+ },
19
+ "type": "module",
20
+ "main": "dist/src/index.js",
21
+ "types": "dist/src/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/src/index.d.ts",
25
+ "import": "./dist/src/index.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist/src",
30
+ "README.md"
31
+ ],
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p .",
40
+ "test": "tsc -p . && node --test dist/test/client.test.js",
41
+ "prepublishOnly": "npm test"
42
+ },
43
+ "devDependencies": {
44
+ "typescript": "^5.6.0",
45
+ "@types/node": "^22.0.0"
46
+ }
47
+ }