@clawrent/provider 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 +15 -0
- package/dist/index.d.ts +402 -0
- package/dist/index.js +881 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
// src/api-client.ts
|
|
2
|
+
var ApiClient = class {
|
|
3
|
+
config;
|
|
4
|
+
/** When set (e.g. by ProviderAgent.start), overrides config.token for REST auth. */
|
|
5
|
+
agentTokenOverride = null;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Set a token that overrides config.token for subsequent REST requests.
|
|
11
|
+
* Used by the in-process provider agent: once serving with an agentToken,
|
|
12
|
+
* provider REST calls (approve/list/end + internal autoApprove) authenticate
|
|
13
|
+
* as the agent owner via this token, so they work without a separate user
|
|
14
|
+
* JWT login. The backend's resolveAuth accepts the agt_clawrent_* prefix.
|
|
15
|
+
* Pass null to clear (e.g. on stop_serving).
|
|
16
|
+
*/
|
|
17
|
+
setAgentToken(token) {
|
|
18
|
+
this.agentTokenOverride = token;
|
|
19
|
+
}
|
|
20
|
+
get apiUrl() {
|
|
21
|
+
return this.config.apiUrl;
|
|
22
|
+
}
|
|
23
|
+
get wsUrl() {
|
|
24
|
+
return this.config.wsUrl;
|
|
25
|
+
}
|
|
26
|
+
get userId() {
|
|
27
|
+
return this.config.userId;
|
|
28
|
+
}
|
|
29
|
+
// --- Auth (no auth needed) ---
|
|
30
|
+
async sendVerification(email) {
|
|
31
|
+
return this.request("POST", "/api/auth/send-verification", { email }, false);
|
|
32
|
+
}
|
|
33
|
+
async registerUser(input) {
|
|
34
|
+
return this.request("POST", "/api/auth/register", input, false);
|
|
35
|
+
}
|
|
36
|
+
async login(email, password) {
|
|
37
|
+
return this.request("POST", "/api/auth/login", { email, password }, false);
|
|
38
|
+
}
|
|
39
|
+
async getMe() {
|
|
40
|
+
return this.request("GET", "/api/auth/me");
|
|
41
|
+
}
|
|
42
|
+
// --- Marketplace ---
|
|
43
|
+
async browse(query) {
|
|
44
|
+
const params = new URLSearchParams();
|
|
45
|
+
if (query?.search) params.set("search", query.search);
|
|
46
|
+
if (query?.category) params.set("category", query.category);
|
|
47
|
+
if (query?.page) params.set("page", String(query.page));
|
|
48
|
+
if (query?.limit) params.set("limit", String(query.limit));
|
|
49
|
+
const qs = params.toString();
|
|
50
|
+
return this.request("GET", `/api/marketplace/browse${qs ? `?${qs}` : ""}`);
|
|
51
|
+
}
|
|
52
|
+
async getAgent(slug) {
|
|
53
|
+
return this.request("GET", `/api/marketplace/agents/${encodeURIComponent(slug)}`);
|
|
54
|
+
}
|
|
55
|
+
// --- Sessions ---
|
|
56
|
+
async rent(options) {
|
|
57
|
+
return this.request("POST", "/api/sessions", {
|
|
58
|
+
agentId: options.agentId,
|
|
59
|
+
taskDescription: options.taskDescription,
|
|
60
|
+
grantedPermissions: options.grantedPermissions ?? {}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async getSessions(query) {
|
|
64
|
+
const params = new URLSearchParams();
|
|
65
|
+
if (query?.role) params.set("role", query.role);
|
|
66
|
+
if (query?.status) params.set("status", query.status);
|
|
67
|
+
if (query?.page) params.set("page", String(query.page));
|
|
68
|
+
if (query?.limit) params.set("limit", String(query.limit));
|
|
69
|
+
const qs = params.toString();
|
|
70
|
+
return this.request("GET", `/api/sessions${qs ? `?${qs}` : ""}`);
|
|
71
|
+
}
|
|
72
|
+
async getSession(sessionId) {
|
|
73
|
+
return this.request("GET", `/api/sessions/${encodeURIComponent(sessionId)}`);
|
|
74
|
+
}
|
|
75
|
+
async getSessionMessages(sessionId, options) {
|
|
76
|
+
const params = new URLSearchParams();
|
|
77
|
+
if (options?.since) params.set("since", options.since);
|
|
78
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
79
|
+
const qs = params.toString();
|
|
80
|
+
return this.request(
|
|
81
|
+
"GET",
|
|
82
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/messages${qs ? `?${qs}` : ""}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
/** POST a message via REST (WS fallback, e.g. session not attached after restart). */
|
|
86
|
+
async sendSessionMessage(sessionId, body) {
|
|
87
|
+
return this.request(
|
|
88
|
+
"POST",
|
|
89
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/messages`,
|
|
90
|
+
body
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
async endSession(sessionId) {
|
|
94
|
+
return this.request("POST", `/api/sessions/${encodeURIComponent(sessionId)}/end`);
|
|
95
|
+
}
|
|
96
|
+
async approveSession(sessionId) {
|
|
97
|
+
return this.request("POST", `/api/sessions/${encodeURIComponent(sessionId)}/approve`);
|
|
98
|
+
}
|
|
99
|
+
// --- Billing ---
|
|
100
|
+
async getBalance() {
|
|
101
|
+
return this.request("GET", "/api/billing/wallet");
|
|
102
|
+
}
|
|
103
|
+
async topUp(amount) {
|
|
104
|
+
return this.request("POST", "/api/billing/wallet/topup", { amount });
|
|
105
|
+
}
|
|
106
|
+
// --- Provider: Agents ---
|
|
107
|
+
async registerAgent(data) {
|
|
108
|
+
return this.request("POST", "/api/agents", data);
|
|
109
|
+
}
|
|
110
|
+
async getMyAgents(query) {
|
|
111
|
+
const params = new URLSearchParams();
|
|
112
|
+
if (query?.page) params.set("page", String(query.page));
|
|
113
|
+
if (query?.limit) params.set("limit", String(query.limit));
|
|
114
|
+
if (query?.roles) params.set("roles", query.roles);
|
|
115
|
+
const qs = params.toString();
|
|
116
|
+
return this.request("GET", `/api/agents/my${qs ? `?${qs}` : ""}`);
|
|
117
|
+
}
|
|
118
|
+
/** Resolve the current agent from agentToken. Requires agt_clawrent_ token auth. */
|
|
119
|
+
async getMyAgent() {
|
|
120
|
+
return this.request("GET", "/api/agents/me/agent");
|
|
121
|
+
}
|
|
122
|
+
async applyProvider(agentId, data) {
|
|
123
|
+
return this.request("POST", `/api/agents/${encodeURIComponent(agentId)}/apply-provider`, data);
|
|
124
|
+
}
|
|
125
|
+
/** Publish agent — simplified apply-provider with defaults, submits for admin review */
|
|
126
|
+
async publishAgent(agentId, data) {
|
|
127
|
+
return this.request("POST", `/api/agents/${encodeURIComponent(agentId)}/publish`, data ?? {});
|
|
128
|
+
}
|
|
129
|
+
/** Activate agent — verify admin-approved + WebSocket connected, then go online */
|
|
130
|
+
async activateAgent(agentId) {
|
|
131
|
+
return this.request("POST", `/api/agents/${encodeURIComponent(agentId)}/activate`);
|
|
132
|
+
}
|
|
133
|
+
async setOnlineStatus(agentId, onlineStatus) {
|
|
134
|
+
return this.request("PATCH", `/api/agents/${encodeURIComponent(agentId)}/status`, { onlineStatus });
|
|
135
|
+
}
|
|
136
|
+
async generateAgentToken(agentId) {
|
|
137
|
+
return this.request("POST", `/api/agents/${encodeURIComponent(agentId)}/token`);
|
|
138
|
+
}
|
|
139
|
+
async revokeAgentToken(agentId) {
|
|
140
|
+
return this.request("DELETE", `/api/agents/${encodeURIComponent(agentId)}/token`);
|
|
141
|
+
}
|
|
142
|
+
// --- Orders ---
|
|
143
|
+
async createOrder(data) {
|
|
144
|
+
return this.request("POST", "/api/orders", data);
|
|
145
|
+
}
|
|
146
|
+
async getOrders(query) {
|
|
147
|
+
const params = new URLSearchParams();
|
|
148
|
+
if (query?.status) params.set("status", query.status);
|
|
149
|
+
if (query?.page) params.set("page", String(query.page));
|
|
150
|
+
if (query?.limit) params.set("limit", String(query.limit));
|
|
151
|
+
const qs = params.toString();
|
|
152
|
+
return this.request("GET", `/api/orders${qs ? `?${qs}` : ""}`);
|
|
153
|
+
}
|
|
154
|
+
async getOrder(orderId) {
|
|
155
|
+
return this.request("GET", `/api/orders/${encodeURIComponent(orderId)}`);
|
|
156
|
+
}
|
|
157
|
+
async cancelOrder(orderId) {
|
|
158
|
+
return this.request("POST", `/api/orders/${encodeURIComponent(orderId)}/cancel`);
|
|
159
|
+
}
|
|
160
|
+
// --- Cart ---
|
|
161
|
+
async getCart() {
|
|
162
|
+
return this.request("GET", "/api/cart");
|
|
163
|
+
}
|
|
164
|
+
async addToCart(data) {
|
|
165
|
+
return this.request("POST", "/api/cart", data);
|
|
166
|
+
}
|
|
167
|
+
async updateCartItem(itemId, data) {
|
|
168
|
+
return this.request("PATCH", `/api/cart/${encodeURIComponent(itemId)}`, data);
|
|
169
|
+
}
|
|
170
|
+
async removeFromCart(itemId) {
|
|
171
|
+
return this.request("DELETE", `/api/cart/${encodeURIComponent(itemId)}`);
|
|
172
|
+
}
|
|
173
|
+
async clearCart() {
|
|
174
|
+
return this.request("DELETE", "/api/cart");
|
|
175
|
+
}
|
|
176
|
+
// --- Favorites ---
|
|
177
|
+
async listFavorites(query) {
|
|
178
|
+
const params = new URLSearchParams();
|
|
179
|
+
if (query?.page) params.set("page", String(query.page));
|
|
180
|
+
if (query?.limit) params.set("limit", String(query.limit));
|
|
181
|
+
const qs = params.toString();
|
|
182
|
+
return this.request("GET", `/api/favorites${qs ? `?${qs}` : ""}`);
|
|
183
|
+
}
|
|
184
|
+
async addFavorite(agentId) {
|
|
185
|
+
return this.request("POST", `/api/favorites/${encodeURIComponent(agentId)}`);
|
|
186
|
+
}
|
|
187
|
+
async removeFavorite(agentId) {
|
|
188
|
+
return this.request("DELETE", `/api/favorites/${encodeURIComponent(agentId)}`);
|
|
189
|
+
}
|
|
190
|
+
// --- Health ---
|
|
191
|
+
async health() {
|
|
192
|
+
return this.request("GET", "/api/health", void 0, false);
|
|
193
|
+
}
|
|
194
|
+
// --- Docs (MCP interface) ---
|
|
195
|
+
async getDocsTree() {
|
|
196
|
+
return this.request("GET", "/api/mcp/docs/tree", void 0, false);
|
|
197
|
+
}
|
|
198
|
+
async getDocByPath(path) {
|
|
199
|
+
return this.request("GET", `/api/mcp/docs/by-path/${encodeURI(path)}`, void 0, false);
|
|
200
|
+
}
|
|
201
|
+
async searchDocs(query) {
|
|
202
|
+
const params = new URLSearchParams({ q: query });
|
|
203
|
+
return this.request("GET", `/api/mcp/docs/search?${params.toString()}`, void 0, false);
|
|
204
|
+
}
|
|
205
|
+
async getDoc(id) {
|
|
206
|
+
return this.request("GET", `/api/mcp/docs/${encodeURIComponent(id)}`, void 0, false);
|
|
207
|
+
}
|
|
208
|
+
async createDoc(data) {
|
|
209
|
+
return this.request("POST", "/api/mcp/docs", data);
|
|
210
|
+
}
|
|
211
|
+
async updateDoc(id, data) {
|
|
212
|
+
return this.request("PATCH", `/api/mcp/docs/${encodeURIComponent(id)}`, data);
|
|
213
|
+
}
|
|
214
|
+
async deleteDoc(id) {
|
|
215
|
+
return this.request("DELETE", `/api/mcp/docs/${encodeURIComponent(id)}`);
|
|
216
|
+
}
|
|
217
|
+
async publishDoc(id) {
|
|
218
|
+
return this.request("POST", `/api/mcp/docs/${encodeURIComponent(id)}/publish`);
|
|
219
|
+
}
|
|
220
|
+
async unpublishDoc(id) {
|
|
221
|
+
return this.request("POST", `/api/mcp/docs/${encodeURIComponent(id)}/unpublish`);
|
|
222
|
+
}
|
|
223
|
+
// --- Private ---
|
|
224
|
+
async request(method, path, body, requireAuth = true) {
|
|
225
|
+
const url = `${this.config.apiUrl}${path}`;
|
|
226
|
+
const headers = {};
|
|
227
|
+
if (body !== void 0) {
|
|
228
|
+
headers["Content-Type"] = "application/json";
|
|
229
|
+
}
|
|
230
|
+
if (requireAuth) {
|
|
231
|
+
if (this.agentTokenOverride) {
|
|
232
|
+
headers["Authorization"] = `Bearer ${this.agentTokenOverride}`;
|
|
233
|
+
} else if (this.config.token) {
|
|
234
|
+
headers["Authorization"] = `Bearer ${this.config.token}`;
|
|
235
|
+
} else if (this.config.apiKey) {
|
|
236
|
+
headers["x-api-key"] = this.config.apiKey;
|
|
237
|
+
} else {
|
|
238
|
+
throw new Error("Not authenticated. Run `clawrent auth login` first.");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const response = await fetch(url, {
|
|
242
|
+
method,
|
|
243
|
+
headers,
|
|
244
|
+
body: body ? JSON.stringify(body) : void 0
|
|
245
|
+
});
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
const errBody = await response.json().catch(() => ({ message: response.statusText }));
|
|
248
|
+
throw new Error(`API error ${response.status}: ${errBody["message"] ?? JSON.stringify(errBody)}`);
|
|
249
|
+
}
|
|
250
|
+
return response.json();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/config.ts
|
|
255
|
+
import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
|
|
256
|
+
import { join } from "path";
|
|
257
|
+
import { homedir } from "os";
|
|
258
|
+
var DEFAULT_CONFIG = {
|
|
259
|
+
apiUrl: "https://clawrent.cloud",
|
|
260
|
+
wsUrl: "wss://clawrent.cloud"
|
|
261
|
+
};
|
|
262
|
+
function getConfigDir() {
|
|
263
|
+
return join(homedir(), ".clawrent");
|
|
264
|
+
}
|
|
265
|
+
function getConfigFilePath() {
|
|
266
|
+
return join(getConfigDir(), "config.json");
|
|
267
|
+
}
|
|
268
|
+
function getConfigPath() {
|
|
269
|
+
return getConfigFilePath();
|
|
270
|
+
}
|
|
271
|
+
function loadConfig() {
|
|
272
|
+
let fileConfig = {};
|
|
273
|
+
const configPath = getConfigFilePath();
|
|
274
|
+
if (existsSync(configPath)) {
|
|
275
|
+
try {
|
|
276
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
277
|
+
fileConfig = JSON.parse(raw);
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (fileConfig.apiUrl?.includes("localhost") || fileConfig.wsUrl?.includes("localhost")) {
|
|
282
|
+
delete fileConfig.apiUrl;
|
|
283
|
+
delete fileConfig.wsUrl;
|
|
284
|
+
try {
|
|
285
|
+
const cleaned = { ...fileConfig };
|
|
286
|
+
writeFileSync(configPath, JSON.stringify(cleaned, null, 2), "utf-8");
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const config = {
|
|
291
|
+
...DEFAULT_CONFIG,
|
|
292
|
+
...fileConfig
|
|
293
|
+
};
|
|
294
|
+
const envApiUrl = process.env["CLAWRENT_API_URL"];
|
|
295
|
+
const envWsUrl = process.env["CLAWRENT_WS_URL"];
|
|
296
|
+
const envToken = process.env["CLAWRENT_TOKEN"];
|
|
297
|
+
const envApiKey = process.env["CLAWRENT_API_KEY"];
|
|
298
|
+
const envUserId = process.env["CLAWRENT_USER_ID"];
|
|
299
|
+
if (envApiUrl) config.apiUrl = envApiUrl;
|
|
300
|
+
if (envWsUrl) config.wsUrl = envWsUrl;
|
|
301
|
+
if (envToken) config.token = envToken;
|
|
302
|
+
if (envApiKey) config.apiKey = envApiKey;
|
|
303
|
+
if (envUserId) config.userId = envUserId;
|
|
304
|
+
return config;
|
|
305
|
+
}
|
|
306
|
+
function saveConfig(partial) {
|
|
307
|
+
const configDir = getConfigDir();
|
|
308
|
+
if (!existsSync(configDir)) {
|
|
309
|
+
mkdirSync(configDir, { recursive: true });
|
|
310
|
+
}
|
|
311
|
+
let existing = {};
|
|
312
|
+
const configPath = getConfigFilePath();
|
|
313
|
+
if (existsSync(configPath)) {
|
|
314
|
+
try {
|
|
315
|
+
existing = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const merged = { ...existing, ...partial };
|
|
320
|
+
writeFileSync(configPath, JSON.stringify(merged, null, 2), "utf-8");
|
|
321
|
+
}
|
|
322
|
+
function clearConfig() {
|
|
323
|
+
const configPath = getConfigFilePath();
|
|
324
|
+
if (existsSync(configPath)) {
|
|
325
|
+
unlinkSync(configPath);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/session-manager.ts
|
|
330
|
+
import { EventEmitter } from "events";
|
|
331
|
+
import WebSocket from "ws";
|
|
332
|
+
var TERMINAL_CLOSE_CODES = /* @__PURE__ */ new Set([4e3, 4001, 4002, 4003, 4004]);
|
|
333
|
+
var SessionManager = class extends EventEmitter {
|
|
334
|
+
sessions = /* @__PURE__ */ new Map();
|
|
335
|
+
wsUrl;
|
|
336
|
+
heartbeatInterval;
|
|
337
|
+
maxReconnectDelay;
|
|
338
|
+
maxReconnectAttempts;
|
|
339
|
+
agentId;
|
|
340
|
+
constructor(wsUrl, heartbeatInterval = 25e3, maxReconnectDelay = 3e4, maxReconnectAttempts = 5) {
|
|
341
|
+
super();
|
|
342
|
+
this.wsUrl = wsUrl;
|
|
343
|
+
this.heartbeatInterval = heartbeatInterval;
|
|
344
|
+
this.maxReconnectDelay = maxReconnectDelay;
|
|
345
|
+
this.maxReconnectAttempts = maxReconnectAttempts;
|
|
346
|
+
}
|
|
347
|
+
/** Connect to a session via WebSocket as provider */
|
|
348
|
+
connect(sessionId, sessionToken) {
|
|
349
|
+
if (this.sessions.has(sessionId)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const url = `${this.wsUrl}/ws/session?sessionId=${sessionId}&token=${sessionToken}&role=provider`;
|
|
353
|
+
const ws = new WebSocket(url);
|
|
354
|
+
const conn = {
|
|
355
|
+
sessionId,
|
|
356
|
+
sessionToken,
|
|
357
|
+
ws,
|
|
358
|
+
heartbeatTimer: null,
|
|
359
|
+
reconnectAttempts: 0
|
|
360
|
+
};
|
|
361
|
+
this.sessions.set(sessionId, conn);
|
|
362
|
+
ws.on("open", () => {
|
|
363
|
+
conn.reconnectAttempts = 0;
|
|
364
|
+
conn.heartbeatTimer = setInterval(() => {
|
|
365
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
366
|
+
ws.send(JSON.stringify({ type: "system.heartbeat", payload: {} }));
|
|
367
|
+
}
|
|
368
|
+
}, this.heartbeatInterval);
|
|
369
|
+
this.emit("session:connected", sessionId);
|
|
370
|
+
});
|
|
371
|
+
ws.on("message", (raw) => {
|
|
372
|
+
try {
|
|
373
|
+
const message = JSON.parse(raw.toString());
|
|
374
|
+
if (message["type"] === "system.heartbeat_ack") return;
|
|
375
|
+
this.emit("session:message", sessionId, message);
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
ws.on("close", (code, reason) => {
|
|
380
|
+
this.clearHeartbeat(conn);
|
|
381
|
+
if (TERMINAL_CLOSE_CODES.has(code)) {
|
|
382
|
+
this.sessions.delete(sessionId);
|
|
383
|
+
this.emit("session:dead", sessionId, `session rejected (code ${code}: ${reason.toString()})`);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (code !== 1e3) {
|
|
387
|
+
if (conn.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
388
|
+
this.sessions.delete(sessionId);
|
|
389
|
+
this.emit("session:dead", sessionId, `max reconnect attempts (${this.maxReconnectAttempts}) reached`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const delay = Math.min(
|
|
393
|
+
1e3 * Math.pow(2, conn.reconnectAttempts),
|
|
394
|
+
this.maxReconnectDelay
|
|
395
|
+
);
|
|
396
|
+
conn.reconnectAttempts++;
|
|
397
|
+
this.emit("session:reconnecting", sessionId, delay);
|
|
398
|
+
setTimeout(() => {
|
|
399
|
+
this.sessions.delete(sessionId);
|
|
400
|
+
this.connect(sessionId, sessionToken);
|
|
401
|
+
}, delay);
|
|
402
|
+
} else {
|
|
403
|
+
this.sessions.delete(sessionId);
|
|
404
|
+
this.emit("session:disconnected", sessionId, reason.toString());
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
ws.on("error", (err) => {
|
|
408
|
+
this.emit("session:error", sessionId, err);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/** Send a message to a specific session, auto-wrapping with protocol envelope */
|
|
412
|
+
send(sessionId, message) {
|
|
413
|
+
const conn = this.sessions.get(sessionId);
|
|
414
|
+
if (!conn || conn.ws.readyState !== WebSocket.OPEN) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
const envelope = {
|
|
418
|
+
id: message["id"] ?? `msg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
|
419
|
+
sessionId: message["sessionId"] ?? sessionId,
|
|
420
|
+
timestamp: message["timestamp"] ?? Date.now(),
|
|
421
|
+
sender: message["sender"] ?? { role: "provider", agentId: this.agentId ?? "unknown" },
|
|
422
|
+
type: message["type"] ?? "result.success",
|
|
423
|
+
payload: message["payload"] ?? {}
|
|
424
|
+
};
|
|
425
|
+
conn.ws.send(JSON.stringify(envelope));
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
/** Disconnect a specific session */
|
|
429
|
+
disconnect(sessionId) {
|
|
430
|
+
const conn = this.sessions.get(sessionId);
|
|
431
|
+
if (!conn) return;
|
|
432
|
+
this.clearHeartbeat(conn);
|
|
433
|
+
if (conn.ws.readyState === WebSocket.OPEN || conn.ws.readyState === WebSocket.CONNECTING) {
|
|
434
|
+
conn.ws.close(1e3, "Provider disconnecting");
|
|
435
|
+
}
|
|
436
|
+
this.sessions.delete(sessionId);
|
|
437
|
+
}
|
|
438
|
+
/** Disconnect all sessions */
|
|
439
|
+
disconnectAll() {
|
|
440
|
+
for (const sessionId of this.sessions.keys()) {
|
|
441
|
+
this.disconnect(sessionId);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/** Get active session count */
|
|
445
|
+
get activeCount() {
|
|
446
|
+
return this.sessions.size;
|
|
447
|
+
}
|
|
448
|
+
/** Check if a session is connected */
|
|
449
|
+
isConnected(sessionId) {
|
|
450
|
+
const conn = this.sessions.get(sessionId);
|
|
451
|
+
return conn?.ws.readyState === WebSocket.OPEN;
|
|
452
|
+
}
|
|
453
|
+
clearHeartbeat(conn) {
|
|
454
|
+
if (conn.heartbeatTimer) {
|
|
455
|
+
clearInterval(conn.heartbeatTimer);
|
|
456
|
+
conn.heartbeatTimer = null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// src/cursor.ts
|
|
462
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
|
|
463
|
+
import { dirname } from "path";
|
|
464
|
+
var InMemoryCursorStore = class {
|
|
465
|
+
map = /* @__PURE__ */ new Map();
|
|
466
|
+
get(sessionId) {
|
|
467
|
+
return this.map.get(sessionId) ?? null;
|
|
468
|
+
}
|
|
469
|
+
set(sessionId, createdAt) {
|
|
470
|
+
const prev = this.map.get(sessionId);
|
|
471
|
+
if (prev === void 0 || createdAt > prev) this.map.set(sessionId, createdAt);
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
var FileCursorStore = class {
|
|
475
|
+
constructor(path) {
|
|
476
|
+
this.path = path;
|
|
477
|
+
}
|
|
478
|
+
path;
|
|
479
|
+
cache = null;
|
|
480
|
+
load() {
|
|
481
|
+
if (this.cache) return this.cache;
|
|
482
|
+
if (!existsSync2(this.path)) {
|
|
483
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
484
|
+
return this.cache;
|
|
485
|
+
}
|
|
486
|
+
const raw = readFileSync2(this.path, "utf8");
|
|
487
|
+
try {
|
|
488
|
+
this.cache = new Map(Object.entries(JSON.parse(raw)));
|
|
489
|
+
} catch {
|
|
490
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
491
|
+
}
|
|
492
|
+
return this.cache;
|
|
493
|
+
}
|
|
494
|
+
persist() {
|
|
495
|
+
if (!this.cache) return;
|
|
496
|
+
mkdirSync2(dirname(this.path), { recursive: true });
|
|
497
|
+
writeFileSync2(this.path, JSON.stringify(Object.fromEntries(this.cache), null, 2), "utf8");
|
|
498
|
+
}
|
|
499
|
+
get(sessionId) {
|
|
500
|
+
return this.load().get(sessionId) ?? null;
|
|
501
|
+
}
|
|
502
|
+
set(sessionId, createdAt) {
|
|
503
|
+
const m = this.load();
|
|
504
|
+
const prev = m.get(sessionId);
|
|
505
|
+
if (prev === void 0 || createdAt > prev) {
|
|
506
|
+
m.set(sessionId, createdAt);
|
|
507
|
+
this.persist();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// src/helpers.ts
|
|
513
|
+
async function resumeActiveSessions(client, sessionManager) {
|
|
514
|
+
const res = await client.getSessions({ role: "provider", status: "active" });
|
|
515
|
+
const list = res?.data ?? [];
|
|
516
|
+
const attached = [];
|
|
517
|
+
for (const s of list) {
|
|
518
|
+
const sessionId = s["sessionId"] ?? s["id"];
|
|
519
|
+
const sessionToken = s["sessionToken"];
|
|
520
|
+
if (typeof sessionId !== "string" || typeof sessionToken !== "string") continue;
|
|
521
|
+
attached.push({
|
|
522
|
+
sessionId,
|
|
523
|
+
sessionToken,
|
|
524
|
+
taskDescription: typeof s["taskDescription"] === "string" ? s["taskDescription"] : void 0,
|
|
525
|
+
consumerUserId: typeof s["consumerUserId"] === "string" ? s["consumerUserId"] : void 0,
|
|
526
|
+
slotIndex: typeof s["slotIndex"] === "number" ? s["slotIndex"] : void 0
|
|
527
|
+
});
|
|
528
|
+
sessionManager.connect(sessionId, sessionToken);
|
|
529
|
+
}
|
|
530
|
+
return attached;
|
|
531
|
+
}
|
|
532
|
+
function diffSessionStates(prev, curr) {
|
|
533
|
+
const prevMap = new Map(prev.map((s) => [s.sessionId, s]));
|
|
534
|
+
const currMap = new Map(curr.map((s) => [s.sessionId, s]));
|
|
535
|
+
const newPending = [];
|
|
536
|
+
const activated = [];
|
|
537
|
+
const ended = [];
|
|
538
|
+
for (const [id, c] of currMap) {
|
|
539
|
+
const p = prevMap.get(id);
|
|
540
|
+
if (!p) {
|
|
541
|
+
if (c.status === "pending_approval") newPending.push(c);
|
|
542
|
+
} else if (p.status === "pending_approval" && c.status === "active") {
|
|
543
|
+
activated.push(c);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
for (const [id, p] of prevMap) {
|
|
547
|
+
if (!currMap.has(id)) ended.push(p);
|
|
548
|
+
}
|
|
549
|
+
return { newPending, activated, ended };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/provider-client.ts
|
|
553
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
554
|
+
import WebSocket2 from "ws";
|
|
555
|
+
import { wsAgentControlEventSchema } from "@clawrent/protocol";
|
|
556
|
+
var ProviderClient = class extends EventEmitter2 {
|
|
557
|
+
client;
|
|
558
|
+
cursor;
|
|
559
|
+
heartbeatIntervalMs;
|
|
560
|
+
maxReconnectAttempts;
|
|
561
|
+
autoApprove;
|
|
562
|
+
agentToken;
|
|
563
|
+
agentId = null;
|
|
564
|
+
agentWs = null;
|
|
565
|
+
sessionManager = null;
|
|
566
|
+
heartbeatTimer = null;
|
|
567
|
+
_running = false;
|
|
568
|
+
activeSessions = /* @__PURE__ */ new Map();
|
|
569
|
+
/**
|
|
570
|
+
* Per-session in-flight promise chain: each `session:message` for the same
|
|
571
|
+
* sessionId is chained onto the previous one so calls serialize. Without this,
|
|
572
|
+
* two socket frames arriving in the same tick would both pass the cursor
|
|
573
|
+
* dedupe check (the first call's `cursor.set` is queued behind its `await`
|
|
574
|
+
* and hasn't run when the second call reads the cursor) and onMessage would
|
|
575
|
+
* fire twice. The Map entry is cleared when the tail settles so it doesn't
|
|
576
|
+
* grow unbounded.
|
|
577
|
+
*/
|
|
578
|
+
inflight = /* @__PURE__ */ new Map();
|
|
579
|
+
/** Callbacks captured at start(); used by handleAgentMessage/handleSessionMessage. */
|
|
580
|
+
boundCallbacks = null;
|
|
581
|
+
constructor(opts) {
|
|
582
|
+
super();
|
|
583
|
+
const config = {
|
|
584
|
+
apiUrl: opts.apiUrl ?? "https://clawrent.cloud",
|
|
585
|
+
wsUrl: opts.wsUrl ?? "wss://clawrent.cloud"
|
|
586
|
+
};
|
|
587
|
+
this.client = new ApiClient(config);
|
|
588
|
+
this.client.setAgentToken(opts.agentToken);
|
|
589
|
+
this.agentToken = opts.agentToken;
|
|
590
|
+
this.cursor = opts.cursorStore ?? new InMemoryCursorStore();
|
|
591
|
+
this.heartbeatIntervalMs = opts.heartbeatIntervalMs ?? 25e3;
|
|
592
|
+
this.maxReconnectAttempts = opts.maxReconnectAttempts ?? 5;
|
|
593
|
+
this.autoApprove = opts.autoApprove ?? true;
|
|
594
|
+
}
|
|
595
|
+
get running() {
|
|
596
|
+
return this._running;
|
|
597
|
+
}
|
|
598
|
+
get currentAgentId() {
|
|
599
|
+
return this.agentId;
|
|
600
|
+
}
|
|
601
|
+
/** Cursor store used for per-session message dedupe (wired in Task 6b). */
|
|
602
|
+
get cursorStore() {
|
|
603
|
+
return this.cursor;
|
|
604
|
+
}
|
|
605
|
+
/** Whether sessions are auto-approved on arrival (wired in Task 6b). */
|
|
606
|
+
get isAutoApprove() {
|
|
607
|
+
return this.autoApprove;
|
|
608
|
+
}
|
|
609
|
+
async start(callbacks) {
|
|
610
|
+
if (this._running) throw new Error("ProviderClient already started");
|
|
611
|
+
if (callbacks.agentId) {
|
|
612
|
+
this.agentId = callbacks.agentId;
|
|
613
|
+
} else {
|
|
614
|
+
const me = await this.client.getMyAgent();
|
|
615
|
+
this.agentId = me["id"] ?? me["agentId"] ?? null;
|
|
616
|
+
}
|
|
617
|
+
if (!this.agentId) throw new Error("Could not resolve agentId (pass callbacks.agentId or ensure token is valid)");
|
|
618
|
+
this.sessionManager = new SessionManager(this.client.wsUrl, this.heartbeatIntervalMs, 3e4, this.maxReconnectAttempts);
|
|
619
|
+
this.sessionManager.agentId = this.agentId;
|
|
620
|
+
this.bindSessionManager(callbacks);
|
|
621
|
+
await this.connectAgent();
|
|
622
|
+
try {
|
|
623
|
+
await this.client.activateAgent(this.agentId);
|
|
624
|
+
} catch (err) {
|
|
625
|
+
this.emit("agent:warning", `activation failed: ${err.message}`);
|
|
626
|
+
}
|
|
627
|
+
this._running = true;
|
|
628
|
+
this.emit("agent:started", this.agentId);
|
|
629
|
+
void this.resumeActive(callbacks);
|
|
630
|
+
}
|
|
631
|
+
bindSessionManager(callbacks) {
|
|
632
|
+
this.boundCallbacks = callbacks;
|
|
633
|
+
const sm = this.sessionManager;
|
|
634
|
+
if (!sm) return;
|
|
635
|
+
sm.on("session:connected", (sid) => this.emit("session:connected", sid));
|
|
636
|
+
sm.on("session:disconnected", (sid, reason) => this.emit("session:disconnected", sid, reason));
|
|
637
|
+
sm.on("session:message", (sid, message) => {
|
|
638
|
+
const prev = this.inflight.get(sid) ?? Promise.resolve();
|
|
639
|
+
const next = prev.then(() => this.handleSessionMessage(sid, message)).catch((err) => {
|
|
640
|
+
this.emit("session:error", sid, err);
|
|
641
|
+
});
|
|
642
|
+
this.inflight.set(sid, next);
|
|
643
|
+
next.finally(() => {
|
|
644
|
+
if (this.inflight.get(sid) === next) this.inflight.delete(sid);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
sm.on("session:dead", (sid, reason) => {
|
|
648
|
+
const active = this.activeSessions.get(sid) ?? { sessionId: sid, sessionToken: "" };
|
|
649
|
+
this.activeSessions.delete(sid);
|
|
650
|
+
callbacks.onSessionEnded?.(active, reason);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
async resumeActive(callbacks) {
|
|
654
|
+
if (!this.sessionManager) return;
|
|
655
|
+
try {
|
|
656
|
+
const sessions = await resumeActiveSessions(this.client, this.sessionManager);
|
|
657
|
+
for (const s of sessions) {
|
|
658
|
+
this.activeSessions.set(s.sessionId, s);
|
|
659
|
+
callbacks.onSessionNew?.(s);
|
|
660
|
+
this.emit("session:reattached", s.sessionId);
|
|
661
|
+
}
|
|
662
|
+
} catch (err) {
|
|
663
|
+
this.emit("agent:warning", `resume failed: ${err.message}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
connectAgent() {
|
|
667
|
+
return new Promise((resolve, reject) => {
|
|
668
|
+
const url = `${this.client.wsUrl}/ws/agent?token=${encodeURIComponent(this.agentToken)}`;
|
|
669
|
+
const ws = new WebSocket2(url);
|
|
670
|
+
this.agentWs = ws;
|
|
671
|
+
ws.on("open", () => {
|
|
672
|
+
this.heartbeatTimer = setInterval(() => {
|
|
673
|
+
if (ws.readyState === WebSocket2.OPEN) {
|
|
674
|
+
ws.send(JSON.stringify({ type: "system.heartbeat", payload: {} }));
|
|
675
|
+
}
|
|
676
|
+
}, this.heartbeatIntervalMs);
|
|
677
|
+
this.emit("agent:connected");
|
|
678
|
+
resolve();
|
|
679
|
+
});
|
|
680
|
+
ws.on("error", reject);
|
|
681
|
+
ws.on("close", (code, reason) => {
|
|
682
|
+
this.emit("agent:disconnected", code, reason.toString());
|
|
683
|
+
});
|
|
684
|
+
ws.on("message", (raw) => {
|
|
685
|
+
void this.handleAgentMessage(raw).catch((err) => {
|
|
686
|
+
this.emit("agent:warning", `agent message handler failed: ${err.message}`);
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Parses an inbound /ws/agent frame and routes it.
|
|
693
|
+
*
|
|
694
|
+
* `session.new` / `session.approved` are validated against the protocol
|
|
695
|
+
* discriminated-union schema (fields under `payload`). Other /ws/agent frame
|
|
696
|
+
* types pushed by the backend (agent.connected / agent.status_updated /
|
|
697
|
+
* system.error / system.heartbeat_ack — see apps/platform-api ws-agent-handler.ts)
|
|
698
|
+
* are NOT in wsAgentControlEventSchema, so we route them by `type` directly
|
|
699
|
+
* instead of forcing them through the schema (which would reject them).
|
|
700
|
+
* Unknown types are ignored gracefully.
|
|
701
|
+
*
|
|
702
|
+
* Note: backend does NOT push `session.ended` on /ws/agent; session
|
|
703
|
+
* terminations arrive as `system.session_ended` on /ws/session.
|
|
704
|
+
*/
|
|
705
|
+
async handleAgentMessage(raw) {
|
|
706
|
+
let frame;
|
|
707
|
+
try {
|
|
708
|
+
frame = JSON.parse(raw.toString());
|
|
709
|
+
} catch {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (typeof frame !== "object" || frame === null) return;
|
|
713
|
+
const type = frame["type"];
|
|
714
|
+
if (type === "session.new" || type === "session.approved") {
|
|
715
|
+
let parsed;
|
|
716
|
+
try {
|
|
717
|
+
parsed = wsAgentControlEventSchema.parse(frame);
|
|
718
|
+
} catch {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (parsed.type === "session.new") {
|
|
722
|
+
await this.onSessionNew(parsed.payload);
|
|
723
|
+
} else {
|
|
724
|
+
this.onSessionApproved(parsed.payload);
|
|
725
|
+
}
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const payload = frame["payload"];
|
|
729
|
+
switch (type) {
|
|
730
|
+
case "system.heartbeat_ack":
|
|
731
|
+
return;
|
|
732
|
+
// heartbeat acknowledgement — ignore
|
|
733
|
+
case "system.error":
|
|
734
|
+
this.emit("agent:warning", `server error: ${JSON.stringify(payload) ?? "no payload"}`);
|
|
735
|
+
return;
|
|
736
|
+
case "agent.connected":
|
|
737
|
+
this.emit("agent:connected", payload);
|
|
738
|
+
return;
|
|
739
|
+
case "agent.status_updated":
|
|
740
|
+
this.emit("agent:status", payload);
|
|
741
|
+
return;
|
|
742
|
+
default:
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
/** session.new: register, notify host, optionally auto-approve + connect /ws/session. */
|
|
747
|
+
async onSessionNew(payload) {
|
|
748
|
+
const sessionId = payload.sessionId;
|
|
749
|
+
const sessionToken = payload.sessionToken;
|
|
750
|
+
const active = {
|
|
751
|
+
sessionId,
|
|
752
|
+
sessionToken: sessionToken ?? "",
|
|
753
|
+
taskDescription: payload.taskDescription,
|
|
754
|
+
consumerUserId: payload.consumerUserId
|
|
755
|
+
};
|
|
756
|
+
this.activeSessions.set(sessionId, active);
|
|
757
|
+
this.boundCallbacks?.onSessionNew?.(active);
|
|
758
|
+
this.emit("session:new", active);
|
|
759
|
+
const shouldApprove = this.autoApprove ? true : this.boundCallbacks?.onPendingApproval ? await this.boundCallbacks.onPendingApproval(active) : false;
|
|
760
|
+
if (!shouldApprove) return;
|
|
761
|
+
if (this.autoApprove) {
|
|
762
|
+
try {
|
|
763
|
+
await this.client.approveSession(sessionId);
|
|
764
|
+
} catch (err) {
|
|
765
|
+
this.emit("agent:warning", `approve failed for ${sessionId}: ${err.message}`);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (sessionToken) {
|
|
770
|
+
this.sessionManager?.connect(sessionId, sessionToken);
|
|
771
|
+
} else {
|
|
772
|
+
this.emit("agent:warning", `session.new for ${sessionId} carried no sessionToken; deferring /ws/session connect until session.approved`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/** session.approved: ensure tracked + connect /ws/session (idempotent). */
|
|
776
|
+
onSessionApproved(payload) {
|
|
777
|
+
const sessionId = payload.sessionId;
|
|
778
|
+
const sessionToken = payload.sessionToken;
|
|
779
|
+
const existing = this.activeSessions.get(sessionId);
|
|
780
|
+
const active = existing ?? {
|
|
781
|
+
sessionId,
|
|
782
|
+
sessionToken: sessionToken ?? ""
|
|
783
|
+
};
|
|
784
|
+
if (sessionToken) active.sessionToken = sessionToken;
|
|
785
|
+
this.activeSessions.set(sessionId, active);
|
|
786
|
+
this.emit("session:approved", active);
|
|
787
|
+
if (sessionToken) {
|
|
788
|
+
this.sessionManager?.connect(sessionId, sessionToken);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Dedupes an inbound /ws/session message by per-session cursor, then invokes
|
|
793
|
+
* the host's onMessage callback. Public (no underscore prefix) so the
|
|
794
|
+
* integration test can drive the cursor/dedupe path directly as a hook.
|
|
795
|
+
*
|
|
796
|
+
* At-least-once invariant (Task 6b fix round 1): the cursor is advanced ONLY
|
|
797
|
+
* AFTER `await onMessage` completes successfully. If onMessage throws, the
|
|
798
|
+
* cursor is left un-advanced and `session:error` is emitted — so a future
|
|
799
|
+
* redelivery (e.g. backend replay on restart) will pass the dedupe check and
|
|
800
|
+
* re-process the message. onMessage MUST be idempotent precisely because of
|
|
801
|
+
* this redelivery semantics. The per-session in-flight chain in
|
|
802
|
+
* `bindSessionManager`'s `session:message` handler serializes concurrent
|
|
803
|
+
* frames for the same session so the cursor-after-success commit has a chance
|
|
804
|
+
* to run before the next frame's dedupe check reads the cursor.
|
|
805
|
+
*
|
|
806
|
+
* `createdAt` is canonical UTC ISO, taken from `_meta.timestamp` (backend
|
|
807
|
+
* pushes ISO `...Z`) with a `new Date(message.timestamp).toISOString()`
|
|
808
|
+
* fallback (ms epoch).
|
|
809
|
+
*/
|
|
810
|
+
async handleSessionMessage(sessionId, message) {
|
|
811
|
+
const onMessage = this.boundCallbacks?.onMessage;
|
|
812
|
+
if (!onMessage) return;
|
|
813
|
+
const meta = message["_meta"];
|
|
814
|
+
const metaTs = meta?.timestamp;
|
|
815
|
+
const msgTs = message["timestamp"];
|
|
816
|
+
const createdAt = typeof metaTs === "string" ? metaTs : typeof msgTs === "number" ? new Date(msgTs).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
817
|
+
const last = this.cursor.get(sessionId);
|
|
818
|
+
if (last && createdAt <= last) return;
|
|
819
|
+
const session = this.activeSessions.get(sessionId) ?? { sessionId, sessionToken: "" };
|
|
820
|
+
try {
|
|
821
|
+
await onMessage(session, message);
|
|
822
|
+
} catch (err) {
|
|
823
|
+
this.emit("session:error", sessionId, err);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
this.cursor.set(sessionId, createdAt);
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Send an outbound message to a session. Prefers the /ws/session socket when
|
|
830
|
+
* it is OPEN (low latency, no HTTP overhead); falls back to the REST
|
|
831
|
+
* `POST /api/sessions/:id/messages` endpoint when the socket is absent or not
|
|
832
|
+
* yet open (e.g. before start(), during reconnect, or for sessions not yet
|
|
833
|
+
* attached). Returns the transport actually used so callers can observe it.
|
|
834
|
+
*
|
|
835
|
+
* Note: this method does NOT throw when WS is unavailable — it silently falls
|
|
836
|
+
* back to REST. REST errors propagate to the caller as-is.
|
|
837
|
+
*/
|
|
838
|
+
async send(sessionId, message) {
|
|
839
|
+
const sm = this.sessionManager;
|
|
840
|
+
if (sm?.isConnected(sessionId)) {
|
|
841
|
+
const ok = sm.send(sessionId, message);
|
|
842
|
+
if (ok) return { via: "ws" };
|
|
843
|
+
}
|
|
844
|
+
await this.client.sendSessionMessage(sessionId, message);
|
|
845
|
+
return { via: "rest" };
|
|
846
|
+
}
|
|
847
|
+
stop() {
|
|
848
|
+
this._running = false;
|
|
849
|
+
if (this.heartbeatTimer) {
|
|
850
|
+
clearInterval(this.heartbeatTimer);
|
|
851
|
+
this.heartbeatTimer = null;
|
|
852
|
+
}
|
|
853
|
+
this.sessionManager?.disconnectAll();
|
|
854
|
+
if (this.agentWs) {
|
|
855
|
+
this.agentWs.close(1e3, "Provider stopping");
|
|
856
|
+
this.agentWs = null;
|
|
857
|
+
}
|
|
858
|
+
this.activeSessions.clear();
|
|
859
|
+
this.client.setAgentToken(null);
|
|
860
|
+
this.emit("stopped");
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
// src/index.ts
|
|
865
|
+
var PROVIDER_PACKAGE_VERSION = "0.1.0";
|
|
866
|
+
export {
|
|
867
|
+
ApiClient,
|
|
868
|
+
FileCursorStore,
|
|
869
|
+
InMemoryCursorStore,
|
|
870
|
+
PROVIDER_PACKAGE_VERSION,
|
|
871
|
+
ProviderClient,
|
|
872
|
+
SessionManager,
|
|
873
|
+
clearConfig,
|
|
874
|
+
diffSessionStates,
|
|
875
|
+
getConfigDir,
|
|
876
|
+
getConfigPath,
|
|
877
|
+
loadConfig,
|
|
878
|
+
resumeActiveSessions,
|
|
879
|
+
saveConfig
|
|
880
|
+
};
|
|
881
|
+
//# sourceMappingURL=index.js.map
|