hai-api 1.1.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 ADDED
@@ -0,0 +1,14 @@
1
+ # hai-api (Node.js SDK)
2
+
3
+ The official Node.js SDK for Hussain's Private AI Bridge (hAI). High-performance, type-safe integration for multi-AI orchestration.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install hai-api
9
+ ```
10
+
11
+ ## Features
12
+ - Native TypeScript support.
13
+ - Stream-ready API responses.
14
+ - Easy integration with hAI Multi-AI Server.
@@ -0,0 +1,97 @@
1
+ export interface HSeekResponse {
2
+ text: string;
3
+ thinking_text: string;
4
+ search_text: string;
5
+ session_id: string | null;
6
+ parent_message_id: string | null;
7
+ latency_ms: number | null;
8
+ is_done: boolean;
9
+ /** Export the response as a plain object */
10
+ toDict(): Record<string, unknown>;
11
+ readonly process_text: string;
12
+ }
13
+ export interface ChatOptions {
14
+ /** Used to identify the conversation thread. SDK auto-manages session IDs for you. */
15
+ sessionId?: string;
16
+ /** Enable chain-of-thought reasoning before answering. */
17
+ thinking?: boolean;
18
+ /** Enable real-time web search to ground the response. */
19
+ search?: boolean;
20
+ /** Raw backend chat session ID (for manual persistence). Overrides sessionId. */
21
+ chatSessionId?: string;
22
+ /** Raw backend parent message ID (for manual persistence). */
23
+ parentMessageId?: string;
24
+ /** Set a persistent system-level personality or instruction for the AI. */
25
+ system?: string;
26
+ /** Control response creativity: 0.0 = precise/deterministic, 1.0 = creative/random. */
27
+ temperature?: number;
28
+ /** Limit the maximum number of tokens in the response. */
29
+ maxTokens?: number;
30
+ /** Force the response to be a valid JSON object. */
31
+ jsonMode?: boolean;
32
+ /** Auto-translate all responses to this language (e.g., "Arabic", "French"). */
33
+ translate?: string;
34
+ /** Put the AI in expert senior programmer mode. */
35
+ codeMode?: boolean;
36
+ /** Ask the AI to internally verify its answer before responding. */
37
+ verify?: boolean;
38
+ /** Append a 3-bullet summary at the end of every response. */
39
+ summarize?: boolean;
40
+ /** Whether to include the AI's internal thought process in the text response (when thinking=true). */
41
+ includeThought?: boolean;
42
+ /** URL of an image to analyze alongside the prompt (Vision). */
43
+ image?: string;
44
+ /** URL of a YouTube video. SDK will fetch the transcript and analyze it. */
45
+ youtube?: string;
46
+ /** Number of past message exchanges to include for context. 0 = no history, -1 = all. Default: 10. */
47
+ memoryDepth?: number;
48
+ /** Number of times to automatically retry on network failure. Default: 3. */
49
+ retries?: number;
50
+ /** Model to use (e.g., 'deepseek', 'gemini-nano-banana'). */
51
+ model?: string;
52
+ /** Callback function called once streaming is complete. */
53
+ onDone?: (response: HSeekResponse) => void;
54
+ }
55
+ interface SessionData {
56
+ chatSessionId: string | null;
57
+ parentMessageId: string | null;
58
+ history: Array<{
59
+ role: string;
60
+ content: string;
61
+ }>;
62
+ }
63
+ /**
64
+ * HSeekClient — The official client for the hAI private AI.
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * const ai = new HSeekClient();
69
+ * const res = await ai.chat("Hello!", { sessionId: "user_1", thinking: true });
70
+ * console.log(res.text);
71
+ * ```
72
+ */
73
+ export declare class HSeekClient {
74
+ private apiKey;
75
+ private baseUrl;
76
+ private memory;
77
+ constructor(apiKey?: string, baseUrl?: string);
78
+ private buildPayload;
79
+ private createResponse;
80
+ /**
81
+ * Stream a response in real-time, yielding chunks as they arrive.
82
+ */
83
+ streamChat(prompt: string, options?: ChatOptions): AsyncGenerator<HSeekResponse, void, unknown>;
84
+ /**
85
+ * Send a message and get the full response at once (non-streaming).
86
+ */
87
+ chat(prompt: string, options?: ChatOptions): Promise<HSeekResponse>;
88
+ /**
89
+ * Export the full conversation history for a session.
90
+ */
91
+ exportHistory(sessionId: string): SessionData;
92
+ /**
93
+ * Clear the conversation memory for a session.
94
+ */
95
+ clearHistory(sessionId: string): void;
96
+ }
97
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,330 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HSeekClient = void 0;
37
+ const dotenv = __importStar(require("dotenv"));
38
+ dotenv.config();
39
+ // ========================
40
+ // Internal Memory Store
41
+ // ========================
42
+ class MemoryStore {
43
+ sessions = new Map();
44
+ getIds(sessionId) {
45
+ if (!sessionId)
46
+ return { chatSessionId: null, parentMessageId: null };
47
+ const s = this.sessions.get(sessionId);
48
+ return { chatSessionId: s?.chatSessionId ?? null, parentMessageId: s?.parentMessageId ?? null };
49
+ }
50
+ getHistory(sessionId, depth) {
51
+ const history = this.sessions.get(sessionId)?.history ?? [];
52
+ if (depth === 0)
53
+ return [];
54
+ if (depth < 0)
55
+ return history;
56
+ return history.slice(-(depth * 2));
57
+ }
58
+ addToHistory(sessionId, role, content) {
59
+ if (!this.sessions.has(sessionId)) {
60
+ this.sessions.set(sessionId, { chatSessionId: null, parentMessageId: null, history: [] });
61
+ }
62
+ this.sessions.get(sessionId).history.push({ role, content });
63
+ }
64
+ saveIds(sessionId, chatSessionId, parentMessageId) {
65
+ if (!sessionId || !chatSessionId || !parentMessageId)
66
+ return;
67
+ if (!this.sessions.has(sessionId)) {
68
+ this.sessions.set(sessionId, { chatSessionId: null, parentMessageId: null, history: [] });
69
+ }
70
+ const s = this.sessions.get(sessionId);
71
+ s.chatSessionId = chatSessionId;
72
+ s.parentMessageId = parentMessageId;
73
+ }
74
+ export(sessionId) {
75
+ return this.sessions.get(sessionId) ?? { chatSessionId: null, parentMessageId: null, history: [] };
76
+ }
77
+ clear(sessionId) {
78
+ this.sessions.delete(sessionId);
79
+ }
80
+ }
81
+ // ========================
82
+ // YouTube Helpers
83
+ // ========================
84
+ function extractYouTubeId(url) {
85
+ const match = url.match(/(?:v=|youtu\.be\/|\/v\/|\/embed\/|\/shorts\/)([0-9A-Za-z_-]{11})/);
86
+ return match ? match[1] : null;
87
+ }
88
+ async function fetchYouTubeTranscript(videoId) {
89
+ // Fetches transcript via a free public API
90
+ const apiUrl = `https://yt-transcript-api.vercel.app/api/transcript?videoId=${videoId}`;
91
+ try {
92
+ const res = await fetch(apiUrl);
93
+ if (!res.ok)
94
+ throw new Error(`Transcript API returned ${res.status}`);
95
+ const data = await res.json();
96
+ return data.map((d) => d.text).join(' ').substring(0, 15000);
97
+ }
98
+ catch (e) {
99
+ throw new Error(`Failed to fetch YouTube transcript for ${videoId}: ${e}`);
100
+ }
101
+ }
102
+ // ========================
103
+ // Main HSeekClient
104
+ // ========================
105
+ /**
106
+ * HSeekClient — The official client for the hAI private AI.
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * const ai = new HSeekClient();
111
+ * const res = await ai.chat("Hello!", { sessionId: "user_1", thinking: true });
112
+ * console.log(res.text);
113
+ * ```
114
+ */
115
+ class HSeekClient {
116
+ apiKey;
117
+ baseUrl;
118
+ memory;
119
+ constructor(apiKey, baseUrl = "https://ai.hussain.ink") {
120
+ this.apiKey = apiKey || process.env.MY_SECRET_API_KEY || '';
121
+ if (!this.apiKey) {
122
+ throw new Error("API Key is missing. Pass it directly or set MY_SECRET_API_KEY in .env");
123
+ }
124
+ this.baseUrl = baseUrl.replace(/\/$/, "");
125
+ this.memory = new MemoryStore();
126
+ }
127
+ buildPayload(prompt, options) {
128
+ const { sessionId, thinking = false, search = false, chatSessionId, parentMessageId, system, temperature, maxTokens, jsonMode, translate, codeMode, verify, summarize, includeThought, image, memoryDepth = 10, } = options;
129
+ // Build messages array
130
+ const messages = [];
131
+ if (sessionId && memoryDepth !== 0) {
132
+ messages.push(...this.memory.getHistory(sessionId, memoryDepth));
133
+ }
134
+ const userContent = image
135
+ ? [{ type: "image_url", image_url: { url: image } }, { type: "text", text: prompt }]
136
+ : prompt;
137
+ messages.push({ role: "user", content: userContent });
138
+ const payload = {
139
+ messages,
140
+ stream: true,
141
+ thinking,
142
+ search,
143
+ };
144
+ // Optional feature flags
145
+ if (system)
146
+ payload.system = system;
147
+ if (temperature !== undefined)
148
+ payload.temperature = temperature;
149
+ if (maxTokens !== undefined)
150
+ payload.max_tokens = maxTokens;
151
+ if (jsonMode)
152
+ payload.json_mode = true;
153
+ if (translate)
154
+ payload.translate = translate;
155
+ if (codeMode)
156
+ payload.code_mode = true;
157
+ if (verify)
158
+ payload.verify = true;
159
+ if (summarize)
160
+ payload.summarize = true;
161
+ if (includeThought)
162
+ payload.include_thought = true;
163
+ // Session management
164
+ if (chatSessionId) {
165
+ payload.chat_session_id = chatSessionId;
166
+ }
167
+ else if (sessionId) {
168
+ const ids = this.memory.getIds(sessionId);
169
+ if (ids.chatSessionId)
170
+ payload.chat_session_id = ids.chatSessionId;
171
+ if (ids.parentMessageId)
172
+ payload.parent_message_id = ids.parentMessageId;
173
+ }
174
+ if (parentMessageId)
175
+ payload.parent_message_id = parentMessageId;
176
+ if (options.model)
177
+ payload.model = options.model;
178
+ return payload;
179
+ }
180
+ createResponse() {
181
+ const r = {
182
+ text: "",
183
+ thinking_text: "",
184
+ search_text: "",
185
+ session_id: null,
186
+ parent_message_id: null,
187
+ latency_ms: null,
188
+ is_done: false,
189
+ toDict() {
190
+ return {
191
+ text: this.text,
192
+ thinking_text: this.thinking_text,
193
+ search_text: this.search_text,
194
+ session_id: this.session_id,
195
+ parent_message_id: this.parent_message_id,
196
+ latency_ms: this.latency_ms,
197
+ is_done: this.is_done
198
+ };
199
+ },
200
+ get process_text() {
201
+ let p = this.search_text;
202
+ if (this.thinking_text) {
203
+ if (p && !p.endsWith("\n"))
204
+ p += "\n";
205
+ p += this.thinking_text;
206
+ }
207
+ return p;
208
+ }
209
+ };
210
+ return r;
211
+ }
212
+ /**
213
+ * Stream a response in real-time, yielding chunks as they arrive.
214
+ */
215
+ async *streamChat(prompt, options = {}) {
216
+ const { sessionId, youtube, retries = 3, onDone } = options;
217
+ // Handle YouTube
218
+ let actualPrompt = prompt;
219
+ if (youtube) {
220
+ const videoId = extractYouTubeId(youtube);
221
+ if (!videoId)
222
+ throw new Error(`Could not extract YouTube video ID from: ${youtube}`);
223
+ const transcript = await fetchYouTubeTranscript(videoId);
224
+ actualPrompt = `[YOUTUBE VIDEO TRANSCRIPT]:\n${transcript}\n\n[USER REQUEST]:\n${prompt}`;
225
+ }
226
+ const payload = this.buildPayload(actualPrompt, options);
227
+ const currentResponse = this.createResponse();
228
+ let attempt = 0;
229
+ while (attempt <= retries) {
230
+ try {
231
+ const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
232
+ method: 'POST',
233
+ headers: {
234
+ 'Authorization': `Bearer ${this.apiKey}`,
235
+ 'Content-Type': 'application/json'
236
+ },
237
+ body: JSON.stringify(payload)
238
+ });
239
+ if (!response.ok)
240
+ throw new Error(`HSeek API Error: ${response.status}`);
241
+ if (!response.body)
242
+ throw new Error("Response body is null");
243
+ const reader = response.body.getReader();
244
+ const decoder = new TextDecoder();
245
+ let buffer = '';
246
+ while (true) {
247
+ const { done, value } = await reader.read();
248
+ if (done)
249
+ break;
250
+ buffer += decoder.decode(value, { stream: true });
251
+ const lines = buffer.split('\n');
252
+ buffer = lines.pop() || '';
253
+ for (const line of lines) {
254
+ if (!line.startsWith('data: '))
255
+ continue;
256
+ const dataStr = line.substring(6).trim();
257
+ if (dataStr === '[DONE]') {
258
+ currentResponse.is_done = true;
259
+ if (sessionId) {
260
+ this.memory.addToHistory(sessionId, 'user', actualPrompt);
261
+ this.memory.addToHistory(sessionId, 'assistant', currentResponse.text);
262
+ }
263
+ if (onDone)
264
+ onDone(currentResponse);
265
+ yield currentResponse;
266
+ return;
267
+ }
268
+ try {
269
+ const dataJson = JSON.parse(dataStr);
270
+ if (dataJson.choices?.[0]?.delta) {
271
+ const delta = dataJson.choices[0].delta;
272
+ if (delta.content) {
273
+ currentResponse.text += delta.content;
274
+ yield currentResponse;
275
+ }
276
+ if (delta.reasoning_content) {
277
+ currentResponse.thinking_text += delta.reasoning_content;
278
+ yield currentResponse;
279
+ }
280
+ if (delta.search_content) {
281
+ currentResponse.search_text += delta.search_content;
282
+ yield currentResponse;
283
+ }
284
+ }
285
+ else if (dataJson.object === 'chat.completion.meta') {
286
+ currentResponse.parent_message_id = dataJson.message_id;
287
+ currentResponse.session_id = dataJson.chat_session_id;
288
+ currentResponse.latency_ms = dataJson.latency_ms ?? null;
289
+ this.memory.saveIds(sessionId, currentResponse.session_id, currentResponse.parent_message_id);
290
+ }
291
+ }
292
+ catch (_) { /* ignore json parse errors */ }
293
+ }
294
+ }
295
+ break; // Success
296
+ }
297
+ catch (e) {
298
+ attempt++;
299
+ if (attempt > retries)
300
+ throw new Error(`HSeek failed after ${retries} retries. Last error: ${e}`);
301
+ await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
302
+ }
303
+ }
304
+ }
305
+ /**
306
+ * Send a message and get the full response at once (non-streaming).
307
+ */
308
+ async chat(prompt, options = {}) {
309
+ let finalResponse = null;
310
+ for await (const chunk of this.streamChat(prompt, options)) {
311
+ finalResponse = chunk;
312
+ }
313
+ if (!finalResponse)
314
+ throw new Error("HSeek API returned an empty response.");
315
+ return finalResponse;
316
+ }
317
+ /**
318
+ * Export the full conversation history for a session.
319
+ */
320
+ exportHistory(sessionId) {
321
+ return this.memory.export(sessionId);
322
+ }
323
+ /**
324
+ * Clear the conversation memory for a session.
325
+ */
326
+ clearHistory(sessionId) {
327
+ this.memory.clear(sessionId);
328
+ }
329
+ }
330
+ exports.HSeekClient = HSeekClient;
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "hai-api",
3
+ "version": "1.1.2",
4
+ "description": "The official Node.js SDK for hAI Private AI.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "prepublishOnly": "npm run build"
10
+ },
11
+ "author": "Hussain Alkhatib",
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "dotenv": "^16.4.5",
15
+ "hseek-api": "^1.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.12.7",
19
+ "typescript": "^5.4.5"
20
+ }
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,370 @@
1
+ import * as dotenv from 'dotenv';
2
+
3
+ dotenv.config();
4
+
5
+ // ========================
6
+ // Types & Interfaces
7
+ // ========================
8
+
9
+ export interface HSeekResponse {
10
+ text: string;
11
+ thinking_text: string;
12
+ search_text: string;
13
+ session_id: string | null;
14
+ parent_message_id: string | null;
15
+ latency_ms: number | null;
16
+ is_done: boolean;
17
+ /** Export the response as a plain object */
18
+ toDict(): Record<string, unknown>;
19
+ readonly process_text: string;
20
+ }
21
+
22
+ export interface ChatOptions {
23
+ /** Used to identify the conversation thread. SDK auto-manages session IDs for you. */
24
+ sessionId?: string;
25
+ /** Enable chain-of-thought reasoning before answering. */
26
+ thinking?: boolean;
27
+ /** Enable real-time web search to ground the response. */
28
+ search?: boolean;
29
+ /** Raw backend chat session ID (for manual persistence). Overrides sessionId. */
30
+ chatSessionId?: string;
31
+ /** Raw backend parent message ID (for manual persistence). */
32
+ parentMessageId?: string;
33
+ /** Set a persistent system-level personality or instruction for the AI. */
34
+ system?: string;
35
+ /** Control response creativity: 0.0 = precise/deterministic, 1.0 = creative/random. */
36
+ temperature?: number;
37
+ /** Limit the maximum number of tokens in the response. */
38
+ maxTokens?: number;
39
+ /** Force the response to be a valid JSON object. */
40
+ jsonMode?: boolean;
41
+ /** Auto-translate all responses to this language (e.g., "Arabic", "French"). */
42
+ translate?: string;
43
+ /** Put the AI in expert senior programmer mode. */
44
+ codeMode?: boolean;
45
+ /** Ask the AI to internally verify its answer before responding. */
46
+ verify?: boolean;
47
+ /** Append a 3-bullet summary at the end of every response. */
48
+ summarize?: boolean;
49
+ /** Whether to include the AI's internal thought process in the text response (when thinking=true). */
50
+ includeThought?: boolean;
51
+ /** URL of an image to analyze alongside the prompt (Vision). */
52
+ image?: string;
53
+ /** URL of a YouTube video. SDK will fetch the transcript and analyze it. */
54
+ youtube?: string;
55
+ /** Number of past message exchanges to include for context. 0 = no history, -1 = all. Default: 10. */
56
+ memoryDepth?: number;
57
+ /** Number of times to automatically retry on network failure. Default: 3. */
58
+ retries?: number;
59
+ /** Model to use (e.g., 'deepseek', 'gemini-nano-banana'). */
60
+ model?: string;
61
+ /** Callback function called once streaming is complete. */
62
+ onDone?: (response: HSeekResponse) => void;
63
+ }
64
+
65
+ interface SessionData {
66
+ chatSessionId: string | null;
67
+ parentMessageId: string | null;
68
+ history: Array<{ role: string; content: string }>;
69
+ }
70
+
71
+ // ========================
72
+ // Internal Memory Store
73
+ // ========================
74
+ class MemoryStore {
75
+ private sessions: Map<string, SessionData> = new Map();
76
+
77
+ getIds(sessionId?: string): { chatSessionId: string | null; parentMessageId: string | null } {
78
+ if (!sessionId) return { chatSessionId: null, parentMessageId: null };
79
+ const s = this.sessions.get(sessionId);
80
+ return { chatSessionId: s?.chatSessionId ?? null, parentMessageId: s?.parentMessageId ?? null };
81
+ }
82
+
83
+ getHistory(sessionId: string, depth: number): Array<{ role: string; content: string }> {
84
+ const history = this.sessions.get(sessionId)?.history ?? [];
85
+ if (depth === 0) return [];
86
+ if (depth < 0) return history;
87
+ return history.slice(-(depth * 2));
88
+ }
89
+
90
+ addToHistory(sessionId: string, role: string, content: string): void {
91
+ if (!this.sessions.has(sessionId)) {
92
+ this.sessions.set(sessionId, { chatSessionId: null, parentMessageId: null, history: [] });
93
+ }
94
+ this.sessions.get(sessionId)!.history.push({ role, content });
95
+ }
96
+
97
+ saveIds(sessionId: string | undefined, chatSessionId: string | null, parentMessageId: string | null): void {
98
+ if (!sessionId || !chatSessionId || !parentMessageId) return;
99
+ if (!this.sessions.has(sessionId)) {
100
+ this.sessions.set(sessionId, { chatSessionId: null, parentMessageId: null, history: [] });
101
+ }
102
+ const s = this.sessions.get(sessionId)!;
103
+ s.chatSessionId = chatSessionId;
104
+ s.parentMessageId = parentMessageId;
105
+ }
106
+
107
+ export(sessionId: string): SessionData {
108
+ return this.sessions.get(sessionId) ?? { chatSessionId: null, parentMessageId: null, history: [] };
109
+ }
110
+
111
+ clear(sessionId: string): void {
112
+ this.sessions.delete(sessionId);
113
+ }
114
+ }
115
+
116
+ // ========================
117
+ // YouTube Helpers
118
+ // ========================
119
+ function extractYouTubeId(url: string): string | null {
120
+ const match = url.match(/(?:v=|youtu\.be\/|\/v\/|\/embed\/|\/shorts\/)([0-9A-Za-z_-]{11})/);
121
+ return match ? match[1] : null;
122
+ }
123
+
124
+ async function fetchYouTubeTranscript(videoId: string): Promise<string> {
125
+ // Fetches transcript via a free public API
126
+ const apiUrl = `https://yt-transcript-api.vercel.app/api/transcript?videoId=${videoId}`;
127
+ try {
128
+ const res = await fetch(apiUrl);
129
+ if (!res.ok) throw new Error(`Transcript API returned ${res.status}`);
130
+ const data = await res.json() as Array<{ text: string }>;
131
+ return data.map((d) => d.text).join(' ').substring(0, 15000);
132
+ } catch (e) {
133
+ throw new Error(`Failed to fetch YouTube transcript for ${videoId}: ${e}`);
134
+ }
135
+ }
136
+
137
+ // ========================
138
+ // Main HSeekClient
139
+ // ========================
140
+
141
+ /**
142
+ * HSeekClient — The official client for the hAI private AI.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * const ai = new HSeekClient();
147
+ * const res = await ai.chat("Hello!", { sessionId: "user_1", thinking: true });
148
+ * console.log(res.text);
149
+ * ```
150
+ */
151
+ export class HSeekClient {
152
+ private apiKey: string;
153
+ private baseUrl: string;
154
+ private memory: MemoryStore;
155
+
156
+ constructor(apiKey?: string, baseUrl: string = "https://ai.hussain.ink") {
157
+ this.apiKey = apiKey || process.env.MY_SECRET_API_KEY || '';
158
+ if (!this.apiKey) {
159
+ throw new Error("API Key is missing. Pass it directly or set MY_SECRET_API_KEY in .env");
160
+ }
161
+ this.baseUrl = baseUrl.replace(/\/$/, "");
162
+ this.memory = new MemoryStore();
163
+ }
164
+
165
+ private buildPayload(prompt: string, options: ChatOptions): Record<string, unknown> {
166
+ const {
167
+ sessionId, thinking = false, search = false,
168
+ chatSessionId, parentMessageId,
169
+ system, temperature, maxTokens, jsonMode,
170
+ translate, codeMode, verify, summarize, includeThought,
171
+ image, memoryDepth = 10,
172
+ } = options;
173
+
174
+ // Build messages array
175
+ const messages: Array<{ role: string; content: unknown }> = [];
176
+ if (sessionId && memoryDepth !== 0) {
177
+ messages.push(...this.memory.getHistory(sessionId, memoryDepth));
178
+ }
179
+
180
+ const userContent = image
181
+ ? [{ type: "image_url", image_url: { url: image } }, { type: "text", text: prompt }]
182
+ : prompt;
183
+
184
+ messages.push({ role: "user", content: userContent });
185
+
186
+ const payload: Record<string, unknown> = {
187
+ messages,
188
+ stream: true,
189
+ thinking,
190
+ search,
191
+ };
192
+
193
+ // Optional feature flags
194
+ if (system) payload.system = system;
195
+ if (temperature !== undefined) payload.temperature = temperature;
196
+ if (maxTokens !== undefined) payload.max_tokens = maxTokens;
197
+ if (jsonMode) payload.json_mode = true;
198
+ if (translate) payload.translate = translate;
199
+ if (codeMode) payload.code_mode = true;
200
+ if (verify) payload.verify = true;
201
+ if (summarize) payload.summarize = true;
202
+ if (includeThought) payload.include_thought = true;
203
+
204
+ // Session management
205
+ if (chatSessionId) {
206
+ payload.chat_session_id = chatSessionId;
207
+ } else if (sessionId) {
208
+ const ids = this.memory.getIds(sessionId);
209
+ if (ids.chatSessionId) payload.chat_session_id = ids.chatSessionId;
210
+ if (ids.parentMessageId) payload.parent_message_id = ids.parentMessageId;
211
+ }
212
+ if (parentMessageId) payload.parent_message_id = parentMessageId;
213
+ if (options.model) payload.model = options.model;
214
+
215
+ return payload;
216
+ }
217
+
218
+ private createResponse(): HSeekResponse {
219
+ const r: HSeekResponse = {
220
+ text: "",
221
+ thinking_text: "",
222
+ search_text: "",
223
+ session_id: null,
224
+ parent_message_id: null,
225
+ latency_ms: null,
226
+ is_done: false,
227
+ toDict() {
228
+ return {
229
+ text: this.text,
230
+ thinking_text: this.thinking_text,
231
+ search_text: this.search_text,
232
+ session_id: this.session_id,
233
+ parent_message_id: this.parent_message_id,
234
+ latency_ms: this.latency_ms,
235
+ is_done: this.is_done
236
+ };
237
+ },
238
+ get process_text() {
239
+ let p = this.search_text;
240
+ if (this.thinking_text) {
241
+ if (p && !p.endsWith("\n")) p += "\n";
242
+ p += this.thinking_text;
243
+ }
244
+ return p;
245
+ }
246
+ };
247
+ return r;
248
+ }
249
+
250
+ /**
251
+ * Stream a response in real-time, yielding chunks as they arrive.
252
+ */
253
+ async *streamChat(prompt: string, options: ChatOptions = {}): AsyncGenerator<HSeekResponse, void, unknown> {
254
+ const { sessionId, youtube, retries = 3, onDone } = options;
255
+
256
+ // Handle YouTube
257
+ let actualPrompt = prompt;
258
+ if (youtube) {
259
+ const videoId = extractYouTubeId(youtube);
260
+ if (!videoId) throw new Error(`Could not extract YouTube video ID from: ${youtube}`);
261
+ const transcript = await fetchYouTubeTranscript(videoId);
262
+ actualPrompt = `[YOUTUBE VIDEO TRANSCRIPT]:\n${transcript}\n\n[USER REQUEST]:\n${prompt}`;
263
+ }
264
+
265
+ const payload = this.buildPayload(actualPrompt, options);
266
+
267
+ const currentResponse = this.createResponse();
268
+ let attempt = 0;
269
+
270
+ while (attempt <= retries) {
271
+ try {
272
+ const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
273
+ method: 'POST',
274
+ headers: {
275
+ 'Authorization': `Bearer ${this.apiKey}`,
276
+ 'Content-Type': 'application/json'
277
+ },
278
+ body: JSON.stringify(payload)
279
+ });
280
+
281
+ if (!response.ok) throw new Error(`HSeek API Error: ${response.status}`);
282
+ if (!response.body) throw new Error("Response body is null");
283
+
284
+ const reader = response.body.getReader();
285
+ const decoder = new TextDecoder();
286
+ let buffer = '';
287
+
288
+ while (true) {
289
+ const { done, value } = await reader.read();
290
+ if (done) break;
291
+ buffer += decoder.decode(value, { stream: true });
292
+ const lines = buffer.split('\n');
293
+ buffer = lines.pop() || '';
294
+
295
+ for (const line of lines) {
296
+ if (!line.startsWith('data: ')) continue;
297
+ const dataStr = line.substring(6).trim();
298
+
299
+ if (dataStr === '[DONE]') {
300
+ currentResponse.is_done = true;
301
+ if (sessionId) {
302
+ this.memory.addToHistory(sessionId, 'user', actualPrompt);
303
+ this.memory.addToHistory(sessionId, 'assistant', currentResponse.text);
304
+ }
305
+ if (onDone) onDone(currentResponse);
306
+ yield currentResponse;
307
+ return;
308
+ }
309
+
310
+ try {
311
+ const dataJson = JSON.parse(dataStr);
312
+ if (dataJson.choices?.[0]?.delta) {
313
+ const delta = dataJson.choices[0].delta;
314
+ if (delta.content) {
315
+ currentResponse.text += delta.content;
316
+ yield currentResponse;
317
+ }
318
+ if (delta.reasoning_content) {
319
+ currentResponse.thinking_text += delta.reasoning_content;
320
+ yield currentResponse;
321
+ }
322
+ if (delta.search_content) {
323
+ currentResponse.search_text += delta.search_content;
324
+ yield currentResponse;
325
+ }
326
+ } else if (dataJson.object === 'chat.completion.meta') {
327
+ currentResponse.parent_message_id = dataJson.message_id;
328
+ currentResponse.session_id = dataJson.chat_session_id;
329
+ currentResponse.latency_ms = dataJson.latency_ms ?? null;
330
+ this.memory.saveIds(sessionId, currentResponse.session_id, currentResponse.parent_message_id);
331
+ }
332
+ } catch (_) { /* ignore json parse errors */ }
333
+ }
334
+ }
335
+ break; // Success
336
+
337
+ } catch (e) {
338
+ attempt++;
339
+ if (attempt > retries) throw new Error(`HSeek failed after ${retries} retries. Last error: ${e}`);
340
+ await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
341
+ }
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Send a message and get the full response at once (non-streaming).
347
+ */
348
+ async chat(prompt: string, options: ChatOptions = {}): Promise<HSeekResponse> {
349
+ let finalResponse: HSeekResponse | null = null;
350
+ for await (const chunk of this.streamChat(prompt, options)) {
351
+ finalResponse = chunk;
352
+ }
353
+ if (!finalResponse) throw new Error("HSeek API returned an empty response.");
354
+ return finalResponse;
355
+ }
356
+
357
+ /**
358
+ * Export the full conversation history for a session.
359
+ */
360
+ exportHistory(sessionId: string): SessionData {
361
+ return this.memory.export(sessionId);
362
+ }
363
+
364
+ /**
365
+ * Clear the conversation memory for a session.
366
+ */
367
+ clearHistory(sessionId: string): void {
368
+ this.memory.clear(sessionId);
369
+ }
370
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "node16",
6
+ "declaration": true,
7
+ "rootDir": "./src",
8
+ "outDir": "./dist",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src/**/*"]
15
+ }