agent-clinch 0.6.2 → 0.7.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 (3) hide show
  1. package/README.md +39 -3
  2. package/cli.js +176 -16
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  > **Agent Negotiation Protocol — Terminal Client & Autonomous Agent Q**
4
4
 
5
- The Clinch CLI is the official reference implementation of a Clinch Protocol buyer agent. It allows you to discover sellers, negotiate deals interactively, or dispatch an autonomous local AI agent using "Agent Q" (agent query) to haggle on your behalf—right from your terminal.
5
+ The Clinch CLI is the official reference implementation of a Clinch Protocol buyer agent. It allows you to discover sellers, negotiate deals interactively, manage local API credentials, or dispatch an autonomous local AI agent ("Agent Q") to haggle on your behalf—right from your terminal.
6
6
 
7
- By keeping the execution edge-first, all cryptographic keys, session transcripts, downloaded models, and dynamic session states are stored strictly on your local machine. With robust state serialization, you can even close your terminal and resume dropped or asynchronous negotiations later.
7
+ By keeping the execution edge-first, all cryptographic keys, session transcripts, downloaded models, credentials, and deal artifacts are stored strictly on your local machine. With robust state serialization, you can even close your terminal and resume dropped or asynchronous negotiations later.
8
8
 
9
9
  ---
10
10
 
@@ -81,7 +81,22 @@ clinch negotiate ANP/C.amazon.anp --budget 85.00
81
81
  clinch negotiate ANP/C.cloudflare.anp --budget 50.00 --auto
82
82
  ```
83
83
 
84
- ### 4. Resuming Asynchronous Sessions
84
+ ### 4. Cascading Squeeze vs. Parallel Races
85
+ When discovering and negotiating with multiple sellers across a category, you can command your agent to use two distinct bargaining strategies:
86
+
87
+ #### Sequential Squeeze (`--squeeze <n>`)
88
+ Negotiate with sellers one after the other. The final price agreed upon by the previous seller is used as the strict maximum budget ceiling for the next. This systematically underbids the market.
89
+ ```bash
90
+ clinch negotiate --category domain_name --budget 150.00 --squeeze 3 --auto
91
+ ```
92
+
93
+ #### Parallel Race (`--parallel <n>`)
94
+ For time-critical needs like ride-hailing or delivery. Handshake and negotiate with all selected sellers simultaneously. Once all sessions finish, the CLI selects the cheapest deal converged under your budget.
95
+ ```bash
96
+ clinch negotiate --category ride_hailing --budget 25.00 --parallel 3 --auto
97
+ ```
98
+
99
+ ### 5. Resuming Asynchronous Sessions
85
100
  If you exit a negotiation before it concludes, or a seller places your offer in an asynchronous callback queue, the CLI automatically saves your exact session state and cryptographic session keys to disk.
86
101
 
87
102
  List your saved sessions:
@@ -94,6 +109,16 @@ Resume a specific session and listen for webhooks/callbacks:
94
109
  clinch resume <sessionId> --auto
95
110
  ```
96
111
 
112
+ ### 6. Managing Local Secrets (Blind Key Pass)
113
+ To authenticate with services that require authorization tokens or private API keys (such as Apify), you can register these credentials locally.
114
+
115
+ The credentials are encrypted locally with `AES-256-GCM` using a dynamic key bound directly to your physical machine (derived from hostname and OS parameters). The Clinch CLI will automatically decrypt and silently inject these keys at the network transport layer during handshakes, completely shielding them from your AI agent's context window.
116
+
117
+ Add an API key securely:
118
+ ```bash
119
+ clinch key
120
+ ```
121
+
97
122
  ---
98
123
 
99
124
  ## 🛠 Command Reference
@@ -111,6 +136,10 @@ Queries the registry for seller nodes.
111
136
  Opens a session with a target seller address. If `address` or `--budget` are omitted, launches the conversational wizard.
112
137
  * `[address]`: The target seller address MUST include the protocol prefix (e.g., `ANP/C.seller_domain`).
113
138
  * `--budget <n>`: Your absolute maximum budget in USD.
139
+ * `--item <name>`: Specific item to negotiate.
140
+ * `--category <name>`: Market category (Triggers cascade negotiation across matching sellers if address is omitted).
141
+ * `--squeeze <n>`: Number of sellers to sequentially squeeze (Default: `3`).
142
+ * `--parallel <n>`: Number of sellers to negotiate with simultaneously in parallel.
114
143
  * `--auto`: Delegates turn-based negotiation to the local AI sandbox.
115
144
 
116
145
  ### `clinch sessions`
@@ -120,6 +149,12 @@ Lists all historical and active negotiation sessions stored on your machine, dis
120
149
  Rehydrates a specific session's state and cryptographic keys into memory to continue negotiating or wait for remote seller callbacks.
121
150
  * `--auto`: Immediately hands the resumed session back to the local AI sandbox to evaluate incoming callbacks.
122
151
 
152
+ ### `clinch key [options]`
153
+ Manages third-party API credentials in your local hardware-bound vault. If no options are specified, launches an interactive setup prompt.
154
+ * `--set`: Interactively save a new API key credential.
155
+ * `--list`: List domains with registered local credentials.
156
+ * `--remove <domain>`: Delete a credential from your local vault.
157
+
123
158
  ---
124
159
 
125
160
  ## 📂 Local Storage & Privacy
@@ -129,4 +164,5 @@ The Clinch CLI operates on a strict zero-trust, edge-first model. Your data neve
129
164
  All state is stored locally in your home directory (`~/.clinch/`):
130
165
  * `config.json`: Your identity keys, Registry authorization token, and preferences. **Do not share this file.**
131
166
  * `sessions.json`: Local transcripts, current turns, constraint vectors, and the ephemeral Ed25519 session keys necessary to prove your identity during resumed negotiations.
167
+ * `secrets.json`: Local third-party API credentials, encrypted with AES-256-GCM and locked down with strict `0600` file permissions.
132
168
  * `deals.json`: Placeholder for cryptographically signed deal artifacts (Implementation upcoming).
package/cli.js CHANGED
@@ -9,13 +9,74 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const os = require('os');
11
11
  const readline = require('readline');
12
+ const crypto = require('crypto');
12
13
 
13
14
  const CONFIG_DIR = path.join(os.homedir(), '.clinch');
14
15
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
15
16
  const SESSIONS_FILE = path.join(CONFIG_DIR, 'sessions.json');
16
- const DEALS_FILE = path.join(CONFIG_DIR, 'deals.json'); // Restored
17
+ const DEALS_FILE = path.join(CONFIG_DIR, 'deals.json');
18
+ const SECRETS_FILE = path.join(CONFIG_DIR, 'secrets.json');
19
+
20
+ // ── Cryptographic Key Vault Helpers (Blind Key Pass) ─────────
21
+ function getEncryptionKey() {
22
+ const salt = 'clinch-local-secret-salt-398457';
23
+ const machineId = os.hostname() + os.arch() + os.platform() + os.userInfo().username;
24
+ return crypto.pbkdf2Sync(machineId, salt, 10000, 32, 'sha256');
25
+ }
26
+
27
+ function encrypt(text) {
28
+ const key = getEncryptionKey();
29
+ const iv = crypto.randomBytes(12);
30
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
31
+ let encrypted = cipher.update(text, 'utf8', 'hex');
32
+ encrypted += cipher.final('hex');
33
+ const authTag = cipher.getAuthTag().toString('hex');
34
+ return JSON.stringify({ iv: iv.toString('hex'), data: encrypted, tag: authTag });
35
+ }
36
+
37
+ function decrypt(encJson) {
38
+ try {
39
+ const key = getEncryptionKey();
40
+ const { iv, data, tag } = JSON.parse(encJson);
41
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'hex'));
42
+ decipher.setAuthTag(Buffer.from(tag, 'hex'));
43
+ let decrypted = decipher.update(data, 'hex', 'utf8');
44
+ decrypted += decipher.final('utf8');
45
+ return decrypted;
46
+ } catch (e) {
47
+ return null; // Decryption failed (file moved to different machine or tampered)
48
+ }
49
+ }
17
50
 
18
- // ── Persistence Helpers ───────────────────────────────────────
51
+ function loadSecrets() {
52
+ if (!fs.existsSync(SECRETS_FILE)) return {};
53
+ try {
54
+ const encryptedRaw = JSON.parse(fs.readFileSync(SECRETS_FILE, 'utf8'));
55
+ const decrypted = {};
56
+ for (const [domain, payload] of Object.entries(encryptedRaw)) {
57
+ const decryptedValue = decrypt(payload.encValue);
58
+ if (decryptedValue) {
59
+ decrypted[domain] = { key: decryptedValue, name: payload.name };
60
+ }
61
+ }
62
+ return decrypted;
63
+ } catch {
64
+ return {};
65
+ }
66
+ }
67
+
68
+ function saveSecrets(secrets) {
69
+ const encryptedRaw = {};
70
+ for (const [domain, payload] of Object.entries(secrets)) {
71
+ encryptedRaw[domain] = {
72
+ name: payload.name,
73
+ encValue: encrypt(payload.key)
74
+ };
75
+ }
76
+ fs.writeFileSync(SECRETS_FILE, JSON.stringify(encryptedRaw, null, 2), { mode: 0o600 });
77
+ }
78
+
79
+ // ── Config & Session Persistence Helpers ──────────────────────
19
80
  function loadConfig() {
20
81
  if (!fs.existsSync(CONFIG_FILE)) return null;
21
82
  return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
@@ -64,6 +125,12 @@ function getClinchCore(cfg) {
64
125
  const { ClinchCore } = ClinchCoreModule;
65
126
  const core = new ClinchCore({ registryUrl: cfg.registryUrl });
66
127
 
128
+ // Dynamically hydrate ClinchCore memory with locally stored API keys
129
+ const secrets = loadSecrets();
130
+ for (const [domain, s] of Object.entries(secrets)) {
131
+ core.registerSecret(domain, s.key, s.name);
132
+ }
133
+
67
134
  core.on('log', msg => console.log(msg));
68
135
  core.on('error', err => console.error('Error:', err.message));
69
136
 
@@ -224,8 +291,12 @@ program
224
291
  program
225
292
  .command('negotiate')
226
293
  .description('Start a negotiation with a seller agent')
227
- .argument('[address]', 'ANP address — format: MODE.domain.anp (e.g. ANP/A.amazon.anp)')
294
+ .argument('[address]', 'ANP address — format: MODE.domain.anp (e.g. ANP/C.amazon.anp)')
228
295
  .option('--budget <n>', 'Max budget (USD)')
296
+ .option('--item <name>', 'Specific item to negotiate')
297
+ .option('--category <name>', 'Market category (Triggers cascade negotiation across matching sellers if address is omitted)')
298
+ .option('--squeeze <n>', 'Number of sellers to sequentially squeeze (Low urgency / price optimization)', '3')
299
+ .option('--parallel <n>', 'Number of sellers to negotiate with simultaneously in parallel (High urgency / ride-hailing)')
229
300
  .option('--auto', 'Run sandbox auto-negotiation')
230
301
  .action(async (address, opts) => {
231
302
  const cfg = requireConfig();
@@ -234,7 +305,7 @@ program
234
305
  let constraints = {};
235
306
 
236
307
  // ── WIZARD MODE ──
237
- if (!targetAddress || !budget) {
308
+ if (!targetAddress && !opts.category && !budget) {
238
309
  banner();
239
310
  console.log(c.bold("💬 Clinch Onboarding Wizard — Tell me what you're looking for.\n"));
240
311
 
@@ -277,24 +348,61 @@ program
277
348
  targetAddress = `ANP/C.${sellers[parseInt(selection) - 1].agent_id}`;
278
349
  }
279
350
  } else {
280
- constraints = { intent: 'purchase', item: 'Item', max_budget: parseFloat(budget) };
281
- }
282
-
283
- if (!targetAddress.startsWith('ANP/')) {
284
- console.error(c.red(`\n✗ Invalid Address: ${targetAddress}`));
285
- console.error(" Address MUST include the protocol mode prefix.");
286
- console.error(" Example: ANP/C.amazon.anp\n");
287
- process.exit(1);
351
+ constraints = {
352
+ intent: 'purchase',
353
+ item: opts.item || 'Item',
354
+ max_budget: parseFloat(budget || 100)
355
+ };
356
+ if (opts.category) constraints.category = opts.category;
288
357
  }
289
358
 
290
- const core = getClinchCore(cfg);
291
359
  let runAuto = opts.auto;
292
-
293
360
  if (runAuto === undefined) {
294
361
  const autoInput = await prompt("\n👉 Let Agent Q negotiate autonomously? (Y/n): ");
295
362
  runAuto = autoInput.toLowerCase() !== 'n';
296
363
  }
297
364
 
365
+ const core = getClinchCore(cfg);
366
+
367
+ // ── CASCADING ITERATIVE CASCADE TRIGGER (Sequential Squeeze vs. Parallel Concurrency) ──
368
+ if (!targetAddress && opts.category) {
369
+ let maxSellers = 3;
370
+ let strategy = 'sequential';
371
+
372
+ if (opts.parallel && !opts.squeeze) {
373
+ maxSellers = parseInt(opts.parallel);
374
+ strategy = 'parallel';
375
+ console.log(c.yellow(`🤖 Parallel Mode: Handshaking concurrently with top ${maxSellers} sellers for "${opts.category}"...\n`));
376
+ } else {
377
+ maxSellers = parseInt(opts.squeeze || '3');
378
+ strategy = 'sequential';
379
+ console.log(c.yellow(`🤖 Squeeze Mode: Sequentially bargaining across top ${maxSellers} sellers for "${opts.category}"...\n`));
380
+ }
381
+
382
+ if (runAuto) {
383
+ await core.sandbox({ modelPath: cfg.modelPath });
384
+ } else {
385
+ await core.initialize(cfg.token);
386
+ }
387
+
388
+ const bestDeal = await core.negotiateCascade(opts.category, constraints, maxSellers, strategy);
389
+
390
+ if (bestDeal) {
391
+ console.log(c.green(c.bold(`\n🏆 CASCADE COMPLETE: Secured optimal deal with ${bestDeal.sellerId} at $${bestDeal.finalPrice}!`)));
392
+ } else {
393
+ console.log(c.red(`\n✗ Cascade completed without any successful deals.`));
394
+ }
395
+ process.exit(0);
396
+ }
397
+
398
+ // ── STANDARD ONE-ON-ONE HANDSHAKE ──
399
+ if (targetAddress && !targetAddress.startsWith('ANP/')) {
400
+ console.error(c.red(`\n✗ Invalid Address: ${targetAddress}`));
401
+ console.error(" Address MUST include the protocol mode prefix.");
402
+ console.error(" Example: ANP/C.amazon.anp\n");
403
+ process.exit(1);
404
+ }
405
+
298
406
  if (runAuto) {
299
407
  console.log(c.yellow('🤖 Auto-mode: Local LLM sandbox active.\n'));
300
408
  await core.sandbox({ modelPath: cfg.modelPath });
@@ -304,7 +412,7 @@ program
304
412
 
305
413
  core.on('session_started', ({ sessionId }) => {
306
414
  console.log(c.green(`\n✓ Session started: ${c.bold(sessionId)}`));
307
- saveSessionState(sessionId, core); // Initial save
415
+ saveSessionState(sessionId, core);
308
416
  });
309
417
 
310
418
  core.on('callback_received', ({ sessionId }) => saveSessionState(sessionId, core));
@@ -400,9 +508,61 @@ program
400
508
  }
401
509
  });
402
510
 
511
+ // ── KEY VAULT COMMANDS (Blind Key Pass Management) ───────────
512
+ program
513
+ .command('key')
514
+ .description('Manage third-party API credentials (Blind Key Pass vault)')
515
+ .option('--set', 'Interactively save a new API key credential')
516
+ .option('--list', 'List domains with registered local credentials')
517
+ .option('--remove <domain>', 'Delete a credential from your local vault')
518
+ .action(async (opts) => {
519
+ const cfg = requireConfig();
520
+ const secrets = loadSecrets();
521
+
522
+ if (opts.remove) {
523
+ const domain = opts.remove.toLowerCase().trim();
524
+ if (secrets[domain]) {
525
+ delete secrets[domain];
526
+ saveSecrets(secrets);
527
+ console.log(c.green(`✓ Credential vault cleared for domain: ${domain}`));
528
+ } else {
529
+ console.log(c.yellow(`No credential found for domain: ${domain}`));
530
+ }
531
+ return;
532
+ }
533
+
534
+ if (opts.list) {
535
+ const entries = Object.entries(secrets);
536
+ if (entries.length === 0) return console.log(c.yellow('Your Blind Key Pass vault is empty.'));
537
+ console.log(c.bold('\n🔑 Registered Blind Key Credentials:\n'));
538
+ entries.forEach(([domain, s]) => {
539
+ console.log(` - ${c.cyan(domain)} (${c.dim(s.name || 'unnamed')})`);
540
+ });
541
+ console.log('');
542
+ return;
543
+ }
544
+
545
+ // Default: Interactive configuration
546
+ console.log(c.bold('\n🔑 Register a local Blind Key Pass credential'));
547
+ console.log(c.dim(' Your credentials are AES-GCM encrypted and bound to this hardware locally.\n'));
548
+
549
+ const domain = await prompt('👉 Target Domain (e.g. apify.anp): ');
550
+ if (!domain) return;
551
+ const normalizedDomain = domain.toLowerCase().trim();
552
+
553
+ const name = await prompt('👉 Key Label (e.g. Apify Production Token): ');
554
+ const value = await prompt('👉 Secret Value / API Key: ');
555
+ if (!value) return;
556
+
557
+ secrets[normalizedDomain] = { key: value, name: name || 'Unnamed Key' };
558
+ saveSecrets(secrets);
559
+
560
+ console.log(c.green(`\n✓ Key registered! Handshakes targeting ${normalizedDomain} will silently inject this token.`));
561
+ });
562
+
403
563
  program
404
564
  .name('clinch')
405
565
  .description('Clinch Protocol — Agent Negotiation CLI')
406
- .version('0.4.0');
566
+ .version('0.1.0');
407
567
 
408
568
  program.parse();
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
 
8
8
  "name": "agent-clinch",
9
- "version": "0.6.2",
9
+ "version": "0.7.0",
10
10
  "description": "Clinch Protocol CLI — agent negotiation from your terminal",
11
11
  "main": "cli.js",
12
12
  "bin": {
@@ -19,6 +19,6 @@
19
19
  "dependencies": {
20
20
  "commander": "^14.0.3",
21
21
  "node-llama-cpp": "^3.18.1",
22
- "clinch-core": "0.6.2"
22
+ "clinch-core": "0.7.0"
23
23
  }
24
24
  }