@advicenxt/sbp-client 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/package.json +50 -0
- package/src/index.test.ts +16 -0
- package/src/index.ts +526 -0
- package/tsconfig.json +19 -0
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@advicenxt/sbp-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Stigmergic Blackboard Protocol - TypeScript/JavaScript Client",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"sbp",
|
|
22
|
+
"stigmergy",
|
|
23
|
+
"blackboard",
|
|
24
|
+
"multi-agent",
|
|
25
|
+
"coordination",
|
|
26
|
+
"sse"
|
|
27
|
+
],
|
|
28
|
+
"author": "SBP Contributors",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/AdviceNXT/sbp.git",
|
|
32
|
+
"directory": "packages/client-ts"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://sbp.dev",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/AdviceNXT/sbp/issues"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^20.11.0",
|
|
41
|
+
"typescript": "^5.3.3",
|
|
42
|
+
"vitest": "^1.2.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"typescript": ">=4.7"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { SbpClient, SbpAgent } from './index.js';
|
|
3
|
+
|
|
4
|
+
describe('SbpClient', () => {
|
|
5
|
+
it('should be instantiable', () => {
|
|
6
|
+
const client = new SbpClient();
|
|
7
|
+
expect(client).toBeDefined();
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('SbpAgent', () => {
|
|
12
|
+
it('should be instantiable', () => {
|
|
13
|
+
const agent = new SbpAgent('test-agent');
|
|
14
|
+
expect(agent).toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SBP Client - TypeScript/JavaScript SDK
|
|
3
|
+
* Streamable HTTP with SSE (following MCP transport patterns)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// TYPES
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export interface ExponentialDecay {
|
|
11
|
+
type: "exponential";
|
|
12
|
+
half_life_ms: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LinearDecay {
|
|
16
|
+
type: "linear";
|
|
17
|
+
rate_per_ms: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ImmortalDecay {
|
|
21
|
+
type: "immortal";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type DecayModel = ExponentialDecay | LinearDecay | ImmortalDecay;
|
|
25
|
+
|
|
26
|
+
export interface PheromoneSnapshot {
|
|
27
|
+
id: string;
|
|
28
|
+
trail: string;
|
|
29
|
+
type: string;
|
|
30
|
+
current_intensity: number;
|
|
31
|
+
payload: Record<string, unknown>;
|
|
32
|
+
age_ms: number;
|
|
33
|
+
tags: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ThresholdCondition {
|
|
37
|
+
type: "threshold";
|
|
38
|
+
trail: string;
|
|
39
|
+
signal_type: string;
|
|
40
|
+
aggregation: "sum" | "max" | "avg" | "count" | "any";
|
|
41
|
+
operator: ">=" | ">" | "<=" | "<" | "==" | "!=";
|
|
42
|
+
value: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CompositeCondition {
|
|
46
|
+
type: "composite";
|
|
47
|
+
operator: "and" | "or" | "not";
|
|
48
|
+
conditions: ScentCondition[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RateCondition {
|
|
52
|
+
type: "rate";
|
|
53
|
+
trail: string;
|
|
54
|
+
signal_type: string;
|
|
55
|
+
metric: "emissions_per_second" | "intensity_delta";
|
|
56
|
+
window_ms: number;
|
|
57
|
+
operator: ">=" | ">" | "<=" | "<";
|
|
58
|
+
value: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type ScentCondition = ThresholdCondition | CompositeCondition | RateCondition;
|
|
62
|
+
|
|
63
|
+
export interface EmitResult {
|
|
64
|
+
pheromone_id: string;
|
|
65
|
+
action: "created" | "reinforced" | "replaced" | "merged";
|
|
66
|
+
previous_intensity?: number;
|
|
67
|
+
new_intensity: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface AggregateStats {
|
|
71
|
+
count: number;
|
|
72
|
+
sum_intensity: number;
|
|
73
|
+
max_intensity: number;
|
|
74
|
+
avg_intensity: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface SniffResult {
|
|
78
|
+
timestamp: number;
|
|
79
|
+
pheromones: PheromoneSnapshot[];
|
|
80
|
+
aggregates: Record<string, AggregateStats>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface RegisterScentResult {
|
|
84
|
+
scent_id: string;
|
|
85
|
+
status: "registered" | "updated";
|
|
86
|
+
current_condition_state: {
|
|
87
|
+
met: boolean;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TriggerPayload {
|
|
92
|
+
scent_id: string;
|
|
93
|
+
triggered_at: number;
|
|
94
|
+
condition_snapshot: Record<string, { value: number; pheromone_ids: string[] }>;
|
|
95
|
+
context_pheromones: PheromoneSnapshot[];
|
|
96
|
+
activation_payload: Record<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// HELPER FUNCTIONS
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
export function exponentialDecay(halfLifeMs: number): ExponentialDecay {
|
|
104
|
+
return { type: "exponential", half_life_ms: halfLifeMs };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function linearDecay(ratePerMs: number): LinearDecay {
|
|
108
|
+
return { type: "linear", rate_per_ms: ratePerMs };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function immortal(): ImmortalDecay {
|
|
112
|
+
return { type: "immortal" };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function threshold(
|
|
116
|
+
trail: string,
|
|
117
|
+
signalType: string,
|
|
118
|
+
operator: ThresholdCondition["operator"],
|
|
119
|
+
value: number,
|
|
120
|
+
aggregation: ThresholdCondition["aggregation"] = "max"
|
|
121
|
+
): ThresholdCondition {
|
|
122
|
+
return {
|
|
123
|
+
type: "threshold",
|
|
124
|
+
trail,
|
|
125
|
+
signal_type: signalType,
|
|
126
|
+
aggregation,
|
|
127
|
+
operator,
|
|
128
|
+
value,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function and(...conditions: ScentCondition[]): CompositeCondition {
|
|
133
|
+
return { type: "composite", operator: "and", conditions };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function or(...conditions: ScentCondition[]): CompositeCondition {
|
|
137
|
+
return { type: "composite", operator: "or", conditions };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function not(condition: ScentCondition): CompositeCondition {
|
|
141
|
+
return { type: "composite", operator: "not", conditions: [condition] };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// CLIENT
|
|
146
|
+
// ============================================================================
|
|
147
|
+
|
|
148
|
+
export interface SbpClientOptions {
|
|
149
|
+
url?: string;
|
|
150
|
+
agentId?: string;
|
|
151
|
+
timeout?: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface EmitOptions {
|
|
155
|
+
decay?: DecayModel;
|
|
156
|
+
payload?: Record<string, unknown>;
|
|
157
|
+
tags?: string[];
|
|
158
|
+
mergeStrategy?: "reinforce" | "replace" | "max" | "add" | "new";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface SniffOptions {
|
|
162
|
+
trails?: string[];
|
|
163
|
+
types?: string[];
|
|
164
|
+
minIntensity?: number;
|
|
165
|
+
limit?: number;
|
|
166
|
+
includeEvaporated?: boolean;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface RegisterScentOptions {
|
|
170
|
+
cooldownMs?: number;
|
|
171
|
+
activationPayload?: Record<string, unknown>;
|
|
172
|
+
triggerMode?: "level" | "edge_rising" | "edge_falling";
|
|
173
|
+
contextTrails?: string[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
type TriggerHandler = (payload: TriggerPayload) => void | Promise<void>;
|
|
177
|
+
|
|
178
|
+
export class SbpClient {
|
|
179
|
+
private url: string;
|
|
180
|
+
private agentId: string;
|
|
181
|
+
private timeout: number;
|
|
182
|
+
private sessionId: string | null = null;
|
|
183
|
+
private sseController: AbortController | null = null;
|
|
184
|
+
private sseHandlers = new Map<string, TriggerHandler>();
|
|
185
|
+
private requestId = 0;
|
|
186
|
+
private lastEventId: string | null = null;
|
|
187
|
+
|
|
188
|
+
constructor(options: SbpClientOptions = {}) {
|
|
189
|
+
this.url = options.url ?? "http://localhost:3000";
|
|
190
|
+
this.agentId = options.agentId ?? `agent-${Math.random().toString(36).slice(2, 10)}`;
|
|
191
|
+
this.timeout = options.timeout ?? 30000;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ==========================================================================
|
|
195
|
+
// JSON-RPC via POST
|
|
196
|
+
// ==========================================================================
|
|
197
|
+
|
|
198
|
+
private async rpc<T>(method: string, params: Record<string, unknown>): Promise<T> {
|
|
199
|
+
const headers: Record<string, string> = {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
Accept: "application/json, text/event-stream",
|
|
202
|
+
"Sbp-Protocol-Version": "0.1",
|
|
203
|
+
"Sbp-Agent-Id": this.agentId,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (this.sessionId) {
|
|
207
|
+
headers["Sbp-Session-Id"] = this.sessionId;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const response = await fetch(`${this.url}/sbp`, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers,
|
|
213
|
+
body: JSON.stringify({
|
|
214
|
+
jsonrpc: "2.0",
|
|
215
|
+
id: ++this.requestId,
|
|
216
|
+
method,
|
|
217
|
+
params,
|
|
218
|
+
}),
|
|
219
|
+
signal: AbortSignal.timeout(this.timeout),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Capture session ID
|
|
223
|
+
const newSessionId = response.headers.get("Sbp-Session-Id");
|
|
224
|
+
if (newSessionId) {
|
|
225
|
+
this.sessionId = newSessionId;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const data = await response.json() as { result?: T; error?: { code: number; message: string } };
|
|
229
|
+
|
|
230
|
+
if (data.error) {
|
|
231
|
+
throw new Error(`SBP Error ${data.error.code}: ${data.error.message}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return data.result as T;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ==========================================================================
|
|
238
|
+
// EMIT
|
|
239
|
+
// ==========================================================================
|
|
240
|
+
|
|
241
|
+
async emit(
|
|
242
|
+
trail: string,
|
|
243
|
+
type: string,
|
|
244
|
+
intensity: number,
|
|
245
|
+
options: EmitOptions = {}
|
|
246
|
+
): Promise<EmitResult> {
|
|
247
|
+
return this.rpc<EmitResult>("sbp/emit", {
|
|
248
|
+
trail,
|
|
249
|
+
type,
|
|
250
|
+
intensity,
|
|
251
|
+
decay: options.decay,
|
|
252
|
+
payload: options.payload,
|
|
253
|
+
tags: options.tags,
|
|
254
|
+
merge_strategy: options.mergeStrategy ?? "reinforce",
|
|
255
|
+
source_agent: this.agentId,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ==========================================================================
|
|
260
|
+
// SNIFF
|
|
261
|
+
// ==========================================================================
|
|
262
|
+
|
|
263
|
+
async sniff(options: SniffOptions = {}): Promise<SniffResult> {
|
|
264
|
+
return this.rpc<SniffResult>("sbp/sniff", {
|
|
265
|
+
trails: options.trails,
|
|
266
|
+
types: options.types,
|
|
267
|
+
min_intensity: options.minIntensity ?? 0,
|
|
268
|
+
limit: options.limit ?? 100,
|
|
269
|
+
include_evaporated: options.includeEvaporated ?? false,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ==========================================================================
|
|
274
|
+
// REGISTER_SCENT
|
|
275
|
+
// ==========================================================================
|
|
276
|
+
|
|
277
|
+
async registerScent(
|
|
278
|
+
scentId: string,
|
|
279
|
+
condition: ScentCondition,
|
|
280
|
+
options: RegisterScentOptions = {}
|
|
281
|
+
): Promise<RegisterScentResult> {
|
|
282
|
+
return this.rpc<RegisterScentResult>("sbp/register_scent", {
|
|
283
|
+
scent_id: scentId,
|
|
284
|
+
agent_endpoint: `sse://${this.agentId}`,
|
|
285
|
+
condition,
|
|
286
|
+
cooldown_ms: options.cooldownMs ?? 0,
|
|
287
|
+
activation_payload: options.activationPayload,
|
|
288
|
+
trigger_mode: options.triggerMode ?? "level",
|
|
289
|
+
context_trails: options.contextTrails,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ==========================================================================
|
|
294
|
+
// DEREGISTER_SCENT
|
|
295
|
+
// ==========================================================================
|
|
296
|
+
|
|
297
|
+
async deregisterScent(scentId: string): Promise<{ scent_id: string; status: string }> {
|
|
298
|
+
return this.rpc("sbp/deregister_scent", { scent_id: scentId });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ==========================================================================
|
|
302
|
+
// INSPECT
|
|
303
|
+
// ==========================================================================
|
|
304
|
+
|
|
305
|
+
async inspect(include?: string[]): Promise<Record<string, unknown>> {
|
|
306
|
+
return this.rpc("sbp/inspect", { include: include ?? ["trails", "scents", "stats"] });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ==========================================================================
|
|
310
|
+
// SSE SUBSCRIPTIONS
|
|
311
|
+
// ==========================================================================
|
|
312
|
+
|
|
313
|
+
async subscribe(scentId: string, handler: TriggerHandler): Promise<void> {
|
|
314
|
+
// Register handler
|
|
315
|
+
this.sseHandlers.set(scentId, handler);
|
|
316
|
+
|
|
317
|
+
// Tell server we want this scent's triggers
|
|
318
|
+
await this.rpc("sbp/subscribe", { scent_id: scentId });
|
|
319
|
+
|
|
320
|
+
// Start SSE listener if not already running
|
|
321
|
+
if (!this.sseController) {
|
|
322
|
+
this.startSSE();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async unsubscribe(scentId: string): Promise<void> {
|
|
327
|
+
this.sseHandlers.delete(scentId);
|
|
328
|
+
await this.rpc("sbp/unsubscribe", { scent_id: scentId });
|
|
329
|
+
|
|
330
|
+
// Stop SSE if no more handlers
|
|
331
|
+
if (this.sseHandlers.size === 0 && this.sseController) {
|
|
332
|
+
this.sseController.abort();
|
|
333
|
+
this.sseController = null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private async startSSE(): Promise<void> {
|
|
338
|
+
this.sseController = new AbortController();
|
|
339
|
+
|
|
340
|
+
const headers: Record<string, string> = {
|
|
341
|
+
Accept: "text/event-stream",
|
|
342
|
+
"Sbp-Protocol-Version": "0.1",
|
|
343
|
+
"Sbp-Agent-Id": this.agentId,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
if (this.sessionId) {
|
|
347
|
+
headers["Sbp-Session-Id"] = this.sessionId;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (this.lastEventId) {
|
|
351
|
+
headers["Last-Event-ID"] = this.lastEventId;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const response = await fetch(`${this.url}/sbp`, {
|
|
356
|
+
method: "GET",
|
|
357
|
+
headers,
|
|
358
|
+
signal: this.sseController.signal,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
if (!response.ok || !response.body) {
|
|
362
|
+
console.error("[SBP] Failed to open SSE stream");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Capture session ID
|
|
367
|
+
const newSessionId = response.headers.get("Sbp-Session-Id");
|
|
368
|
+
if (newSessionId) {
|
|
369
|
+
this.sessionId = newSessionId;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const reader = response.body.getReader();
|
|
373
|
+
const decoder = new TextDecoder();
|
|
374
|
+
let buffer = "";
|
|
375
|
+
let eventType = "";
|
|
376
|
+
let eventData = "";
|
|
377
|
+
let eventId = "";
|
|
378
|
+
|
|
379
|
+
while (true) {
|
|
380
|
+
const { done, value } = await reader.read();
|
|
381
|
+
if (done) break;
|
|
382
|
+
|
|
383
|
+
buffer += decoder.decode(value, { stream: true });
|
|
384
|
+
const lines = buffer.split("\n");
|
|
385
|
+
buffer = lines.pop() || "";
|
|
386
|
+
|
|
387
|
+
for (const line of lines) {
|
|
388
|
+
if (line.startsWith("event:")) {
|
|
389
|
+
eventType = line.slice(6).trim();
|
|
390
|
+
} else if (line.startsWith("id:")) {
|
|
391
|
+
eventId = line.slice(3).trim();
|
|
392
|
+
this.lastEventId = eventId;
|
|
393
|
+
} else if (line.startsWith("data:")) {
|
|
394
|
+
eventData = line.slice(5).trim();
|
|
395
|
+
} else if (line === "" && eventData) {
|
|
396
|
+
// End of event
|
|
397
|
+
this.handleSSEEvent(eventType, eventData);
|
|
398
|
+
eventType = "";
|
|
399
|
+
eventData = "";
|
|
400
|
+
eventId = "";
|
|
401
|
+
}
|
|
402
|
+
// Ignore comment lines starting with ":"
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
} catch (err) {
|
|
406
|
+
if ((err as Error).name !== "AbortError") {
|
|
407
|
+
console.error("[SBP] SSE error:", err);
|
|
408
|
+
// Reconnect after delay
|
|
409
|
+
setTimeout(() => {
|
|
410
|
+
if (this.sseHandlers.size > 0) {
|
|
411
|
+
this.startSSE();
|
|
412
|
+
}
|
|
413
|
+
}, 1000);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private handleSSEEvent(eventType: string, data: string): void {
|
|
419
|
+
try {
|
|
420
|
+
if (eventType === "message") {
|
|
421
|
+
const msg = JSON.parse(data);
|
|
422
|
+
if (msg.method === "sbp/trigger") {
|
|
423
|
+
const payload = msg.params as TriggerPayload;
|
|
424
|
+
const handler = this.sseHandlers.get(payload.scent_id);
|
|
425
|
+
if (handler) {
|
|
426
|
+
Promise.resolve(handler(payload)).catch((err) => {
|
|
427
|
+
console.error("[SBP] Trigger handler error:", err);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} catch (err) {
|
|
433
|
+
console.error("[SBP] SSE event parse error:", err);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async close(): Promise<void> {
|
|
438
|
+
if (this.sseController) {
|
|
439
|
+
this.sseController.abort();
|
|
440
|
+
this.sseController = null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ============================================================================
|
|
446
|
+
// AGENT HELPER
|
|
447
|
+
// ============================================================================
|
|
448
|
+
|
|
449
|
+
export interface AgentOptions extends SbpClientOptions {
|
|
450
|
+
onError?: (error: Error) => void;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export class SbpAgent {
|
|
454
|
+
private client: SbpClient;
|
|
455
|
+
private scents: Array<{
|
|
456
|
+
scentId: string;
|
|
457
|
+
condition: ScentCondition;
|
|
458
|
+
handler: TriggerHandler;
|
|
459
|
+
options: RegisterScentOptions;
|
|
460
|
+
}> = [];
|
|
461
|
+
|
|
462
|
+
constructor(agentId: string, options: AgentOptions = {}) {
|
|
463
|
+
this.client = new SbpClient({ ...options, agentId });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
when(
|
|
467
|
+
trail: string,
|
|
468
|
+
signalType: string,
|
|
469
|
+
operator: ThresholdCondition["operator"],
|
|
470
|
+
value: number,
|
|
471
|
+
handler: TriggerHandler,
|
|
472
|
+
options: RegisterScentOptions = {}
|
|
473
|
+
): this {
|
|
474
|
+
const scentId = `${trail}/${signalType}`;
|
|
475
|
+
this.scents.push({
|
|
476
|
+
scentId,
|
|
477
|
+
condition: threshold(trail, signalType, operator, value),
|
|
478
|
+
handler,
|
|
479
|
+
options,
|
|
480
|
+
});
|
|
481
|
+
return this;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
onScent(
|
|
485
|
+
scentId: string,
|
|
486
|
+
condition: ScentCondition,
|
|
487
|
+
handler: TriggerHandler,
|
|
488
|
+
options: RegisterScentOptions = {}
|
|
489
|
+
): this {
|
|
490
|
+
this.scents.push({ scentId, condition, handler, options });
|
|
491
|
+
return this;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async emit(
|
|
495
|
+
trail: string,
|
|
496
|
+
type: string,
|
|
497
|
+
intensity: number,
|
|
498
|
+
options?: EmitOptions
|
|
499
|
+
): Promise<EmitResult> {
|
|
500
|
+
return this.client.emit(trail, type, intensity, options);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async sniff(options?: SniffOptions): Promise<SniffResult> {
|
|
504
|
+
return this.client.sniff(options);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async run(): Promise<void> {
|
|
508
|
+
for (const { scentId, condition, handler, options } of this.scents) {
|
|
509
|
+
await this.client.registerScent(scentId, condition, options);
|
|
510
|
+
await this.client.subscribe(scentId, handler);
|
|
511
|
+
console.log(`[SBP Agent] Registered: ${scentId}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
console.log(`[SBP Agent] Running with ${this.scents.length} scents`);
|
|
515
|
+
|
|
516
|
+
// Keep alive
|
|
517
|
+
await new Promise(() => {});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async stop(): Promise<void> {
|
|
521
|
+
for (const { scentId } of this.scents) {
|
|
522
|
+
await this.client.deregisterScent(scentId);
|
|
523
|
+
}
|
|
524
|
+
await this.client.close();
|
|
525
|
+
}
|
|
526
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|