clinch-core 0.4.0 → 0.6.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/README.md CHANGED
@@ -40,7 +40,7 @@ npm install node-llama-cpp
40
40
 
41
41
  ### Core Statuses (`CoreStatus`)
42
42
  * `OFFLINE`: Client is disconnected.
43
- * `CONNECTING`: Resolving registry DNS and performing Proof-of-Work auth.
43
+ * `CONNECTING`: Resolving registry configuration and performing Proof-of-Work auth.
44
44
  * `IDLE`: Connected and authenticated. Waiting for handshakes or callbacks.
45
45
  * `RECONNECTING`: Socket connection lost; executing exponential backoff.
46
46
  * `NEGOTIATING`: Active turn-based negotiation sequence in progress.
@@ -86,6 +86,7 @@ async function startLocalAgent() {
86
86
  await core.sandbox({ downloadLLM: true });
87
87
 
88
88
  // 2. Initiate Negotiation: Session automatically transitions to 'NEGOTIATING'
89
+ // Format: PROTOCOL_MODE.domain.anp (e.g., ANP/C.amazon.anp)
89
90
  const sessionId = await core.negotiate('ANP/C.amazon.anp', {
90
91
  intent: 'purchase',
91
92
  category: 'electronics',
@@ -147,6 +148,7 @@ webhookCore.on('callback_received', async (event) => {
147
148
  console.log("Deal reached!");
148
149
  } else {
149
150
  await webhookCore.sendCounter(event.sessionId, aiDecision.price, aiDecision.message);
151
+ // Reserialize and update DB after turn to maintain state sync
150
152
  await db.update(event.sessionId, webhookCore.exportSessionState(event.sessionId));
151
153
  }
152
154
  });
@@ -168,7 +170,10 @@ const app = express();
168
170
  app.use(express.json());
169
171
 
170
172
  // Initialize with your permanent Dashboard-generated Private Key
171
- const seller = new ClinchSeller({ privateKeyHex: process.env.SELLER_PRIVATE_KEY });
173
+ const seller = new ClinchSeller({
174
+ privateKeyHex: process.env.SELLER_PRIVATE_KEY
175
+ // registryUrl: 'http://localhost:7860' // Optional: Override for local dev
176
+ });
172
177
 
173
178
  // Publish your routing endpoint to the network on boot
174
179
  await seller.registerEndpoint({
@@ -206,19 +211,20 @@ app.listen(8080);
206
211
  ### Buyer Client (`ClinchCore`)
207
212
 
208
213
  #### `new ClinchCore(config)`
209
- * `config.registryUrl` *(string)*: Override Firebase dynamic routing.
214
+ * `config.registryUrl` *(string)*: Override default dynamic configuration resolution for local testing.
210
215
  * `config.timeoutMs` *(number)*: Connection timeout limit (Default: `5000`ms).
211
216
 
212
217
  #### `async initialize(cachedToken?)`
213
- Authenticates the node, completes Identity-Bound PoW, and connects the WebSocket.
218
+ Authenticates the node on the network, completes Identity-Bound PoW, and connects the WebSocket.
219
+ * `cachedToken` *(string)*: Optional. Re-use an existing network token to bypass PoW calculations on restart.
214
220
 
215
221
  #### `async negotiate(address, constraints)`
216
222
  Launches a cryptographic session handshake. Returns `sessionId`.
217
- * `address` *(string)*: e.g. `ANP/C.cloudflare.anp`. (Must include mode prefix).
218
- * `constraints` *(ConstraintVector)*: Must include `max_budget` (number).
223
+ * `address` *(string)*: Target seller address. Must include the protocol mode prefix (e.g., `ANP/C.amazon.anp`).
224
+ * `constraints` *(ConstraintVector)*: Must include `max_budget` (number) and `item` (string).
219
225
 
220
226
  #### `exportSessionState(sessionId)` / `importSessionState(serializedData)`
221
- Serializes the active session—including the ephemeral cryptographic keys, current turn, and LLM context parameters—to a JSON string. Used to scale instances horizontally or survive pod restarts.
227
+ Serializes the active session—including the ephemeral cryptographic keys, current turn, and LLM context parameters—to a JSON string. Used to scale instances horizontally, survive pod restarts, or pick up negotiations across async callback windows.
222
228
 
223
229
  #### `buildAgentPrompt(sessionId, incomingMessage)`
224
230
  Returns a highly-optimized, state-aware string to pass to an external LLM as a System Prompt. Ensures the LLM outputs strict JSON matching the protocol rules.
@@ -232,10 +238,13 @@ Closes the active connection and generates a single-use re-engagement Callback t
232
238
  #### `async sandbox(config)`
233
239
  Initializes the edge-AI execution context, downloads the GGUF, and auto-listens.
234
240
 
241
+ ---
242
+
235
243
  ### Seller Client (`ClinchSeller`)
236
244
 
237
245
  #### `new ClinchSeller(config)`
238
246
  * `config.privateKeyHex` *(string)*: The Ed25519 private key generated from the Clinch Dashboard. Used to cryptographically authenticate endpoint updates.
247
+ * `config.registryUrl` *(string)*: Optional. Overrides Registry configuration resolution for local testing.
239
248
 
240
249
  #### `async registerEndpoint(record)`
241
250
  Publishes the seller's DNS-style record to the Registry so buyers can discover and route to it. Signed locally via the Ed25519 identity key.
package/dist/index.js CHANGED
@@ -50,7 +50,12 @@ function toHex(arr) {
50
50
  return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
51
51
  }
52
52
  function fromHex(hex) {
53
- return new Uint8Array(hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
53
+ // Strips all spaces, newlines, and rogue quotes from .env strings
54
+ const clean = hex.replace(/[^0-9a-fA-F]/g, '');
55
+ const match = clean.match(/.{1,2}/g);
56
+ if (!match)
57
+ return new Uint8Array(0);
58
+ return new Uint8Array(match.map(byte => parseInt(byte, 16)));
54
59
  }
55
60
  // ============================================================================
56
61
  // THE CLINCH CORE LIBRARY (Buyer)
@@ -66,11 +71,9 @@ class ClinchCore extends events_1.EventEmitter {
66
71
  identityPubKey;
67
72
  activeSessions = new Map();
68
73
  ws = null;
69
- // Sandbox Engine Base (Model/Context are global, Sequences are per-session)
70
74
  isSandboxMode = false;
71
75
  sandboxModelContext = null;
72
76
  sandboxMaxTurns = 6;
73
- // Legacy support for single-tenant applications checking the last ID
74
77
  get activeNegotiationId() {
75
78
  return Array.from(this.activeSessions.keys()).pop() || null;
76
79
  }
@@ -228,9 +231,6 @@ class ClinchCore extends events_1.EventEmitter {
228
231
  this.ws = null;
229
232
  }
230
233
  }
231
- // --------------------------------------------------------------------------
232
- // SESSION STATE MANAGEMENT (For Enterprise Horizontal Scaling & Reconnects)
233
- // --------------------------------------------------------------------------
234
234
  exportSessionState(sessionId) {
235
235
  const session = this.activeSessions.get(sessionId);
236
236
  if (!session)
@@ -266,9 +266,6 @@ class ClinchCore extends events_1.EventEmitter {
266
266
  getSession(sessionId) {
267
267
  return this.activeSessions.get(sessionId);
268
268
  }
269
- // --------------------------------------------------------------------------
270
- // UNIVERSAL PROMPT BUILDER
271
- // --------------------------------------------------------------------------
272
269
  buildAgentPrompt(sessionId, incomingMessage) {
273
270
  const session = this.activeSessions.get(sessionId);
274
271
  if (!session)
@@ -303,9 +300,6 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
303
300
  "message": "<One concise sentence of negotiation dialogue>"
304
301
  }`;
305
302
  }
306
- // --------------------------------------------------------------------------
307
- // PROTOCOL OPERATIONS
308
- // --------------------------------------------------------------------------
309
303
  async search(query, mode) {
310
304
  let url = `/api/discover?category=${encodeURIComponent(query)}`;
311
305
  if (mode)
@@ -355,7 +349,6 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
355
349
  headers: { 'Content-Type': 'application/json' },
356
350
  body: JSON.stringify({ ...payload, buyer_sig })
357
351
  });
358
- // Sync state if deal reached
359
352
  if (response.msg_type === 'accept' || response.status === 'COMMITTED') {
360
353
  session.status = 'CLOSED';
361
354
  session.lastKnownPrice = response.price || price;
@@ -375,9 +368,6 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
375
368
  session.exitTokenHash = res.token_hash;
376
369
  return res.token_hash;
377
370
  }
378
- // --------------------------------------------------------------------------
379
- // OUT-OF-THE-BOX SANDBOX (AUTO-TURN ENGINE)
380
- // --------------------------------------------------------------------------
381
371
  async sandbox(config = {}) {
382
372
  this.isSandboxMode = true;
383
373
  this.sandboxMaxTurns = config.maxTurns || 6;
@@ -392,7 +382,7 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
392
382
  }
393
383
  async setupSandbox(config = {}) {
394
384
  if (this.sandboxModelContext)
395
- return; // Already initialized
385
+ return;
396
386
  const settings = {
397
387
  downloadLLM: true,
398
388
  modelUrl: "https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf",
@@ -475,7 +465,6 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
475
465
  async sandboxEvaluate(session, incomingOffer) {
476
466
  const { LlamaChatSession, ChatMLChatWrapper } = await Promise.resolve().then(() => __importStar(require('node-llama-cpp')));
477
467
  const systemPrompt = this.buildAgentPrompt(session.sessionId, incomingOffer);
478
- // State Isolation: Bind sequence to the session, not the global class!
479
468
  if (!session.sandboxSequence) {
480
469
  session.sandboxSequence = this.sandboxModelContext.getSequence();
481
470
  session.sandboxSession = new LlamaChatSession({
@@ -491,9 +480,6 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
491
480
  });
492
481
  return responseText;
493
482
  }
494
- // --------------------------------------------------------------------------
495
- // UTILITIES
496
- // --------------------------------------------------------------------------
497
483
  extractPrice(text, prefix) {
498
484
  const regex = new RegExp(`${prefix}\\s*\\:?\\s*\\$?(\\d+(?:\\.\\d{2})?)`, 'i');
499
485
  const match = text.match(regex);
@@ -550,10 +536,19 @@ class ClinchSeller extends events_1.EventEmitter {
550
536
  super();
551
537
  this.config = { timeoutMs: 8000, ...config };
552
538
  if (config.privateKeyHex) {
553
- this.identityPrivKey = fromHex(config.privateKeyHex);
554
- const kp = tweetnacl_1.default.sign.keyPair.fromSecretKey(this.identityPrivKey);
555
- this.identityPubKey = toHex(kp.publicKey);
556
- this.emit('log', `[Seller] Loaded permanent identity. PubKey: ${this.identityPubKey.substring(0, 12)}...`);
539
+ try {
540
+ const cleanHex = config.privateKeyHex.replace(/[^0-9a-fA-F]/g, '');
541
+ if (cleanHex.length !== 128) {
542
+ throw new Error(`Expected 128 hex chars (64 bytes), got ${cleanHex.length}`);
543
+ }
544
+ this.identityPrivKey = fromHex(cleanHex);
545
+ const kp = tweetnacl_1.default.sign.keyPair.fromSecretKey(this.identityPrivKey);
546
+ this.identityPubKey = toHex(kp.publicKey);
547
+ this.emit('log', `[Seller] Loaded permanent identity. PubKey: ${this.identityPubKey.substring(0, 12)}...`);
548
+ }
549
+ catch (e) {
550
+ throw new Error(`[Seller] Invalid privateKeyHex in constructor: ${e.message}`);
551
+ }
557
552
  }
558
553
  else {
559
554
  const kp = tweetnacl_1.default.sign.keyPair();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clinch-core",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
 
5
5
  "description": "Clinch Protocol Edge Client",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -14,7 +14,11 @@ function toHex(arr: Uint8Array | number[]): string {
14
14
  }
15
15
 
16
16
  function fromHex(hex: string): Uint8Array {
17
- return new Uint8Array(hex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
17
+ // Strips all spaces, newlines, and rogue quotes from .env strings
18
+ const clean = hex.replace(/[^0-9a-fA-F]/g, '');
19
+ const match = clean.match(/.{1,2}/g);
20
+ if (!match) return new Uint8Array(0);
21
+ return new Uint8Array(match.map(byte => parseInt(byte, 16)));
18
22
  }
19
23
 
20
24
  // ============================================================================
@@ -56,11 +60,9 @@ export interface SessionState {
56
60
  exitTokenHash?: string;
57
61
  constraints: ConstraintVector;
58
62
 
59
- // Isolated State Tracking (Crucial for concurrency)
60
63
  currentTurn: number;
61
64
  lastKnownPrice: number;
62
65
 
63
- // Local LLM Context Tracking
64
66
  sandboxSequence?: any;
65
67
  sandboxSession?: any;
66
68
  }
@@ -95,12 +97,10 @@ export class ClinchCore extends EventEmitter {
95
97
  private activeSessions = new Map<string, SessionState>();
96
98
  private ws: WebSocket | null = null;
97
99
 
98
- // Sandbox Engine Base (Model/Context are global, Sequences are per-session)
99
100
  private isSandboxMode = false;
100
101
  private sandboxModelContext: any = null;
101
102
  private sandboxMaxTurns = 6;
102
103
 
103
- // Legacy support for single-tenant applications checking the last ID
104
104
  public get activeNegotiationId(): string | null {
105
105
  return Array.from(this.activeSessions.keys()).pop() || null;
106
106
  }
@@ -266,9 +266,6 @@ export class ClinchCore extends EventEmitter {
266
266
  if (this.ws) { this.ws.close(); this.ws = null; }
267
267
  }
268
268
 
269
- // --------------------------------------------------------------------------
270
- // SESSION STATE MANAGEMENT (For Enterprise Horizontal Scaling & Reconnects)
271
- // --------------------------------------------------------------------------
272
269
  public exportSessionState(sessionId: string): string {
273
270
  const session = this.activeSessions.get(sessionId);
274
271
  if (!session) throw new Error("Session not found");
@@ -289,7 +286,6 @@ export class ClinchCore extends EventEmitter {
289
286
 
290
287
  public importSessionState(serializedData: string): void {
291
288
  const data = JSON.parse(serializedData);
292
-
293
289
  const secretKey = fromHex(data.ephemeralSecretKeyHex);
294
290
  const keyPair = nacl.sign.keyPair.fromSecretKey(secretKey);
295
291
 
@@ -311,9 +307,6 @@ export class ClinchCore extends EventEmitter {
311
307
  return this.activeSessions.get(sessionId);
312
308
  }
313
309
 
314
- // --------------------------------------------------------------------------
315
- // UNIVERSAL PROMPT BUILDER
316
- // --------------------------------------------------------------------------
317
310
  public buildAgentPrompt(sessionId: string, incomingMessage: string): string {
318
311
  const session = this.activeSessions.get(sessionId);
319
312
  if (!session) throw new Error("Cannot build prompt: Session not found.");
@@ -350,9 +343,6 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
350
343
  }`;
351
344
  }
352
345
 
353
- // --------------------------------------------------------------------------
354
- // PROTOCOL OPERATIONS
355
- // --------------------------------------------------------------------------
356
346
  async search(query: string, mode?: string): Promise<any> {
357
347
  let url = `/api/discover?category=${encodeURIComponent(query)}`;
358
348
  if (mode) url += `&mode=${encodeURIComponent(mode)}`;
@@ -414,7 +404,6 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
414
404
  body: JSON.stringify({ ...payload, buyer_sig })
415
405
  });
416
406
 
417
- // Sync state if deal reached
418
407
  if (response.msg_type === 'accept' || response.status === 'COMMITTED') {
419
408
  session.status = 'CLOSED';
420
409
  session.lastKnownPrice = response.price || price;
@@ -437,9 +426,6 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
437
426
  return res.token_hash;
438
427
  }
439
428
 
440
- // --------------------------------------------------------------------------
441
- // OUT-OF-THE-BOX SANDBOX (AUTO-TURN ENGINE)
442
- // --------------------------------------------------------------------------
443
429
  async sandbox(config: SandboxConfig = {}): Promise<void> {
444
430
  this.isSandboxMode = true;
445
431
  this.sandboxMaxTurns = config.maxTurns || 6;
@@ -455,7 +441,7 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
455
441
  }
456
442
 
457
443
  private async setupSandbox(config: SandboxConfig = {}): Promise<void> {
458
- if (this.sandboxModelContext) return; // Already initialized
444
+ if (this.sandboxModelContext) return;
459
445
 
460
446
  const settings = {
461
447
  downloadLLM: true,
@@ -546,7 +532,6 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
546
532
 
547
533
  const systemPrompt = this.buildAgentPrompt(session.sessionId, incomingOffer);
548
534
 
549
- // State Isolation: Bind sequence to the session, not the global class!
550
535
  if (!session.sandboxSequence) {
551
536
  session.sandboxSequence = this.sandboxModelContext.getSequence();
552
537
  session.sandboxSession = new LlamaChatSession({
@@ -565,9 +550,6 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
565
550
  return responseText;
566
551
  }
567
552
 
568
- // --------------------------------------------------------------------------
569
- // UTILITIES
570
- // --------------------------------------------------------------------------
571
553
  private extractPrice(text: string, prefix: string): number | null {
572
554
  const regex = new RegExp(`${prefix}\\s*\\:?\\s*\\$?(\\d+(?:\\.\\d{2})?)`, 'i');
573
555
  const match = text.match(regex);
@@ -623,7 +605,7 @@ export interface SellerRecord {
623
605
  supported_modes: string[];
624
606
  categories: string[];
625
607
  capabilities: string[];
626
- display_name?: string; // Optional legacy support
608
+ display_name?: string;
627
609
  }
628
610
 
629
611
  export class ClinchSeller extends EventEmitter {
@@ -637,10 +619,18 @@ export class ClinchSeller extends EventEmitter {
637
619
  this.config = { timeoutMs: 8000, ...config };
638
620
 
639
621
  if (config.privateKeyHex) {
640
- this.identityPrivKey = fromHex(config.privateKeyHex);
641
- const kp = nacl.sign.keyPair.fromSecretKey(this.identityPrivKey);
642
- this.identityPubKey = toHex(kp.publicKey);
643
- this.emit('log', `[Seller] Loaded permanent identity. PubKey: ${this.identityPubKey.substring(0, 12)}...`);
622
+ try {
623
+ const cleanHex = config.privateKeyHex.replace(/[^0-9a-fA-F]/g, '');
624
+ if (cleanHex.length !== 128) {
625
+ throw new Error(`Expected 128 hex chars (64 bytes), got ${cleanHex.length}`);
626
+ }
627
+ this.identityPrivKey = fromHex(cleanHex);
628
+ const kp = nacl.sign.keyPair.fromSecretKey(this.identityPrivKey);
629
+ this.identityPubKey = toHex(kp.publicKey);
630
+ this.emit('log', `[Seller] Loaded permanent identity. PubKey: ${this.identityPubKey.substring(0, 12)}...`);
631
+ } catch (e: any) {
632
+ throw new Error(`[Seller] Invalid privateKeyHex in constructor: ${e.message}`);
633
+ }
644
634
  } else {
645
635
  const kp = nacl.sign.keyPair();
646
636
  this.identityPrivKey = kp.secretKey;