@fereai/sdk 0.1.0-dev.4
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 +19 -0
- package/dist/auth.d.ts +27 -0
- package/dist/auth.js +213 -0
- package/dist/client.d.ts +52 -0
- package/dist/client.js +110 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +11 -0
- package/dist/low-level.d.ts +58 -0
- package/dist/low-level.js +268 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.js +15 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @fere/sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for the FereAI Gateway API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @fere/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { FereClient } from "@fere/sdk";
|
|
15
|
+
|
|
16
|
+
const client = new FereClient({ apiKey: "your-api-key", baseUrl: "https://api.fere.ai" });
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
See the [FereAI docs](https://api.fere.ai) for full API reference.
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth module — Ed25519 keypair management and token lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Uses @noble/ed25519 for Ed25519 signing.
|
|
5
|
+
*/
|
|
6
|
+
export declare class AuthManager {
|
|
7
|
+
private readonly agentName;
|
|
8
|
+
private readonly baseUrl;
|
|
9
|
+
private readonly keyPath;
|
|
10
|
+
private secretKey;
|
|
11
|
+
private publicKey;
|
|
12
|
+
private agentId;
|
|
13
|
+
private token;
|
|
14
|
+
private tokenExpiresAt;
|
|
15
|
+
constructor(agentName: string, baseUrl?: string, keyPath?: string);
|
|
16
|
+
private loadOrCreateKeypair;
|
|
17
|
+
private getPublicKey;
|
|
18
|
+
getPublicKeyB64(): Promise<string>;
|
|
19
|
+
private sign;
|
|
20
|
+
private saveKeypair;
|
|
21
|
+
private readCreds;
|
|
22
|
+
ensureRegistered(): Promise<string>;
|
|
23
|
+
ensureToken(): Promise<string>;
|
|
24
|
+
authHeaders(): Record<string, string>;
|
|
25
|
+
/** Clear cached token to force refresh on next call. */
|
|
26
|
+
clearToken(): void;
|
|
27
|
+
}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auth module — Ed25519 keypair management and token lifecycle.
|
|
4
|
+
*
|
|
5
|
+
* Uses @noble/ed25519 for Ed25519 signing.
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.AuthManager = void 0;
|
|
42
|
+
const ed = __importStar(require("@noble/ed25519"));
|
|
43
|
+
const fs_1 = require("fs");
|
|
44
|
+
const path_1 = require("path");
|
|
45
|
+
const os_1 = require("os");
|
|
46
|
+
class AuthManager {
|
|
47
|
+
agentName;
|
|
48
|
+
baseUrl;
|
|
49
|
+
keyPath;
|
|
50
|
+
secretKey = null;
|
|
51
|
+
publicKey = null;
|
|
52
|
+
agentId = null;
|
|
53
|
+
token = null;
|
|
54
|
+
tokenExpiresAt = 0;
|
|
55
|
+
constructor(agentName, baseUrl = "https://api.fereai.xyz", keyPath) {
|
|
56
|
+
this.agentName = agentName;
|
|
57
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
58
|
+
this.keyPath = keyPath ?? (0, path_1.join)((0, os_1.homedir)(), ".fere", "keys.json");
|
|
59
|
+
}
|
|
60
|
+
// ----------------------------------------------------------
|
|
61
|
+
// Keypair management
|
|
62
|
+
// ----------------------------------------------------------
|
|
63
|
+
loadOrCreateKeypair() {
|
|
64
|
+
if (this.secretKey)
|
|
65
|
+
return;
|
|
66
|
+
const creds = this.readCreds();
|
|
67
|
+
const entry = creds[this.baseUrl]?.[this.agentName];
|
|
68
|
+
if (entry?.secret_key) {
|
|
69
|
+
this.secretKey = Buffer.from(entry.secret_key, "base64");
|
|
70
|
+
this.publicKey = Buffer.from(entry.public_key, "base64");
|
|
71
|
+
this.agentId = entry.agent_id ?? null;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this.secretKey = ed.utils.randomPrivateKey();
|
|
75
|
+
this.publicKey = null; // Will be derived on first use
|
|
76
|
+
this.saveKeypair();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async getPublicKey() {
|
|
80
|
+
this.loadOrCreateKeypair();
|
|
81
|
+
if (!this.publicKey) {
|
|
82
|
+
this.publicKey = await ed.getPublicKeyAsync(this.secretKey);
|
|
83
|
+
this.saveKeypair();
|
|
84
|
+
}
|
|
85
|
+
return this.publicKey;
|
|
86
|
+
}
|
|
87
|
+
async getPublicKeyB64() {
|
|
88
|
+
const pk = await this.getPublicKey();
|
|
89
|
+
return Buffer.from(pk).toString("base64");
|
|
90
|
+
}
|
|
91
|
+
async sign(message) {
|
|
92
|
+
this.loadOrCreateKeypair();
|
|
93
|
+
const pk = await this.getPublicKey();
|
|
94
|
+
const sig = await ed.signAsync(new TextEncoder().encode(message), this.secretKey);
|
|
95
|
+
return Buffer.from(sig).toString("base64");
|
|
96
|
+
}
|
|
97
|
+
saveKeypair() {
|
|
98
|
+
const creds = this.readCreds();
|
|
99
|
+
if (!creds[this.baseUrl])
|
|
100
|
+
creds[this.baseUrl] = {};
|
|
101
|
+
creds[this.baseUrl][this.agentName] = {
|
|
102
|
+
secret_key: Buffer.from(this.secretKey).toString("base64"),
|
|
103
|
+
public_key: this.publicKey
|
|
104
|
+
? Buffer.from(this.publicKey).toString("base64")
|
|
105
|
+
: "",
|
|
106
|
+
agent_id: this.agentId ?? undefined,
|
|
107
|
+
};
|
|
108
|
+
const dir = (0, path_1.dirname)(this.keyPath);
|
|
109
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
110
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
111
|
+
(0, fs_1.writeFileSync)(this.keyPath, JSON.stringify(creds, null, 2));
|
|
112
|
+
}
|
|
113
|
+
readCreds() {
|
|
114
|
+
if ((0, fs_1.existsSync)(this.keyPath)) {
|
|
115
|
+
return JSON.parse((0, fs_1.readFileSync)(this.keyPath, "utf-8"));
|
|
116
|
+
}
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
// ----------------------------------------------------------
|
|
120
|
+
// Registration + token flow
|
|
121
|
+
// ----------------------------------------------------------
|
|
122
|
+
async ensureRegistered() {
|
|
123
|
+
this.loadOrCreateKeypair();
|
|
124
|
+
if (this.agentId)
|
|
125
|
+
return this.agentId;
|
|
126
|
+
const pubKey = await this.getPublicKeyB64();
|
|
127
|
+
// Step 1: Register
|
|
128
|
+
const regResp = await fetch(`${this.baseUrl}/v1/auth/register`, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "Content-Type": "application/json" },
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
agent_name: this.agentName,
|
|
133
|
+
public_key: pubKey,
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
if (!regResp.ok)
|
|
137
|
+
throw new Error(`Registration failed: ${regResp.status}`);
|
|
138
|
+
const reg = (await regResp.json());
|
|
139
|
+
// Step 2: Sign challenge and verify
|
|
140
|
+
const sig = await this.sign(reg.challenge);
|
|
141
|
+
const verifyResp = await fetch(`${this.baseUrl}/v1/auth/verify`, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: { "Content-Type": "application/json" },
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
registration_id: reg.registration_id,
|
|
146
|
+
challenge: reg.challenge,
|
|
147
|
+
signature: sig,
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
if (!verifyResp.ok)
|
|
151
|
+
throw new Error(`Verification failed: ${verifyResp.status}`);
|
|
152
|
+
const verify = (await verifyResp.json());
|
|
153
|
+
this.agentId = verify.agent_id;
|
|
154
|
+
this.saveKeypair();
|
|
155
|
+
return this.agentId;
|
|
156
|
+
}
|
|
157
|
+
async ensureToken() {
|
|
158
|
+
if (this.token && this.tokenExpiresAt > Date.now() / 1000 + 30) {
|
|
159
|
+
return this.token;
|
|
160
|
+
}
|
|
161
|
+
const agentId = await this.ensureRegistered();
|
|
162
|
+
const ts = Math.floor(Date.now() / 1000).toString();
|
|
163
|
+
const sig = await this.sign(ts);
|
|
164
|
+
const resp = await fetch(`${this.baseUrl}/v1/auth/token`, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: { "Content-Type": "application/json" },
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
agent_id: agentId,
|
|
169
|
+
timestamp: ts,
|
|
170
|
+
signature: sig,
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
if (resp.status === 401) {
|
|
174
|
+
// Re-register
|
|
175
|
+
this.agentId = null;
|
|
176
|
+
const newAgentId = await this.ensureRegistered();
|
|
177
|
+
const newTs = Math.floor(Date.now() / 1000).toString();
|
|
178
|
+
const newSig = await this.sign(newTs);
|
|
179
|
+
const retryResp = await fetch(`${this.baseUrl}/v1/auth/token`, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: { "Content-Type": "application/json" },
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
agent_id: newAgentId,
|
|
184
|
+
timestamp: newTs,
|
|
185
|
+
signature: newSig,
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
if (!retryResp.ok)
|
|
189
|
+
throw new Error(`Token request failed: ${retryResp.status}`);
|
|
190
|
+
const data = (await retryResp.json());
|
|
191
|
+
this.token = data.token;
|
|
192
|
+
this.tokenExpiresAt = Date.now() / 1000 + data.expires_in;
|
|
193
|
+
return this.token;
|
|
194
|
+
}
|
|
195
|
+
if (!resp.ok)
|
|
196
|
+
throw new Error(`Token request failed: ${resp.status}`);
|
|
197
|
+
const data = (await resp.json());
|
|
198
|
+
this.token = data.token;
|
|
199
|
+
this.tokenExpiresAt = Date.now() / 1000 + data.expires_in;
|
|
200
|
+
return this.token;
|
|
201
|
+
}
|
|
202
|
+
authHeaders() {
|
|
203
|
+
if (!this.token)
|
|
204
|
+
throw new Error("Call ensureToken() first");
|
|
205
|
+
return { Authorization: `Bearer ${this.token}` };
|
|
206
|
+
}
|
|
207
|
+
/** Clear cached token to force refresh on next call. */
|
|
208
|
+
clearToken() {
|
|
209
|
+
this.token = null;
|
|
210
|
+
this.tokenExpiresAt = 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
exports.AuthManager = AuthManager;
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level SDK — developer-friendly wrapper that hides async tasks.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```ts
|
|
6
|
+
* const client = await FereClient.create({ agentName: "my-bot" });
|
|
7
|
+
* const result = await client.swap({
|
|
8
|
+
* chain_id_in: 8453,
|
|
9
|
+
* chain_id_out: 8453,
|
|
10
|
+
* token_in: "0xEeee...EEeE",
|
|
11
|
+
* token_out: "0x8335...2913",
|
|
12
|
+
* amount: "1000000000000000000",
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import type { ChatEvent, SwapRequest, LimitOrderRequest, HooksRequest, DepositRequest, WithdrawRequest } from "./types.js";
|
|
17
|
+
export declare class FereClient {
|
|
18
|
+
private readonly api;
|
|
19
|
+
private constructor();
|
|
20
|
+
static create(opts: {
|
|
21
|
+
agentName: string;
|
|
22
|
+
baseUrl?: string;
|
|
23
|
+
keyPath?: string;
|
|
24
|
+
}): Promise<FereClient>;
|
|
25
|
+
chat(query: string, opts?: {
|
|
26
|
+
threadId?: string;
|
|
27
|
+
agent?: string;
|
|
28
|
+
}): Promise<Record<string, unknown>>;
|
|
29
|
+
chatStream(query: string, opts?: {
|
|
30
|
+
threadId?: string;
|
|
31
|
+
agent?: string;
|
|
32
|
+
}): AsyncGenerator<ChatEvent>;
|
|
33
|
+
swap(request: SwapRequest, timeout?: number): Promise<Record<string, unknown>>;
|
|
34
|
+
limitOrder(request: LimitOrderRequest, timeout?: number): Promise<Record<string, unknown>>;
|
|
35
|
+
setHooks(request: HooksRequest): Promise<unknown>;
|
|
36
|
+
deposit(request: DepositRequest, timeout?: number): Promise<Record<string, unknown>>;
|
|
37
|
+
withdraw(request: WithdrawRequest, timeout?: number): Promise<Record<string, unknown>>;
|
|
38
|
+
getWallets: () => Promise<unknown>;
|
|
39
|
+
getHoldings: () => Promise<unknown>;
|
|
40
|
+
getCredits: () => Promise<import("./types.js").CreditsInfo>;
|
|
41
|
+
getUser: () => Promise<unknown>;
|
|
42
|
+
getChains: () => Promise<unknown>;
|
|
43
|
+
getEarnInfo: () => Promise<unknown>;
|
|
44
|
+
getPositions: () => Promise<unknown>;
|
|
45
|
+
getThreads: (skip?: number, limit?: number) => Promise<unknown[]>;
|
|
46
|
+
getLimitOrders: (status?: string) => Promise<unknown[]>;
|
|
47
|
+
getNotifications: (opts?: {
|
|
48
|
+
limit?: number;
|
|
49
|
+
offset?: number;
|
|
50
|
+
type?: string;
|
|
51
|
+
}) => Promise<unknown>;
|
|
52
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* High-level SDK — developer-friendly wrapper that hides async tasks.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ```ts
|
|
7
|
+
* const client = await FereClient.create({ agentName: "my-bot" });
|
|
8
|
+
* const result = await client.swap({
|
|
9
|
+
* chain_id_in: 8453,
|
|
10
|
+
* chain_id_out: 8453,
|
|
11
|
+
* token_in: "0xEeee...EEeE",
|
|
12
|
+
* token_out: "0x8335...2913",
|
|
13
|
+
* amount: "1000000000000000000",
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.FereClient = void 0;
|
|
19
|
+
const low_level_js_1 = require("./low-level.js");
|
|
20
|
+
const DEFAULT_TIMEOUT = 120;
|
|
21
|
+
class FereClient {
|
|
22
|
+
api;
|
|
23
|
+
constructor(api) {
|
|
24
|
+
this.api = api;
|
|
25
|
+
}
|
|
26
|
+
static async create(opts) {
|
|
27
|
+
const api = new low_level_js_1.FereAPI(opts);
|
|
28
|
+
await api.authenticate();
|
|
29
|
+
return new FereClient(api);
|
|
30
|
+
}
|
|
31
|
+
// ----------------------------------------------------------
|
|
32
|
+
// Chat
|
|
33
|
+
// ----------------------------------------------------------
|
|
34
|
+
async chat(query, opts) {
|
|
35
|
+
const result = {};
|
|
36
|
+
const toolResponses = [];
|
|
37
|
+
for await (const event of this.api.chat(query, opts)) {
|
|
38
|
+
switch (event.event) {
|
|
39
|
+
case "meta":
|
|
40
|
+
result.chat_id = event.data.chat_id;
|
|
41
|
+
break;
|
|
42
|
+
case "tool_response":
|
|
43
|
+
toolResponses.push(event.data);
|
|
44
|
+
break;
|
|
45
|
+
case "answer":
|
|
46
|
+
result.answer = event.data.text ?? "";
|
|
47
|
+
break;
|
|
48
|
+
case "done":
|
|
49
|
+
result.done = event.data;
|
|
50
|
+
break;
|
|
51
|
+
case "error":
|
|
52
|
+
throw new Error(String(event.data.message ?? JSON.stringify(event.data)));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (toolResponses.length > 0) {
|
|
56
|
+
result.tool_responses = toolResponses;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
async *chatStream(query, opts) {
|
|
61
|
+
yield* this.api.chat(query, opts);
|
|
62
|
+
}
|
|
63
|
+
// ----------------------------------------------------------
|
|
64
|
+
// Trading (blocks until complete)
|
|
65
|
+
// ----------------------------------------------------------
|
|
66
|
+
async swap(request, timeout = DEFAULT_TIMEOUT) {
|
|
67
|
+
return this.api.createSwap(request, {
|
|
68
|
+
wait: true,
|
|
69
|
+
timeout,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async limitOrder(request, timeout = DEFAULT_TIMEOUT) {
|
|
73
|
+
return this.api.createLimitOrder(request, {
|
|
74
|
+
wait: true,
|
|
75
|
+
timeout,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async setHooks(request) {
|
|
79
|
+
return this.api.setHooks(request);
|
|
80
|
+
}
|
|
81
|
+
// ----------------------------------------------------------
|
|
82
|
+
// Earn (blocks until complete)
|
|
83
|
+
// ----------------------------------------------------------
|
|
84
|
+
async deposit(request, timeout = DEFAULT_TIMEOUT) {
|
|
85
|
+
return this.api.deposit(request, {
|
|
86
|
+
wait: true,
|
|
87
|
+
timeout,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async withdraw(request, timeout = DEFAULT_TIMEOUT) {
|
|
91
|
+
return this.api.withdraw(request, {
|
|
92
|
+
wait: true,
|
|
93
|
+
timeout,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// ----------------------------------------------------------
|
|
97
|
+
// Read-only (pass-through)
|
|
98
|
+
// ----------------------------------------------------------
|
|
99
|
+
getWallets = () => this.api.getWallets();
|
|
100
|
+
getHoldings = () => this.api.getHoldings();
|
|
101
|
+
getCredits = () => this.api.getCredits();
|
|
102
|
+
getUser = () => this.api.getUser();
|
|
103
|
+
getChains = () => this.api.getChains();
|
|
104
|
+
getEarnInfo = () => this.api.getEarnInfo();
|
|
105
|
+
getPositions = () => this.api.getPositions();
|
|
106
|
+
getThreads = (skip, limit) => this.api.getThreads(skip, limit);
|
|
107
|
+
getLimitOrders = (status) => this.api.getLimitOrders(status);
|
|
108
|
+
getNotifications = (opts) => this.api.getNotifications(opts);
|
|
109
|
+
}
|
|
110
|
+
exports.FereClient = FereClient;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { FereAPI } from "./low-level.js";
|
|
2
|
+
export { FereClient } from "./client.js";
|
|
3
|
+
export { AuthManager } from "./auth.js";
|
|
4
|
+
export type { ChatEvent, TaskResponse, TaskStatusResponse, SwapRequest, LimitOrderRequest, HooksRequest, DepositRequest, WithdrawRequest, CreditsInfo, WalletInfo, } from "./types.js";
|
|
5
|
+
export { TaskTimeoutError } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TaskTimeoutError = exports.AuthManager = exports.FereClient = exports.FereAPI = void 0;
|
|
4
|
+
var low_level_js_1 = require("./low-level.js");
|
|
5
|
+
Object.defineProperty(exports, "FereAPI", { enumerable: true, get: function () { return low_level_js_1.FereAPI; } });
|
|
6
|
+
var client_js_1 = require("./client.js");
|
|
7
|
+
Object.defineProperty(exports, "FereClient", { enumerable: true, get: function () { return client_js_1.FereClient; } });
|
|
8
|
+
var auth_js_1 = require("./auth.js");
|
|
9
|
+
Object.defineProperty(exports, "AuthManager", { enumerable: true, get: function () { return auth_js_1.AuthManager; } });
|
|
10
|
+
var types_js_1 = require("./types.js");
|
|
11
|
+
Object.defineProperty(exports, "TaskTimeoutError", { enumerable: true, get: function () { return types_js_1.TaskTimeoutError; } });
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level SDK — 1:1 typed wrappers around every gateway endpoint.
|
|
3
|
+
*/
|
|
4
|
+
import type { ChatEvent, CreditsInfo, DepositRequest, HooksRequest, LimitOrderRequest, SwapRequest, TaskResponse, TaskStatusResponse, WithdrawRequest } from "./types.js";
|
|
5
|
+
export declare class FereAPI {
|
|
6
|
+
private readonly baseUrl;
|
|
7
|
+
private readonly auth;
|
|
8
|
+
constructor(opts: {
|
|
9
|
+
agentName: string;
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
keyPath?: string;
|
|
12
|
+
});
|
|
13
|
+
authenticate(): Promise<string>;
|
|
14
|
+
private headers;
|
|
15
|
+
private authedGet;
|
|
16
|
+
private authedPost;
|
|
17
|
+
private authedDelete;
|
|
18
|
+
chat(query: string, opts?: {
|
|
19
|
+
threadId?: string;
|
|
20
|
+
agent?: string;
|
|
21
|
+
}): AsyncGenerator<ChatEvent>;
|
|
22
|
+
getThreads(skip?: number, limit?: number): Promise<unknown[]>;
|
|
23
|
+
getThread(threadId: string): Promise<unknown[]>;
|
|
24
|
+
createSwap(request: SwapRequest, opts?: {
|
|
25
|
+
wait?: boolean;
|
|
26
|
+
timeout?: number;
|
|
27
|
+
}): Promise<TaskResponse | Record<string, unknown>>;
|
|
28
|
+
createLimitOrder(request: LimitOrderRequest, opts?: {
|
|
29
|
+
wait?: boolean;
|
|
30
|
+
timeout?: number;
|
|
31
|
+
}): Promise<TaskResponse | Record<string, unknown>>;
|
|
32
|
+
getLimitOrders(status?: string): Promise<unknown[]>;
|
|
33
|
+
getLimitOrder(orderId: string): Promise<unknown>;
|
|
34
|
+
cancelLimitOrder(orderId: string): Promise<unknown>;
|
|
35
|
+
setHooks(request: HooksRequest): Promise<unknown>;
|
|
36
|
+
getWallets(): Promise<unknown>;
|
|
37
|
+
getHoldings(): Promise<unknown>;
|
|
38
|
+
getEarnInfo(): Promise<unknown>;
|
|
39
|
+
deposit(request: DepositRequest, opts?: {
|
|
40
|
+
wait?: boolean;
|
|
41
|
+
timeout?: number;
|
|
42
|
+
}): Promise<TaskResponse | Record<string, unknown>>;
|
|
43
|
+
withdraw(request: WithdrawRequest, opts?: {
|
|
44
|
+
wait?: boolean;
|
|
45
|
+
timeout?: number;
|
|
46
|
+
}): Promise<TaskResponse | Record<string, unknown>>;
|
|
47
|
+
getPositions(): Promise<unknown>;
|
|
48
|
+
getTask(taskId: string): Promise<TaskStatusResponse>;
|
|
49
|
+
getNotifications(opts?: {
|
|
50
|
+
limit?: number;
|
|
51
|
+
offset?: number;
|
|
52
|
+
type?: string;
|
|
53
|
+
}): Promise<unknown>;
|
|
54
|
+
notificationStream(): AsyncGenerator<ChatEvent>;
|
|
55
|
+
getCredits(): Promise<CreditsInfo>;
|
|
56
|
+
getUser(): Promise<unknown>;
|
|
57
|
+
getChains(): Promise<unknown>;
|
|
58
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Low-level SDK — 1:1 typed wrappers around every gateway endpoint.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.FereAPI = void 0;
|
|
7
|
+
const auth_js_1 = require("./auth.js");
|
|
8
|
+
const DEFAULT_BASE_URL = "https://api.fereai.xyz";
|
|
9
|
+
class FereAPI {
|
|
10
|
+
baseUrl;
|
|
11
|
+
auth;
|
|
12
|
+
constructor(opts) {
|
|
13
|
+
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
14
|
+
this.auth = new auth_js_1.AuthManager(opts.agentName, this.baseUrl, opts.keyPath);
|
|
15
|
+
}
|
|
16
|
+
async authenticate() {
|
|
17
|
+
return this.auth.ensureToken();
|
|
18
|
+
}
|
|
19
|
+
async headers() {
|
|
20
|
+
await this.auth.ensureToken();
|
|
21
|
+
return {
|
|
22
|
+
...this.auth.authHeaders(),
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async authedGet(path, params) {
|
|
27
|
+
const h = await this.headers();
|
|
28
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
29
|
+
if (params)
|
|
30
|
+
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
31
|
+
let resp = await fetch(url.toString(), { headers: h });
|
|
32
|
+
if (resp.status === 401) {
|
|
33
|
+
this.auth.clearToken();
|
|
34
|
+
const h2 = await this.headers();
|
|
35
|
+
resp = await fetch(url.toString(), { headers: h2 });
|
|
36
|
+
}
|
|
37
|
+
if (!resp.ok)
|
|
38
|
+
throw new Error(`GET ${path} failed: ${resp.status}`);
|
|
39
|
+
return resp.json();
|
|
40
|
+
}
|
|
41
|
+
async authedPost(path, body, params) {
|
|
42
|
+
const h = await this.headers();
|
|
43
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
44
|
+
if (params)
|
|
45
|
+
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
46
|
+
let resp = await fetch(url.toString(), {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: h,
|
|
49
|
+
body: JSON.stringify(body),
|
|
50
|
+
});
|
|
51
|
+
if (resp.status === 401) {
|
|
52
|
+
this.auth.clearToken();
|
|
53
|
+
const h2 = await this.headers();
|
|
54
|
+
resp = await fetch(url.toString(), {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: h2,
|
|
57
|
+
body: JSON.stringify(body),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (!resp.ok)
|
|
61
|
+
throw new Error(`POST ${path} failed: ${resp.status}`);
|
|
62
|
+
return resp.json();
|
|
63
|
+
}
|
|
64
|
+
async authedDelete(path) {
|
|
65
|
+
const h = await this.headers();
|
|
66
|
+
let resp = await fetch(`${this.baseUrl}${path}`, {
|
|
67
|
+
method: "DELETE",
|
|
68
|
+
headers: h,
|
|
69
|
+
});
|
|
70
|
+
if (resp.status === 401) {
|
|
71
|
+
this.auth.clearToken();
|
|
72
|
+
const h2 = await this.headers();
|
|
73
|
+
resp = await fetch(`${this.baseUrl}${path}`, {
|
|
74
|
+
method: "DELETE",
|
|
75
|
+
headers: h2,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (!resp.ok)
|
|
79
|
+
throw new Error(`DELETE ${path} failed: ${resp.status}`);
|
|
80
|
+
return resp.json();
|
|
81
|
+
}
|
|
82
|
+
// ----------------------------------------------------------
|
|
83
|
+
// Chat
|
|
84
|
+
// ----------------------------------------------------------
|
|
85
|
+
async *chat(query, opts) {
|
|
86
|
+
const h = await this.headers();
|
|
87
|
+
const body = {
|
|
88
|
+
query,
|
|
89
|
+
stream: true,
|
|
90
|
+
agent: opts?.agent ?? "ProAgent",
|
|
91
|
+
...(opts?.threadId ? { thread_id: opts.threadId } : {}),
|
|
92
|
+
};
|
|
93
|
+
const resp = await fetch(`${this.baseUrl}/v1/chat`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: h,
|
|
96
|
+
body: JSON.stringify(body),
|
|
97
|
+
});
|
|
98
|
+
if (!resp.ok)
|
|
99
|
+
throw new Error(`Chat failed: ${resp.status}`);
|
|
100
|
+
if (!resp.body)
|
|
101
|
+
return;
|
|
102
|
+
const reader = resp.body.getReader();
|
|
103
|
+
const decoder = new TextDecoder();
|
|
104
|
+
let buffer = "";
|
|
105
|
+
let eventType = "";
|
|
106
|
+
while (true) {
|
|
107
|
+
const { done, value } = await reader.read();
|
|
108
|
+
if (done)
|
|
109
|
+
break;
|
|
110
|
+
buffer += decoder.decode(value, { stream: true });
|
|
111
|
+
const lines = buffer.split("\n");
|
|
112
|
+
buffer = lines.pop() ?? "";
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
if (line.startsWith("event:")) {
|
|
115
|
+
eventType = line.slice(6).trim();
|
|
116
|
+
}
|
|
117
|
+
else if (line.startsWith("data:")) {
|
|
118
|
+
const data = JSON.parse(line.slice(5).trim());
|
|
119
|
+
yield { event: eventType, data };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async getThreads(skip = 0, limit = 10) {
|
|
125
|
+
return this.authedGet("/v1/chat/threads", {
|
|
126
|
+
skip: String(skip),
|
|
127
|
+
limit: String(limit),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async getThread(threadId) {
|
|
131
|
+
return this.authedGet(`/v1/chat/threads/${threadId}`);
|
|
132
|
+
}
|
|
133
|
+
// ----------------------------------------------------------
|
|
134
|
+
// Trading
|
|
135
|
+
// ----------------------------------------------------------
|
|
136
|
+
async createSwap(request, opts) {
|
|
137
|
+
const params = {};
|
|
138
|
+
if (opts?.wait) {
|
|
139
|
+
params.wait = "true";
|
|
140
|
+
params.timeout = String(opts.timeout ?? 60);
|
|
141
|
+
}
|
|
142
|
+
return this.authedPost("/v1/swap", request, params);
|
|
143
|
+
}
|
|
144
|
+
async createLimitOrder(request, opts) {
|
|
145
|
+
const params = {};
|
|
146
|
+
if (opts?.wait) {
|
|
147
|
+
params.wait = "true";
|
|
148
|
+
params.timeout = String(opts.timeout ?? 60);
|
|
149
|
+
}
|
|
150
|
+
return this.authedPost("/v1/limit-orders", request, params);
|
|
151
|
+
}
|
|
152
|
+
async getLimitOrders(status) {
|
|
153
|
+
const params = {};
|
|
154
|
+
if (status)
|
|
155
|
+
params.status = status;
|
|
156
|
+
return this.authedGet("/v1/limit-orders", params);
|
|
157
|
+
}
|
|
158
|
+
async getLimitOrder(orderId) {
|
|
159
|
+
return this.authedGet(`/v1/limit-orders/${orderId}`);
|
|
160
|
+
}
|
|
161
|
+
async cancelLimitOrder(orderId) {
|
|
162
|
+
return this.authedDelete(`/v1/limit-orders/${orderId}`);
|
|
163
|
+
}
|
|
164
|
+
async setHooks(request) {
|
|
165
|
+
return this.authedPost("/v1/hooks", request);
|
|
166
|
+
}
|
|
167
|
+
// ----------------------------------------------------------
|
|
168
|
+
// Wallets
|
|
169
|
+
// ----------------------------------------------------------
|
|
170
|
+
async getWallets() {
|
|
171
|
+
return this.authedGet("/v1/wallets");
|
|
172
|
+
}
|
|
173
|
+
async getHoldings() {
|
|
174
|
+
return this.authedGet("/v1/holdings");
|
|
175
|
+
}
|
|
176
|
+
// ----------------------------------------------------------
|
|
177
|
+
// Earn
|
|
178
|
+
// ----------------------------------------------------------
|
|
179
|
+
async getEarnInfo() {
|
|
180
|
+
const resp = await fetch(`${this.baseUrl}/v1/earn`);
|
|
181
|
+
if (!resp.ok)
|
|
182
|
+
throw new Error(`GET /v1/earn failed: ${resp.status}`);
|
|
183
|
+
return resp.json();
|
|
184
|
+
}
|
|
185
|
+
async deposit(request, opts) {
|
|
186
|
+
const params = {};
|
|
187
|
+
if (opts?.wait) {
|
|
188
|
+
params.wait = "true";
|
|
189
|
+
params.timeout = String(opts.timeout ?? 60);
|
|
190
|
+
}
|
|
191
|
+
return this.authedPost("/v1/earn/deposit", request, params);
|
|
192
|
+
}
|
|
193
|
+
async withdraw(request, opts) {
|
|
194
|
+
const params = {};
|
|
195
|
+
if (opts?.wait) {
|
|
196
|
+
params.wait = "true";
|
|
197
|
+
params.timeout = String(opts.timeout ?? 60);
|
|
198
|
+
}
|
|
199
|
+
return this.authedPost("/v1/earn/withdraw", request, params);
|
|
200
|
+
}
|
|
201
|
+
async getPositions() {
|
|
202
|
+
return this.authedGet("/v1/earn/positions");
|
|
203
|
+
}
|
|
204
|
+
// ----------------------------------------------------------
|
|
205
|
+
// Tasks
|
|
206
|
+
// ----------------------------------------------------------
|
|
207
|
+
async getTask(taskId) {
|
|
208
|
+
return this.authedGet(`/v1/tasks/${taskId}`);
|
|
209
|
+
}
|
|
210
|
+
// ----------------------------------------------------------
|
|
211
|
+
// Notifications
|
|
212
|
+
// ----------------------------------------------------------
|
|
213
|
+
async getNotifications(opts) {
|
|
214
|
+
const params = {};
|
|
215
|
+
if (opts?.limit !== undefined)
|
|
216
|
+
params.limit = String(opts.limit);
|
|
217
|
+
if (opts?.offset !== undefined)
|
|
218
|
+
params.offset = String(opts.offset);
|
|
219
|
+
if (opts?.type)
|
|
220
|
+
params.type = opts.type;
|
|
221
|
+
return this.authedGet("/v1/notifications", params);
|
|
222
|
+
}
|
|
223
|
+
async *notificationStream() {
|
|
224
|
+
const h = await this.headers();
|
|
225
|
+
const resp = await fetch(`${this.baseUrl}/v1/notifications/stream`, {
|
|
226
|
+
headers: h,
|
|
227
|
+
});
|
|
228
|
+
if (!resp.ok || !resp.body)
|
|
229
|
+
return;
|
|
230
|
+
const reader = resp.body.getReader();
|
|
231
|
+
const decoder = new TextDecoder();
|
|
232
|
+
let buffer = "";
|
|
233
|
+
let eventType = "";
|
|
234
|
+
while (true) {
|
|
235
|
+
const { done, value } = await reader.read();
|
|
236
|
+
if (done)
|
|
237
|
+
break;
|
|
238
|
+
buffer += decoder.decode(value, { stream: true });
|
|
239
|
+
const lines = buffer.split("\n");
|
|
240
|
+
buffer = lines.pop() ?? "";
|
|
241
|
+
for (const line of lines) {
|
|
242
|
+
if (line.startsWith("event:")) {
|
|
243
|
+
eventType = line.slice(6).trim();
|
|
244
|
+
}
|
|
245
|
+
else if (line.startsWith("data:")) {
|
|
246
|
+
const data = JSON.parse(line.slice(5).trim());
|
|
247
|
+
yield { event: eventType, data };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// ----------------------------------------------------------
|
|
253
|
+
// Credits / User / Chains
|
|
254
|
+
// ----------------------------------------------------------
|
|
255
|
+
async getCredits() {
|
|
256
|
+
return this.authedGet("/v1/credits");
|
|
257
|
+
}
|
|
258
|
+
async getUser() {
|
|
259
|
+
return this.authedGet("/v1/user");
|
|
260
|
+
}
|
|
261
|
+
async getChains() {
|
|
262
|
+
const resp = await fetch(`${this.baseUrl}/v1/chains`);
|
|
263
|
+
if (!resp.ok)
|
|
264
|
+
throw new Error(`GET /v1/chains failed: ${resp.status}`);
|
|
265
|
+
return resp.json();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
exports.FereAPI = FereAPI;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Typed request/response models for the FereAI Gateway API. */
|
|
2
|
+
export interface TaskResponse {
|
|
3
|
+
task_id: string;
|
|
4
|
+
status: string;
|
|
5
|
+
poll_url?: string;
|
|
6
|
+
notification_stream?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TaskStatusResponse {
|
|
9
|
+
task_id: string;
|
|
10
|
+
status: string;
|
|
11
|
+
result?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface ChatEvent {
|
|
14
|
+
event: string;
|
|
15
|
+
data: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface TokenInfo {
|
|
18
|
+
token: string;
|
|
19
|
+
expires_in: number;
|
|
20
|
+
}
|
|
21
|
+
export interface RegisterInfo {
|
|
22
|
+
registration_id: string;
|
|
23
|
+
challenge: string;
|
|
24
|
+
}
|
|
25
|
+
export interface WalletInfo {
|
|
26
|
+
address: string;
|
|
27
|
+
chain_type: string;
|
|
28
|
+
}
|
|
29
|
+
export interface CreditsInfo {
|
|
30
|
+
credits_available: number | null;
|
|
31
|
+
}
|
|
32
|
+
export interface SwapRequest {
|
|
33
|
+
chain_id_in: number;
|
|
34
|
+
chain_id_out: number;
|
|
35
|
+
token_in: string;
|
|
36
|
+
token_out: string;
|
|
37
|
+
amount: string;
|
|
38
|
+
slippage_bps?: number;
|
|
39
|
+
dryrun?: boolean;
|
|
40
|
+
stop_loss?: {
|
|
41
|
+
price_percentage: number;
|
|
42
|
+
sell_percentage: number;
|
|
43
|
+
};
|
|
44
|
+
take_profit?: {
|
|
45
|
+
price_percentage: number;
|
|
46
|
+
sell_percentage: number;
|
|
47
|
+
};
|
|
48
|
+
cancel_conditional_orders?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface LimitOrderRequest {
|
|
51
|
+
chain_id_in: number;
|
|
52
|
+
chain_id_out: number;
|
|
53
|
+
token_in: string;
|
|
54
|
+
token_out: string;
|
|
55
|
+
amount: string;
|
|
56
|
+
price_usd_trigger: number;
|
|
57
|
+
trigger_token_address: string;
|
|
58
|
+
trigger_token_chain: string;
|
|
59
|
+
condition: "gte" | "lte";
|
|
60
|
+
slippage_bps?: number;
|
|
61
|
+
query?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface HooksRequest {
|
|
64
|
+
chain_id: number;
|
|
65
|
+
token_address: string;
|
|
66
|
+
stop_loss?: {
|
|
67
|
+
price_percentage: number;
|
|
68
|
+
sell_percentage: number;
|
|
69
|
+
};
|
|
70
|
+
take_profit?: {
|
|
71
|
+
price_percentage: number;
|
|
72
|
+
sell_percentage: number;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export interface DepositRequest {
|
|
76
|
+
amount_usdc: number;
|
|
77
|
+
position_id?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface WithdrawRequest {
|
|
80
|
+
position_id: string;
|
|
81
|
+
amount_usdc: number;
|
|
82
|
+
}
|
|
83
|
+
export declare class TaskTimeoutError extends Error {
|
|
84
|
+
readonly taskId: string;
|
|
85
|
+
readonly timeout: number;
|
|
86
|
+
constructor(taskId: string, timeout: number);
|
|
87
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Typed request/response models for the FereAI Gateway API. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.TaskTimeoutError = void 0;
|
|
5
|
+
class TaskTimeoutError extends Error {
|
|
6
|
+
taskId;
|
|
7
|
+
timeout;
|
|
8
|
+
constructor(taskId, timeout) {
|
|
9
|
+
super(`Task ${taskId} did not complete within ${timeout}s`);
|
|
10
|
+
this.name = "TaskTimeoutError";
|
|
11
|
+
this.taskId = taskId;
|
|
12
|
+
this.timeout = timeout;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.TaskTimeoutError = TaskTimeoutError;
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fereai/sdk",
|
|
3
|
+
"version": "0.1.0-dev.4",
|
|
4
|
+
"description": "TypeScript SDK for the FereAI Gateway API",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "tsc"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@noble/ed25519": "^2.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.0",
|
|
19
|
+
"typescript": "^5.5"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Fere AI <info@fere.ai>"
|
|
23
|
+
}
|