agent-clinch 0.6.2 → 0.7.1
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 +39 -3
- package/cli.js +176 -16
- 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
|
|
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
|
|
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.
|
|
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');
|
|
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
|
-
|
|
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/
|
|
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
|
|
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 = {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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);
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.7.1",
|
|
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.
|
|
22
|
+
"clinch-core": "0.7.1"
|
|
23
23
|
}
|
|
24
24
|
}
|