@advicenxt/sbp-server 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.
Files changed (63) hide show
  1. package/benchmarks/bench.ts +272 -0
  2. package/dist/auth.d.ts +20 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +69 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/blackboard.d.ts +84 -0
  7. package/dist/blackboard.d.ts.map +1 -0
  8. package/dist/blackboard.js +502 -0
  9. package/dist/blackboard.js.map +1 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +102 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/conditions.d.ts +27 -0
  15. package/dist/conditions.d.ts.map +1 -0
  16. package/dist/conditions.js +240 -0
  17. package/dist/conditions.js.map +1 -0
  18. package/dist/decay.d.ts +21 -0
  19. package/dist/decay.d.ts.map +1 -0
  20. package/dist/decay.js +88 -0
  21. package/dist/decay.js.map +1 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +13 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/rate-limiter.d.ts +21 -0
  27. package/dist/rate-limiter.d.ts.map +1 -0
  28. package/dist/rate-limiter.js +75 -0
  29. package/dist/rate-limiter.js.map +1 -0
  30. package/dist/server.d.ts +63 -0
  31. package/dist/server.d.ts.map +1 -0
  32. package/dist/server.js +401 -0
  33. package/dist/server.js.map +1 -0
  34. package/dist/store.d.ts +54 -0
  35. package/dist/store.d.ts.map +1 -0
  36. package/dist/store.js +55 -0
  37. package/dist/store.js.map +1 -0
  38. package/dist/types.d.ts +247 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +26 -0
  41. package/dist/types.js.map +1 -0
  42. package/dist/validation.d.ts +296 -0
  43. package/dist/validation.d.ts.map +1 -0
  44. package/dist/validation.js +205 -0
  45. package/dist/validation.js.map +1 -0
  46. package/eslint.config.js +26 -0
  47. package/package.json +66 -0
  48. package/src/auth.ts +89 -0
  49. package/src/blackboard.test.ts +287 -0
  50. package/src/blackboard.ts +651 -0
  51. package/src/cli.ts +116 -0
  52. package/src/conditions.ts +305 -0
  53. package/src/conformance.test.ts +686 -0
  54. package/src/decay.ts +103 -0
  55. package/src/index.ts +24 -0
  56. package/src/rate-limiter.ts +104 -0
  57. package/src/server.integration.test.ts +436 -0
  58. package/src/server.ts +500 -0
  59. package/src/store.ts +108 -0
  60. package/src/types.ts +314 -0
  61. package/src/validation.ts +251 -0
  62. package/tsconfig.eslint.json +5 -0
  63. package/tsconfig.json +20 -0
package/src/cli.ts ADDED
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SBP Server CLI
4
+ * Streamable HTTP with SSE transport
5
+ */
6
+
7
+ import { SbpServer } from "./server.js";
8
+
9
+ const args = process.argv.slice(2);
10
+
11
+ function parseArgs(args: string[]): Record<string, string | boolean> {
12
+ const result: Record<string, string | boolean> = {};
13
+
14
+ for (let i = 0; i < args.length; i++) {
15
+ const arg = args[i];
16
+
17
+ if (arg.startsWith("--")) {
18
+ const key = arg.slice(2);
19
+ const next = args[i + 1];
20
+
21
+ if (next && !next.startsWith("--")) {
22
+ result[key] = next;
23
+ i++;
24
+ } else {
25
+ result[key] = true;
26
+ }
27
+ }
28
+ }
29
+
30
+ return result;
31
+ }
32
+
33
+ function printHelp(): void {
34
+ console.log(`
35
+ SBP Server - Stigmergic Blackboard Protocol
36
+
37
+ Usage:
38
+ sbp-server [options]
39
+
40
+ Options:
41
+ --port <number> Port to listen on (default: 3000)
42
+ --host <string> Host to bind to (default: localhost)
43
+ --log Enable request logging
44
+ --api-key <keys> Comma-separated API keys for authentication
45
+ --rate-limit <n> Max requests per minute per agent (default: off)
46
+ --help Show this help message
47
+
48
+ Transport:
49
+ Streamable HTTP with SSE (Server-Sent Events)
50
+ - POST /sbp Client -> Server messages
51
+ - GET /sbp Server -> Client triggers (SSE stream)
52
+
53
+ Authentication:
54
+ When --api-key is set, all requests must include:
55
+ Authorization: Bearer <api-key>
56
+ API keys can also be set via SBP_API_KEYS env variable.
57
+
58
+ Examples:
59
+ sbp-server
60
+ sbp-server --port 8080
61
+ sbp-server --host 0.0.0.0 --port 3000 --log
62
+ sbp-server --api-key key1,key2 --rate-limit 500
63
+ `);
64
+ }
65
+
66
+ async function main(): Promise<void> {
67
+ const options = parseArgs(args);
68
+
69
+ if (options.help) {
70
+ printHelp();
71
+ process.exit(0);
72
+ }
73
+
74
+ // Parse API keys from CLI arg or env variable
75
+ const apiKeyArg = (options["api-key"] as string) || process.env.SBP_API_KEYS || "";
76
+ const apiKeys = apiKeyArg
77
+ .split(",")
78
+ .map((k) => k.trim())
79
+ .filter(Boolean);
80
+
81
+ // Parse rate limit
82
+ const rateLimitMax = options["rate-limit"]
83
+ ? parseInt(options["rate-limit"] as string, 10)
84
+ : undefined;
85
+
86
+ const server = new SbpServer({
87
+ port: options.port ? parseInt(options.port as string, 10) : 3000,
88
+ host: (options.host as string) || "localhost",
89
+ logging: !!options.log,
90
+ auth: apiKeys.length > 0
91
+ ? { apiKeys, requireAuth: true }
92
+ : undefined,
93
+ rateLimit: rateLimitMax
94
+ ? { maxRequests: rateLimitMax, windowMs: 60000 }
95
+ : undefined,
96
+ });
97
+
98
+ // Graceful shutdown
99
+ const shutdown = async () => {
100
+ console.log("\n[SBP] Shutting down...");
101
+ await server.stop();
102
+ process.exit(0);
103
+ };
104
+
105
+ process.on("SIGINT", shutdown);
106
+ process.on("SIGTERM", shutdown);
107
+
108
+ try {
109
+ await server.start();
110
+ } catch (err) {
111
+ console.error("[SBP] Failed to start server:", err);
112
+ process.exit(1);
113
+ }
114
+ }
115
+
116
+ main();
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Scent condition evaluation
3
+ */
4
+
5
+ import type {
6
+ Pheromone,
7
+ PheromoneSnapshot,
8
+ ScentCondition,
9
+ ThresholdCondition,
10
+ CompositeCondition,
11
+ RateCondition,
12
+ PatternCondition,
13
+ TagFilter,
14
+ } from "./types.js";
15
+ import { computeIntensity, isEvaporated } from "./decay.js";
16
+
17
+ export interface EvaluationContext {
18
+ pheromones: Pheromone[];
19
+ now: number;
20
+ emissionHistory?: Array<{ trail: string; type: string; timestamp: number }>;
21
+ }
22
+
23
+ export interface EvaluationResult {
24
+ met: boolean;
25
+ value: number;
26
+ matchingPheromoneIds: string[];
27
+ }
28
+
29
+ /**
30
+ * Evaluate a scent condition against the current environment
31
+ */
32
+ export function evaluateCondition(
33
+ condition: ScentCondition,
34
+ ctx: EvaluationContext
35
+ ): EvaluationResult {
36
+ switch (condition.type) {
37
+ case "threshold":
38
+ return evaluateThreshold(condition, ctx);
39
+ case "composite":
40
+ return evaluateComposite(condition, ctx);
41
+ case "rate":
42
+ return evaluateRate(condition, ctx);
43
+ case "pattern":
44
+ return evaluatePattern(condition, ctx);
45
+ default:
46
+ return { met: false, value: 0, matchingPheromoneIds: [] };
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Evaluate a threshold condition
52
+ */
53
+ function evaluateThreshold(
54
+ condition: ThresholdCondition,
55
+ ctx: EvaluationContext
56
+ ): EvaluationResult {
57
+ const { pheromones, now } = ctx;
58
+
59
+ // Filter matching pheromones
60
+ const matching = pheromones.filter((p) => {
61
+ // Trail must match
62
+ if (p.trail !== condition.trail) return false;
63
+
64
+ // Type must match (or wildcard)
65
+ if (condition.signal_type !== "*" && p.type !== condition.signal_type) return false;
66
+
67
+ // Must not be evaporated
68
+ if (isEvaporated(p, now)) return false;
69
+
70
+ // Tag filter
71
+ if (condition.tags && !matchTags(p.tags, condition.tags)) return false;
72
+
73
+ return true;
74
+ });
75
+
76
+ // Compute aggregate value
77
+ let aggValue: number;
78
+ const intensities = matching.map((p) => computeIntensity(p, now));
79
+
80
+ switch (condition.aggregation) {
81
+ case "sum":
82
+ aggValue = intensities.reduce((a, b) => a + b, 0);
83
+ break;
84
+ case "max":
85
+ aggValue = intensities.length > 0 ? Math.max(...intensities) : 0;
86
+ break;
87
+ case "avg":
88
+ aggValue = intensities.length > 0
89
+ ? intensities.reduce((a, b) => a + b, 0) / intensities.length
90
+ : 0;
91
+ break;
92
+ case "count":
93
+ aggValue = matching.length;
94
+ break;
95
+ case "any":
96
+ aggValue = matching.length > 0 ? 1 : 0;
97
+ break;
98
+ default:
99
+ aggValue = 0;
100
+ }
101
+
102
+ // Compare
103
+ const met = compare(aggValue, condition.operator, condition.value);
104
+
105
+ return {
106
+ met,
107
+ value: aggValue,
108
+ matchingPheromoneIds: matching.map((p) => p.id),
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Evaluate a composite condition (AND, OR, NOT)
114
+ */
115
+ function evaluateComposite(
116
+ condition: CompositeCondition,
117
+ ctx: EvaluationContext
118
+ ): EvaluationResult {
119
+ if (condition.conditions.length === 0) {
120
+ return { met: false, value: 0, matchingPheromoneIds: [] };
121
+ }
122
+
123
+ const results = condition.conditions.map((c) => evaluateCondition(c, ctx));
124
+ const allPheromoneIds = [...new Set(results.flatMap((r) => r.matchingPheromoneIds))];
125
+
126
+ let met: boolean;
127
+ switch (condition.operator) {
128
+ case "and":
129
+ met = results.every((r) => r.met);
130
+ break;
131
+ case "or":
132
+ met = results.some((r) => r.met);
133
+ break;
134
+ case "not":
135
+ met = !results[0].met;
136
+ break;
137
+ default:
138
+ met = false;
139
+ }
140
+
141
+ return {
142
+ met,
143
+ value: results.filter((r) => r.met).length,
144
+ matchingPheromoneIds: allPheromoneIds,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Evaluate a rate condition
150
+ */
151
+ function evaluateRate(
152
+ condition: RateCondition,
153
+ ctx: EvaluationContext
154
+ ): EvaluationResult {
155
+ const { emissionHistory = [], now } = ctx;
156
+
157
+ // Filter emissions in the window
158
+ const windowStart = now - condition.window_ms;
159
+ const relevantEmissions = emissionHistory.filter(
160
+ (e) =>
161
+ e.trail === condition.trail &&
162
+ (condition.signal_type === "*" || e.type === condition.signal_type) &&
163
+ e.timestamp >= windowStart
164
+ );
165
+
166
+ let value: number;
167
+ if (condition.metric === "emissions_per_second") {
168
+ const windowSeconds = condition.window_ms / 1000;
169
+ value = relevantEmissions.length / windowSeconds;
170
+ } else {
171
+ // intensity_delta would require tracking intensity over time
172
+ // For now, approximate with emission count
173
+ value = relevantEmissions.length;
174
+ }
175
+
176
+ const met = compare(value, condition.operator, condition.value);
177
+
178
+ return {
179
+ met,
180
+ value,
181
+ matchingPheromoneIds: [],
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Evaluate a pattern condition
187
+ * Checks if a sequence of pheromone emissions occurred within a time window
188
+ */
189
+ function evaluatePattern(
190
+ condition: PatternCondition,
191
+ ctx: EvaluationContext
192
+ ): EvaluationResult {
193
+ const { emissionHistory = [], now } = ctx;
194
+ const { sequence, window_ms, ordered = true } = condition;
195
+
196
+ // Filter emissions within the window
197
+ const windowStart = now - window_ms;
198
+ const relevant = emissionHistory.filter((e) => e.timestamp >= windowStart);
199
+
200
+ if (relevant.length === 0 || sequence.length === 0) {
201
+ return { met: false, value: 0, matchingPheromoneIds: [] };
202
+ }
203
+
204
+ if (ordered) {
205
+ // Ordered: each step must appear after the previous one
206
+ let searchFrom = 0;
207
+ let matchCount = 0;
208
+
209
+ for (const step of sequence) {
210
+ let found = false;
211
+ for (let i = searchFrom; i < relevant.length; i++) {
212
+ const emission = relevant[i];
213
+ if (
214
+ emission.trail === step.trail &&
215
+ emission.type === step.signal_type
216
+ ) {
217
+ found = true;
218
+ searchFrom = i + 1;
219
+ matchCount++;
220
+ break;
221
+ }
222
+ }
223
+ if (!found) break;
224
+ }
225
+
226
+ return {
227
+ met: matchCount === sequence.length,
228
+ value: matchCount / sequence.length,
229
+ matchingPheromoneIds: [],
230
+ };
231
+ } else {
232
+ // Unordered: all steps must appear in any order
233
+ const remaining = [...relevant];
234
+ let matchCount = 0;
235
+
236
+ for (const step of sequence) {
237
+ const idx = remaining.findIndex(
238
+ (e) => e.trail === step.trail && e.type === step.signal_type
239
+ );
240
+ if (idx >= 0) {
241
+ remaining.splice(idx, 1);
242
+ matchCount++;
243
+ }
244
+ }
245
+
246
+ return {
247
+ met: matchCount === sequence.length,
248
+ value: matchCount / sequence.length,
249
+ matchingPheromoneIds: [],
250
+ };
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Match tags against a filter
256
+ */
257
+ function matchTags(tags: string[], filter: TagFilter): boolean {
258
+ if (filter.any && filter.any.length > 0) {
259
+ if (!filter.any.some((t) => tags.includes(t))) return false;
260
+ }
261
+ if (filter.all && filter.all.length > 0) {
262
+ if (!filter.all.every((t) => tags.includes(t))) return false;
263
+ }
264
+ if (filter.none && filter.none.length > 0) {
265
+ if (filter.none.some((t) => tags.includes(t))) return false;
266
+ }
267
+ return true;
268
+ }
269
+
270
+ /**
271
+ * Compare two values with an operator
272
+ */
273
+ function compare(a: number, op: string, b: number): boolean {
274
+ switch (op) {
275
+ case ">=":
276
+ return a >= b;
277
+ case ">":
278
+ return a > b;
279
+ case "<=":
280
+ return a <= b;
281
+ case "<":
282
+ return a < b;
283
+ case "==":
284
+ return a === b;
285
+ case "!=":
286
+ return a !== b;
287
+ default:
288
+ return false;
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Create a snapshot of a pheromone for trigger payloads
294
+ */
295
+ export function createSnapshot(pheromone: Pheromone, now: number): PheromoneSnapshot {
296
+ return {
297
+ id: pheromone.id,
298
+ trail: pheromone.trail,
299
+ type: pheromone.type,
300
+ current_intensity: computeIntensity(pheromone, now),
301
+ payload: pheromone.payload,
302
+ age_ms: now - pheromone.emitted_at,
303
+ tags: pheromone.tags,
304
+ };
305
+ }