agent-clinch 0.7.7 → 0.8.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 (2) hide show
  1. package/cli.js +327 -682
  2. package/package.json +2 -2
package/cli.js CHANGED
@@ -1,744 +1,389 @@
1
1
  #!/usr/bin/env node
2
-
3
- // ============================================================
4
- // clinch-cli — Clinch Protocol Command Line Client
5
- // Usage: clinch <command> [options]
6
- // ============================================================
7
2
  const { program } = require('commander');
8
3
  const fs = require('fs');
9
4
  const path = require('path');
10
5
  const os = require('os');
11
- const readline = require('readline');
12
6
  const crypto = require('crypto');
13
- const https = require('https');
14
-
15
- const CONFIG_DIR = path.join(os.homedir(), '.clinch');
7
+ const readline = require('readline');
8
+ const notifier = require('node-notifier');
9
+ const express = require('express');
10
+ const nacl = require('tweetnacl');
11
+ const { ClinchCore, NegotiationState } = require('clinch-core');
12
+
13
+ const CONFIG_DIR = path.join(os.homedir(), '.clinch');
14
+ const VAULT_FILE = path.join(CONFIG_DIR, 'vault.enc');
15
+ const STATE_FILE = path.join(CONFIG_DIR, 'state.json');
16
16
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
17
- const SESSIONS_FILE = path.join(CONFIG_DIR, 'sessions.json');
18
- const DEALS_FILE = path.join(CONFIG_DIR, 'deals.json');
19
- const SECRETS_FILE = path.join(CONFIG_DIR, 'secrets.json');
20
-
21
- // ── Cryptographic Key Vault Helpers (Blind Key Pass) ─────────
22
- function getEncryptionKey() {
23
- const salt = 'clinch-local-secret-salt-398457';
24
- const machineId = os.hostname() + os.arch() + os.platform() + os.userInfo().username;
25
- return crypto.pbkdf2Sync(machineId, salt, 10000, 32, 'sha256');
26
- }
27
17
 
28
- function encrypt(text) {
29
- const key = getEncryptionKey();
30
- const iv = crypto.randomBytes(12);
31
- const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
32
- let encrypted = cipher.update(text, 'utf8', 'hex');
33
- encrypted += cipher.final('hex');
34
- const authTag = cipher.getAuthTag().toString('hex');
35
- return JSON.stringify({ iv: iv.toString('hex'), data: encrypted, tag: authTag });
36
- }
18
+ // ============================================================================
19
+ // SECURE VAULT (Now supports Blind Keys)
20
+ // ============================================================================
21
+ class SecureVault {
22
+ static _deriveKey(passphrase) {
23
+ return crypto.scryptSync(passphrase, 'clinch-protocol-salt-v1', 32, { N: 16384, r: 8, p: 1 });
24
+ }
37
25
 
38
- function decrypt(encJson) {
39
- try {
40
- const key = getEncryptionKey();
41
- const { iv, data, tag } = JSON.parse(encJson);
42
- const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'hex'));
43
- decipher.setAuthTag(Buffer.from(tag, 'hex'));
44
- let decrypted = decipher.update(data, 'hex', 'utf8');
45
- decrypted += decipher.final('utf8');
46
- return decrypted;
47
- } catch (e) {
48
- return null;
49
- }
50
- }
26
+ static async getPassphrase(isDirect) {
27
+ if (process.env.CLINCH_PASSPHRASE) return process.env.CLINCH_PASSPHRASE;
28
+ if (isDirect) throw new Error("Running in --direct mode requires CLINCH_PASSPHRASE env var.");
29
+
30
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
31
+ return new Promise(resolve => {
32
+ rl.question('\n🔒 Enter Clinch Vault Passphrase: ', ans => {
33
+ rl.close(); resolve(ans.trim());
34
+ });
35
+ });
36
+ }
51
37
 
52
- function loadSecrets() {
53
- if (!fs.existsSync(SECRETS_FILE)) return {};
54
- try {
55
- const encryptedRaw = JSON.parse(fs.readFileSync(SECRETS_FILE, 'utf8'));
56
- const decrypted = {};
57
- for (const [domain, payload] of Object.entries(encryptedRaw)) {
58
- const decryptedValue = decrypt(payload.encValue);
59
- if (decryptedValue) {
60
- decrypted[domain] = { key: decryptedValue, name: payload.name };
61
- }
38
+ static async unlock(isDirect = false) {
39
+ if (!fs.existsSync(VAULT_FILE)) return null;
40
+ const pass = await this.getPassphrase(isDirect);
41
+ const key = this._deriveKey(pass);
42
+ try {
43
+ const enc = JSON.parse(fs.readFileSync(VAULT_FILE, 'utf8'));
44
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(enc.iv, 'hex'));
45
+ decipher.setAuthTag(Buffer.from(enc.tag, 'hex'));
46
+ let dec = decipher.update(enc.data, 'hex', 'utf8');
47
+ dec += decipher.final('utf8');
48
+ const parsed = JSON.parse(dec);
49
+ if (!parsed.blindKeys) parsed.blindKeys = {};
50
+ return { parsed, pass }; // Return pass so we can re-encrypt on updates
51
+ } catch (e) {
52
+ console.error(isDirect ? JSON.stringify({ error: "Invalid passphrase" }) : "❌ Decryption failed.");
53
+ process.exit(1);
54
+ }
62
55
  }
63
- return decrypted;
64
- } catch {
65
- return {};
66
- }
67
- }
68
56
 
69
- function saveSecrets(secrets) {
70
- const encryptedRaw = {};
71
- for (const [domain, payload] of Object.entries(secrets)) {
72
- encryptedRaw[domain] = {
73
- name: payload.name,
74
- encValue: encrypt(payload.key)
75
- };
76
- }
77
- fs.writeFileSync(SECRETS_FILE, JSON.stringify(encryptedRaw, null, 2), { mode: 0o600 });
57
+ static async save(passphrase, keysData) {
58
+ const key = this._deriveKey(passphrase);
59
+ const iv = crypto.randomBytes(12);
60
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
61
+ let data = cipher.update(JSON.stringify(keysData), 'utf8', 'hex');
62
+ data += cipher.final('hex');
63
+ const tag = cipher.getAuthTag().toString('hex');
64
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
65
+ fs.writeFileSync(VAULT_FILE, JSON.stringify({ iv: iv.toString('hex'), data, tag: tag }), { mode: 0o600 });
66
+ }
78
67
  }
79
68
 
80
- // ── Config & Session Persistence Helpers ──────────────────────
69
+ // ============================================================================
70
+ // UTILS & SETUP
71
+ // ============================================================================
81
72
  function loadConfig() {
82
- if (!fs.existsSync(CONFIG_FILE)) return null;
83
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
84
- }
85
-
86
- function saveConfig(config) {
87
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
88
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
89
- }
90
-
91
- function loadSessions() {
92
- if (!fs.existsSync(SESSIONS_FILE)) return {};
93
- return JSON.parse(fs.readFileSync(SESSIONS_FILE, 'utf8'));
94
- }
95
-
96
- function saveSessionState(sessionId, core) {
97
- try {
98
- const serialized = core.exportSessionState(sessionId);
99
- const sessions = loadSessions();
100
- sessions[sessionId] = {
101
- updatedAt: new Date().toISOString(),
102
- state: serialized
103
- };
104
- fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2));
105
- } catch (e) {
106
- // Ignore gracefully
107
- }
108
- }
109
-
110
- function requireConfig() {
111
- const cfg = loadConfig();
112
- if (!cfg) {
113
- console.error(c.red('Not initialized. Run: clinch init'));
114
- process.exit(1);
115
- }
116
- return cfg;
117
- }
118
-
119
- function getClinchCore(cfg) {
120
- let ClinchCoreModule;
121
- try { ClinchCoreModule = require('clinch-core'); }
122
- catch {
123
- console.error(c.red('clinch-core not found. Ensure it is linked or installed.'));
124
- process.exit(1);
125
- }
126
- const { ClinchCore } = ClinchCoreModule;
127
- const core = new ClinchCore({ registryUrl: cfg.registryUrl });
128
-
129
- const secrets = loadSecrets();
130
- for (const [domain, s] of Object.entries(secrets)) {
131
- core.registerSecret(domain, s.key, s.name);
132
- }
133
-
134
- core.on('log', msg => console.log(msg));
135
- core.on('error', err => console.error(c.red('Error:'), err.message));
136
-
137
- return core;
73
+ return fs.existsSync(CONFIG_FILE) ? JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) : { mode: 'buyer', registryUrl: 'https://registry.clinch.network' };
138
74
  }
139
-
140
- function prompt(question) {
141
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
142
- return new Promise(resolve => {
143
- rl.question(question, answer => { rl.close(); resolve(answer.trim()); });
144
- });
75
+ function saveConfig(cfg) {
76
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
77
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
145
78
  }
146
79
 
147
- // ── AI Engine Orchestration (Ollama vs GGUF) ──────────────────
148
- async function ensureAIEngine(cfg) {
149
- if (cfg.engine) return cfg;
150
-
151
- console.log(c.bold("\n🤖 Local AI Engine Setup"));
152
- console.log(c.dim("Agent Q requires a local AI to parse intents and auto-negotiate."));
153
-
154
- const choice = await prompt("Which engine would you like to use?\n 1) Ollama (Recommended - Requires Ollama running locally)\n 2) Standalone GGUF (Downloads ~1.1GB model)\n👉 (1/2): ");
155
-
156
- if (choice === '1') {
157
- cfg.engine = 'ollama';
158
- const model = await prompt("👉 Enter Ollama model name (default: llama3): ");
159
- cfg.ollamaModel = model || 'llama3';
160
- } else {
161
- cfg.engine = 'gguf';
162
- const dl = await prompt("👉 Download default Qwen 1.5B model? (Y/n): ");
163
- if (dl.toLowerCase() !== 'n') {
164
- cfg.ggufUrl = "https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf";
165
- } else {
166
- cfg.ggufUrl = await prompt("👉 Enter custom GGUF download URL: ");
167
- }
168
- }
169
-
170
- saveConfig(cfg);
171
- return cfg;
80
+ function getRawSessions() {
81
+ if (!fs.existsSync(STATE_FILE)) return {};
82
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
172
83
  }
173
84
 
174
- function downloadFile(url, dest) {
175
- return new Promise((resolve, reject) => {
176
- const file = fs.createWriteStream(dest);
177
- const request = (currentUrl) => {
178
- https.get(currentUrl, (response) => {
179
- if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
180
- return request(response.headers.location);
181
- }
182
- if (response.statusCode !== 200) {
183
- return reject(new Error(`Download failed: ${response.statusCode}`));
184
- }
185
- const total = parseInt(response.headers['content-length'], 10);
186
- let downloaded = 0;
187
- response.on('data', (chunk) => {
188
- downloaded += chunk.length;
189
- const msg = total ? `${((downloaded / total) * 100).toFixed(1)}%` : `${(downloaded / 1024 / 1024).toFixed(1)} MB`;
190
- process.stdout.write(`\r${c.yellow('Downloading model...')} ${msg}`);
191
- });
192
- response.pipe(file);
193
- file.on('finish', () => {
194
- console.log(c.green("\n✓ Download complete!"));
195
- file.close(resolve);
196
- });
197
- }).on('error', (err) => {
198
- fs.unlink(dest, () => reject(err));
199
- });
200
- };
201
- request(url);
202
- });
85
+ function syncState(core) {
86
+ if (fs.existsSync(STATE_FILE)) core.importSessions(getRawSessions());
87
+ const save = () => fs.writeFileSync(STATE_FILE, JSON.stringify(core.exportSessions(), null, 2));
88
+ core.on('counter_received', save);
89
+ core.on('approval_required', save);
90
+ core.on('deal_signed', save);
91
+ core.on('session_cancelled', save);
92
+ return save;
203
93
  }
204
94
 
205
- async function promptAI(systemPrompt, userText, cfg) {
206
- if (cfg.engine === 'ollama') {
207
- try {
208
- console.log(c.dim(`\n[Agent Q] Dispatching request to local Ollama (${cfg.ollamaModel || 'llama3'})...`));
209
- const res = await fetch('http://127.0.0.1:11434/api/chat', {
210
- method: 'POST',
211
- headers: { 'Content-Type': 'application/json' },
212
- body: JSON.stringify({
213
- model: cfg.ollamaModel || 'llama3',
214
- messages: [
215
- { role: 'system', content: systemPrompt },
216
- { role: 'user', content: userText }
217
- ],
218
- stream: false
219
- })
220
- });
221
- if (!res.ok) throw new Error(`Ollama returned status ${res.status}`);
222
- const data = await res.json();
223
- return data.message.content;
224
- } catch (e) {
225
- console.error(c.red(`\n[!] Ollama request failed: ${e.message}`));
226
- console.error(c.dim(`Please ensure Ollama is running (http://127.0.0.1:11434) and the model is pulled.`));
227
- process.exit(1);
228
- }
229
- } else {
230
- // GGUF Execution
231
- const resolvedPath = path.resolve(cfg.modelPath || path.join(CONFIG_DIR, 'model.gguf'));
232
- if (!fs.existsSync(resolvedPath)) {
233
- console.log(c.yellow(`\nModel not found at ${resolvedPath}`));
234
- await downloadFile(cfg.ggufUrl || "https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf", resolvedPath);
235
- }
236
-
237
- console.log(c.dim(`\n[Agent Q] Loading GGUF model into memory (this may take a few seconds)...`));
238
-
239
- let nodeLlama;
240
- try { nodeLlama = await import('node-llama-cpp'); }
241
- catch (e) {
242
- console.error(c.red("\nError: 'node-llama-cpp' is required for GGUF execution."));
243
- console.error("Run: npm install -g node-llama-cpp");
244
- process.exit(1);
245
- }
246
-
247
- const llama = await nodeLlama.getLlama();
248
- const model = await llama.loadModel({ modelPath: resolvedPath });
249
- const context = await model.createContext({ contextSize: 2048, threads: Math.max(1, os.cpus().length - 1) });
250
- const session = new nodeLlama.LlamaChatSession({
251
- contextSequence: context.getSequence(),
252
- systemPrompt: systemPrompt,
253
- chatWrapper: new nodeLlama.ChatMLChatWrapper()
95
+ async function getInitializedCore(vaultData, isDirect) {
96
+ const cfg = loadConfig();
97
+ const core = new ClinchCore({
98
+ registryUrl: cfg.registryUrl,
99
+ privateKeyHex: vaultData.privateKeyHex,
100
+ blindKeys: vaultData.blindKeys // Injects blind API keys into core
254
101
  });
255
-
256
- console.log(c.dim(`[Agent Q] Model loaded. Analyzing intent...`));
257
-
258
- let responseText = "";
259
- await session.prompt(userText, { maxTokens: 1500, onTextChunk: (chunk) => { responseText += chunk; } });
260
- return responseText;
261
- }
262
- }
263
-
264
- async function parseIntentWithLLM(userInput, cfg) {
265
- const systemPrompt = `You are a structured data extractor for a purchasing agent.
266
- Analyze the user's input.
267
-
268
- If the user says a greeting (like "hi" or "hello") or does not clearly specify BOTH an item and a budget, output EXACTLY this JSON:
269
- {"error": "Please specify what you want to buy and your maximum budget (e.g. 'Get me a laptop for under $500')."}
270
-
271
- If they DO specify a purchase intent, output EXACTLY this JSON schema:
272
- {
273
- "intent": "purchase",
274
- "category": "string (e.g. electronics, domain_names, software, etc)",
275
- "item": "string (the actual item requested)",
276
- "max_budget": number (integer representing the max budget)
277
- }
278
- Your response MUST be ONLY valid JSON. Do not include conversational text.`;
279
-
280
- try {
281
- const rawRes = await promptAI(systemPrompt, userInput, cfg);
282
- const cleanJson = rawRes.replace(/```json|```/g, "").trim();
283
102
 
284
- const parsed = JSON.parse(cleanJson);
285
-
286
- // Check if the LLM flagged the input as a greeting/unclear
287
- if (parsed.error) {
288
- console.log(c.yellow(`\n[Agent Q] ${parsed.error}`));
289
- return null;
290
- }
103
+ if (!isDirect) console.log("⏳ Connecting to Registry...");
104
+ await core.initialize(cfg.token);
291
105
 
292
- // Fallback validation to ensure it didn't hallucinate missing fields
293
- if (!parsed.item || !parsed.max_budget) {
294
- console.log(c.yellow(`\n[Agent Q] I couldn't quite figure out the item or budget from your request. Please be specific!`));
295
- return null;
106
+ if (core.jwtToken && core.jwtToken !== cfg.token) {
107
+ cfg.token = core.jwtToken;
108
+ saveConfig(cfg);
296
109
  }
297
-
298
- return parsed;
299
- } catch (e) {
300
- console.error(c.red("\n[Agent Q] Failed to parse intent correctly. Falling back to manual entry."));
301
- return null;
302
- }
110
+ return core;
303
111
  }
304
112
 
305
- // ── Color & Banner helpers ────────────────────────────────────
306
- const c = {
307
- green: s => `\x1b[32m${s}\x1b[0m`,
308
- yellow: s => `\x1b[33m${s}\x1b[0m`,
309
- red: s => `\x1b[31m${s}\x1b[0m`,
310
- cyan: s => `\x1b[36m${s}\x1b[0m`,
311
- bold: s => `\x1b[1m${s}\x1b[0m`,
312
- dim: s => `\x1b[2m${s}\x1b[0m`,
313
- };
314
-
315
- function banner() {
316
- console.log(c.cyan(c.bold(`
317
- ██████╗██╗ ██╗███╗ ██╗ ██████╗██╗ ██╗
318
- ██╔════╝██║ ██║████╗ ██║██╔════╝██║ ██║
319
- ██║ ██║ ██║██╔██╗ ██║██║ ███████║
320
- ██║ ██║ ██║██║╚██╗██║██║ ██╔══██║
321
- ╚██████╗███████╗██║██║ ╚████║╚██████╗██║ ██║
322
- ╚═════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝
323
- `)));
324
- console.log(c.dim(' Agent Negotiation Protocol — v0.1.0\n'));
113
+ async function extractConstraints(intentText, isDirect) {
114
+ if (intentText.trim().startsWith('{')) return JSON.parse(intentText);
115
+ if (!isDirect) console.log("🧠 Analyzing constraint intent...");
116
+ const prompt = `Output ONLY valid JSON matching exactly:\n{"intent": "purchase|schedule|service", "item": "string", "max_budget": number | null, "terms": {"key": "value"}}\nExtract from: "${intentText}"`;
117
+ try {
118
+ const res = await fetch('http://127.0.0.1:11434/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'llama3', prompt, stream: false, format: 'json' }) });
119
+ const data = await res.json();
120
+ return JSON.parse(data.response);
121
+ } catch(e) { return { intent: "purchase", item: intentText, max_budget: null, terms: {} }; }
325
122
  }
326
123
 
327
- // ============================================================
328
- // COMMANDS
329
- // ============================================================
330
-
331
- program
332
- .command('init')
333
- .description('Initialize your Clinch buyer agent')
334
- .option('--registry <url>', 'Custom registry URL')
335
- .action(async (opts) => {
336
- banner();
337
- console.log(c.bold('Setting up your Clinch agent...\n'));
338
-
339
- let config = loadConfig() || {};
340
- if (config.pubKey) {
341
- const overwrite = await prompt('Config already exists. Overwrite network identity? (y/N): ');
342
- if (overwrite.toLowerCase() !== 'y') { console.log('Aborted.'); process.exit(0); }
343
- }
344
-
345
- config.registryUrl = opts.registry || 'https://everydaytok-agentq-core-logics.hf.space';
346
- config.modelPath = path.join(CONFIG_DIR, 'model.gguf');
347
- config = await ensureAIEngine(config);
348
-
349
- console.log(c.yellow('\nConnecting to registry and completing PoW handshake...'));
350
-
351
- const core = getClinchCore(config);
352
- await core.initialize();
353
-
354
- config.pubKey = core.identityPubKey;
355
- config.token = core.jwtToken;
356
- config.mode = 'ANP/A';
357
- config.createdAt = new Date().toISOString();
358
-
359
- saveConfig(config);
360
- core.disconnect();
361
-
362
- console.log('\n' + c.green('✓ Agent initialized successfully'));
363
- console.log(c.dim(` Public key: ${config.pubKey.substring(0,16)}...`));
364
-
365
- console.log('\n' + c.bold('🚀 Next Steps:'));
366
- console.log(` 1. Start a natural language negotiation: ${c.cyan('clinch negotiate')}`);
367
- console.log(` 2. Search the network for sellers: ${c.cyan('clinch query "electronics"')}`);
368
- console.log(` 3. Manage blind API key vaults: ${c.cyan('clinch key')}`);
369
-
370
- process.exit(0);
371
- });
372
-
373
- program
374
- .command('query')
375
- .description('Search for seller agents on the network')
376
- .argument('<category>', 'Category to search')
377
- .option('--mode <mode>', 'Filter by protocol mode')
378
- .action(async (category, opts) => {
379
- const cfg = requireConfig();
380
- const core = getClinchCore(cfg);
381
- console.log(c.cyan(`\nSearching for ${c.bold(category)} sellers...\n`));
382
-
383
- await core.initialize(cfg.token);
384
- const results = await core.search(category, opts.mode);
385
- core.disconnect();
386
-
387
- const sellers = results.results || [];
388
- if (!sellers.length) {
389
- console.log(c.yellow('No sellers found for this category.'));
390
- process.exit(0);
391
- }
392
-
393
- console.log(c.bold(`Found ${sellers.length} seller(s):\n`));
394
- sellers.forEach((s, i) => {
395
- const tier = s.verification_tier === 'verified' ? c.green('✓ Verified') : c.dim('Unverified');
396
- console.log(` ${c.bold((i+1) + '.')} ${c.cyan(s.agent_id)} (${tier})`);
397
- console.log(` ANP address: ${c.yellow('ANP/C.' + s.agent_id)}`);
398
- console.log(` Modes: ${(s.supported_modes || []).join(', ')}`);
124
+ // ============================================================================
125
+ // CLI COMMANDS
126
+ // ============================================================================
127
+
128
+ program.name('clinch').description('Clinch Protocol Command Line').version('0.1.0');
129
+
130
+ program.command('config').description('Set CLI configuration')
131
+ .option('--mode <mode>', 'buyer | seller | both')
132
+ .option('--webhook <url>', 'OpenClaw webhook URL')
133
+ .action((opts) => {
134
+ const cfg = loadConfig();
135
+ if (opts.mode) cfg.mode = opts.mode;
136
+ if (opts.webhook) cfg.openClawWebhook = opts.webhook;
137
+ saveConfig(cfg);
138
+ console.log(`✓ Config updated.`);
399
139
  });
400
-
401
- process.exit(0);
402
- });
403
-
404
- program
405
- .command('negotiate')
406
- .description('Start a negotiation with a seller agent')
407
- .argument('[address]', 'ANP address — format: MODE.domain.anp (e.g. ANP/C.amazon.anp)')
408
- .option('--budget <n>', 'Max budget (USD)')
409
- .option('--item <name>', 'Specific item to negotiate')
410
- .option('--category <name>', 'Market category (Triggers cascade negotiation across matching sellers)')
411
- .option('--squeeze <n>', 'Number of sellers to sequentially squeeze', '3')
412
- .option('--parallel <n>', 'Number of sellers to negotiate with simultaneously')
413
- .option('--auto', 'Run CLI-driven LLM auto-negotiation')
414
- .action(async (address, opts) => {
415
- let cfg = requireConfig();
416
- let targetAddress = address;
417
- let budget = opts.budget;
418
- let constraints = {};
419
-
420
- // ── WIZARD MODE ──
421
- if (!targetAddress && !opts.category && !budget) {
422
- banner();
423
- console.log(c.bold("💬 Clinch Onboarding Wizard — Tell me what you're looking for.\n"));
424
-
425
- cfg = await ensureAIEngine(cfg);
426
-
427
- const naturalIntent = await prompt("👉 Describe what you want to negotiate\n" +
428
- c.dim(" (e.g., 'Get me the domain cartpost.shop under 80 dollars')\n\n💬: "));
429
-
430
- if (!naturalIntent) process.exit(1);
431
-
432
- const parsed = await parseIntentWithLLM(naturalIntent, cfg);
433
- if (!parsed) {
434
- // We failed to parse correctly (or the user typed hi), gracefully exit rather than crashing
435
- process.exit(1);
436
- }
437
-
438
- console.log(c.bold("\n📊 Extracted Intention Context:"));
439
- console.log(` - Category: ${c.cyan(parsed.category)}`);
440
- console.log(` - Target Item: ${c.cyan(parsed.item)}`);
441
- console.log(` - Max Budget: ${c.green("$" + parsed.max_budget)}\n`);
442
-
443
- const confirm = await prompt("👉 Is this correct? (Y/n): ");
444
- if (confirm.toLowerCase() === 'n') process.exit(0);
445
-
446
- constraints = parsed;
447
- budget = parsed.max_budget;
448
-
449
- console.log(c.dim(`\n[Network] Querying registry for category "${parsed.category}"...`));
450
- const coreDiscovery = getClinchCore(cfg);
451
- await coreDiscovery.initialize(cfg.token);
452
- const results = await coreDiscovery.search(parsed.category);
453
- coreDiscovery.disconnect();
454
-
455
- const sellers = results.results || [];
456
- if (sellers.length === 0) {
457
- console.log(c.yellow(`\nNo sellers found for "${parsed.category}".`));
458
- targetAddress = await prompt("👉 Enter address manually (e.g. ANP/C.amazon.anp): ");
459
- } else {
460
- console.log(c.bold(`\nAvailable sellers:`));
461
- sellers.forEach((s, idx) => console.log(` ${idx + 1}. ${c.cyan(s.agent_id)}`));
462
- const selection = await prompt(`\n👉 Select a seller (1-${sellers.length}): `);
463
- targetAddress = `ANP/C.${sellers[parseInt(selection) - 1].agent_id}`;
464
- }
465
- } else {
466
- constraints = { intent: 'purchase', item: opts.item || 'Item', max_budget: parseFloat(budget || 100) };
467
- if (opts.category) constraints.category = opts.category;
468
- }
469
-
470
- let runAuto = opts.auto;
471
- if (runAuto === undefined) {
472
- const autoInput = await prompt("\n👉 Let Agent Q negotiate autonomously? (Y/n): ");
473
- runAuto = autoInput.toLowerCase() !== 'n';
474
- }
475
-
476
- if (runAuto) {
477
- cfg = await ensureAIEngine(cfg);
478
- }
479
140
 
480
- const core = getClinchCore(cfg);
481
-
482
- // ── CLI AUTO-NEGOTIATION HOOK ──
483
- if (runAuto) {
484
- console.log(c.yellow(`\n🤖 Auto-mode initialized. Routing inference through: ${c.bold(cfg.engine)}`));
485
-
486
- core.on('callback_received', async ({ sessionId, payload }) => {
487
- const session = core.getSession(sessionId);
488
- if (!session) return;
489
- session.currentTurn++;
490
-
491
- const incomingMessage = payload.message || JSON.stringify(payload);
492
-
493
- // Extract basic price to check constraints
494
- const priceMatch = incomingMessage.match(/price\s*:\s*\$?(\d+(?:\.\d{2})?)/i);
495
- if (priceMatch) session.lastKnownPrice = parseFloat(priceMatch[1]);
496
-
497
- if (session.lastKnownPrice > 0 && session.lastKnownPrice <= session.constraints.max_budget) {
498
- console.log(c.green(`\n🎉 [Agent Q] Seller met budget conditions! Securing deal.`));
499
- await core.sendCounter(sessionId, session.lastKnownPrice, "I accept this offer.");
500
- return;
501
- }
502
-
503
- if (session.currentTurn > 6) {
504
- console.log(c.red(`\n🛑 [Agent Q] Max turns reached. Exiting.`));
505
- await core.exitSession(sessionId);
506
- return;
507
- }
141
+ program.command('init').description('Initialize cryptographic identity')
142
+ .action(async () => {
143
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
144
+ const pass = await new Promise(resolve => rl.question('Enter strong vault passphrase: ', ans => { rl.close(); resolve(ans); }));
145
+ const seed = crypto.randomBytes(32);
146
+ const keyPair = nacl.sign.keyPair.fromSeed(seed);
147
+ const secretKeyHex = Buffer.from(keyPair.secretKey).toString('hex');
148
+ await SecureVault.save(pass, { privateKeyHex: secretKeyHex, blindKeys: {} });
149
+ console.log(`\n🎉 Identity generated and vault locked.`);
150
+ });
508
151
 
509
- const promptStr = core.buildAgentPrompt(sessionId, incomingMessage);
510
- const aiResponse = await promptAI(promptStr, incomingMessage, cfg);
511
-
512
- let price = null;
513
- let msg = "Counter offer";
152
+ // --- BLIND KEY PASS ---
153
+ program.command('key').description('Manage third-party API credentials (Blind Key Pass vault)')
154
+ .option('--set <domain>', 'Set a new key for a domain')
155
+ .option('--value <key>', 'The actual secret key value')
156
+ .option('--list', 'List registered domains')
157
+ .option('--remove <domain>', 'Delete a credential')
158
+ .action(async (opts) => {
159
+ const vaultRes = await SecureVault.unlock(false);
160
+ if (!vaultRes) return console.log("Run 'clinch init' first.");
514
161
 
515
- try {
516
- const clean = aiResponse.replace(/```json|```/g, "").trim();
517
- const parsed = JSON.parse(clean);
518
- if (parsed.price) price = parsed.price;
519
- if (parsed.message) msg = parsed.message;
520
- } catch(e) {
521
- const fallback = aiResponse.match(/"price"\s*:\s*(\d+(?:\.\d{2})?)/i);
522
- if (fallback) price = parseFloat(fallback[1]);
523
- }
524
-
525
- if (price) {
526
- await core.sendCounter(sessionId, Math.min(price, session.constraints.max_budget), msg);
527
- } else {
528
- console.log(c.yellow(`[Agent Q] Failed to parse price constraint. Sending safe fallback.`));
529
- await core.sendCounter(sessionId, session.lastKnownPrice * 0.9, "Can you do slightly better?");
530
- }
531
- });
532
- }
533
-
534
- // ── CASCADING ITERATIVE CASCADE TRIGGER ──
535
- if (!targetAddress && opts.category) {
536
- let maxSellers = 3;
537
- let strategy = 'sequential';
538
-
539
- if (opts.parallel && !opts.squeeze) {
540
- maxSellers = parseInt(opts.parallel);
541
- strategy = 'parallel';
542
- console.log(c.yellow(`🤖 Parallel Mode: Handshaking concurrently with top ${maxSellers} sellers for "${opts.category}"...\n`));
162
+ if (opts.list) {
163
+ console.log("\n🔑 Registered Blind Keys:");
164
+ Object.keys(vaultRes.parsed.blindKeys).forEach(d => console.log(` - ${d}`));
165
+ console.log("");
166
+ } else if (opts.remove) {
167
+ delete vaultRes.parsed.blindKeys[opts.remove];
168
+ await SecureVault.save(vaultRes.pass, vaultRes.parsed);
169
+ console.log(`✓ Removed blind key for ${opts.remove}`);
170
+ } else if (opts.set && opts.value) {
171
+ vaultRes.parsed.blindKeys[opts.set] = opts.value;
172
+ await SecureVault.save(vaultRes.pass, vaultRes.parsed);
173
+ console.log(`✓ Key registered! Handshakes targeting ${opts.set} will silently inject this token.`);
543
174
  } else {
544
- maxSellers = parseInt(opts.squeeze || '3');
545
- strategy = 'sequential';
546
- console.log(c.yellow(`🤖 Squeeze Mode: Sequentially bargaining across top ${maxSellers} sellers for "${opts.category}"...\n`));
175
+ console.log("Provide --list, or --set <domain> --value <key>");
547
176
  }
177
+ });
548
178
 
549
- await core.initialize(cfg.token);
550
- const bestDeal = await core.negotiateCascade(opts.category, constraints, maxSellers, strategy);
551
-
552
- if (bestDeal) {
553
- console.log(c.green(c.bold(`\n🏆 CASCADE COMPLETE: Secured optimal deal with ${bestDeal.sellerId} at $${bestDeal.finalPrice}!`)));
554
- } else {
555
- console.log(c.red(`\n✗ Cascade completed without any successful deals.`));
556
- }
557
- process.exit(0);
558
- }
559
-
560
- // ── STANDARD ONE-ON-ONE HANDSHAKE ──
561
- if (targetAddress && !targetAddress.startsWith('ANP/')) {
562
- console.error(c.red(`\n✗ Invalid Address: ${targetAddress}`));
563
- console.error(" Address MUST include the protocol mode prefix.");
564
- console.error(" Example: ANP/C.amazon.anp\n");
565
- process.exit(1);
566
- }
179
+ program.command('start').description('Start the listener daemon')
180
+ .action(async () => {
181
+ const cfg = loadConfig();
182
+ const vaultRes = await SecureVault.unlock(false);
183
+ if (!vaultRes) return console.log("Run 'clinch init' first.");
184
+
185
+ const core = await getInitializedCore(vaultRes.parsed, false);
186
+ syncState(core);
187
+ console.log("🟢 Clinch Daemon active. Listening for events...");
188
+
189
+ core.on('approval_required', async (s) => {
190
+ const msg = `Approval Required: ${s.targetId} agreed to $${s.lastPrice} for ${s.constraints.item}`;
191
+ console.log(`\n⚠️ ${msg}\n Run: clinch approve ${s.sessionId}`);
192
+ notifier.notify({ title: 'Clinch Protocol', message: msg });
193
+ if (cfg.openClawWebhook) await fetch(cfg.openClawWebhook, { method: 'POST', body: JSON.stringify({ event: 'approval_required', session: s }) }).catch(()=>{});
194
+ });
567
195
 
568
- await core.initialize(cfg.token);
196
+ core.on('counter_received', async (s) => {
197
+ console.log(`\n💬 Counter from ${s.targetId}: $${s.lastPrice}`);
198
+ if (cfg.openClawWebhook) await fetch(cfg.openClawWebhook, { method: 'POST', body: JSON.stringify({ event: 'counter_received', session: s }) }).catch(()=>{});
199
+ });
569
200
 
570
- core.on('session_started', ({ sessionId }) => {
571
- console.log(c.green(`\n Session started: ${c.bold(sessionId)}`));
572
- saveSessionState(sessionId, core);
201
+ core.connectDaemonStream();
202
+ process.on('SIGINT', () => { console.log('\n👋 Shutting down daemon...'); process.exit(0); });
203
+ process.on('SIGTERM', () => { console.log('\n👋 Shutting down daemon...'); process.exit(0); });
573
204
  });
574
205
 
575
- if (!runAuto) {
576
- core.on('callback_received', ({ sessionId, payload }) => {
577
- saveSessionState(sessionId, core);
578
- console.log(c.cyan(`\n💬 Seller says:`), payload);
579
- console.log(c.dim(`\nType a price to counter, or "exit" / "accept":`));
580
- });
581
- }
582
-
583
- core.on('session_closed', ({ sessionId, outcome, finalPrice }) => {
584
- saveSessionState(sessionId, core);
585
- if (outcome === 'deal') {
586
- console.log(c.green(c.bold(`\n🎉 DEAL SECURED at $${finalPrice}`)));
587
- process.exit(0);
588
- }
206
+ // --- CORE NEGOTIATION COMMANDS ---
207
+
208
+ program.command('status').description('List all active negotiations')
209
+ .option('--direct', 'JSON output mode')
210
+ .action((opts) => {
211
+ const data = getRawSessions();
212
+ const sessions = Object.values(data).filter(s => s.state !== 'SIGNED' && s.state !== 'CANCELLED');
213
+ if (opts.direct) return console.log(JSON.stringify(sessions));
214
+ console.log("\n📊 Active Negotiations:");
215
+ sessions.forEach(s => console.log(` [${s.state}] ID: ${s.sessionId} | Target: ${s.targetId.substring(0,8)}... | Item: ${s.constraints.item} | Last Price: $${s.lastPrice}`));
216
+ console.log("");
589
217
  });
590
218
 
591
- core.on('status_changed', status => {
592
- if (status === 'STALEMATE') {
593
- console.log(c.red('\n✗ Stalemate. Exiting.'));
594
- process.exit(0);
595
- }
219
+ program.command('deals').description('List completed/signed deals')
220
+ .option('--direct', 'JSON output mode')
221
+ .action((opts) => {
222
+ const data = getRawSessions();
223
+ const deals = Object.values(data).filter(s => s.state === 'SIGNED' && s.artifact !== null && s.artifact !== undefined).map(s => s.artifact);
224
+ if (opts.direct) return console.log(JSON.stringify(deals));
225
+ console.log("\n🔐 Signed Deals:");
226
+ deals.forEach(d => console.log(` ID: ${d.sessionId} | Item: ${d.item} | Price: $${d.price} | Date: ${new Date(d.timestamp).toLocaleString()}`));
227
+ console.log("");
596
228
  });
597
229
 
598
- const sessionId = await core.negotiate(targetAddress, constraints);
230
+ program.command('negotiate <intent>').description('Initialize a negotiation')
231
+ .option('--target <domain>', 'Target seller domain')
232
+ .option('--direct', 'JSON output mode')
233
+ .action(async (intent, opts) => {
234
+ const vaultRes = await SecureVault.unlock(opts.direct);
235
+ const core = await getInitializedCore(vaultRes.parsed, opts.direct);
236
+ const save = syncState(core);
599
237
 
600
- if (!runAuto) {
601
- console.log(c.bold('\nManual mode await seller response, then type a price to counter, or "exit" / "accept".\n'));
602
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
603
- rl.on('line', async (cmd) => {
604
- if (cmd === 'exit') {
605
- await core.exitSession(sessionId);
606
- saveSessionState(sessionId, core);
607
- process.exit(0);
608
- }
609
- else if (cmd === 'accept') { console.log(c.green(`Accepting...`)); }
610
- else {
611
- const price = parseFloat(cmd);
612
- if (!isNaN(price)) {
613
- await core.sendCounter(sessionId, price, 'Counter offer');
614
- saveSessionState(sessionId, core);
615
- }
616
- }
617
- });
618
- }
619
- });
620
-
621
- program
622
- .command('sessions')
623
- .description('List saved negotiation sessions')
624
- .action(() => {
625
- const sessions = loadSessions();
626
- const ids = Object.keys(sessions);
627
- if (ids.length === 0) {
628
- console.log(c.yellow('No saved sessions found.'));
629
- process.exit(0);
630
- }
238
+ const constraints = await extractConstraints(intent, opts.direct);
239
+ let target = opts.target || (await core.discover(constraints.item))[0]?.agent_id;
240
+ if (!target) return console.log(opts.direct ? JSON.stringify({ error: "No sellers found" }) : "❌ No sellers found.");
241
+
242
+ const session = await core.proposeDeal(target, constraints);
243
+ save();
631
244
 
632
- console.log(c.bold(`Found ${ids.length} session(s):\n`));
633
- ids.forEach(id => {
634
- const s = JSON.parse(sessions[id].state);
635
- console.log(` ${c.cyan(id)} - Target: ${s.sellerId} | Status: ${c.bold(s.status)} | Turn: ${s.currentTurn}`);
245
+ if (session.state === NegotiationState.CANCELLED) {
246
+ if (opts.direct) console.log(JSON.stringify({ status: "CANCELLED", session }));
247
+ else console.log(`\n❌ Seller rejected the proposal.\nSession ID: ${session.sessionId}\n`);
248
+ } else {
249
+ if (opts.direct) console.log(JSON.stringify({ status: "SUCCESS", session }));
250
+ else console.log(`\n✓ Proposal sent to ${target}.\nSession ID: ${session.sessionId}\n`);
251
+ }
636
252
  });
637
-
638
- process.exit(0);
639
- });
640
-
641
- program
642
- .command('resume')
643
- .description('Resume a dropped or asynchronous negotiation session')
644
- .argument('<sessionId>', 'The session ID to resume')
645
- .option('--auto', 'Resume with auto-negotiation')
646
- .action(async (sessionId, opts) => {
647
- let cfg = requireConfig();
648
- const sessions = loadSessions();
649
-
650
- if (!sessions[sessionId]) {
651
- console.error(c.red(`Session ${sessionId} not found in local store.`));
652
- process.exit(1);
653
- }
654
253
 
655
- console.log(c.yellow(`\nRehydrating Session ${c.bold(sessionId)}...\n`));
656
- if (opts.auto) {
657
- cfg = await ensureAIEngine(cfg);
658
- }
254
+ program.command('counter <sessionId> <price>').description('Counter an offer')
255
+ .option('--reason <msg>', 'Reason for counter', 'Counter offer')
256
+ .option('--direct', 'JSON output mode')
257
+ .action(async (sessionId, price, opts) => {
258
+ const vaultRes = await SecureVault.unlock(opts.direct);
259
+ const core = await getInitializedCore(vaultRes.parsed, opts.direct);
260
+ const save = syncState(core);
659
261
 
660
- const core = getClinchCore(cfg);
661
- await core.initialize(cfg.token);
262
+ try {
263
+ const session = await core.counter(sessionId, parseFloat(price), opts.reason);
264
+ save();
265
+ if (session.state === NegotiationState.CANCELLED) {
266
+ if (opts.direct) console.log(JSON.stringify({ status: "CANCELLED", session }));
267
+ else console.log(`\n❌ Seller cancelled the negotiation.\nSession ID: ${session.sessionId}\n`);
268
+ } else {
269
+ if (opts.direct) console.log(JSON.stringify({ status: "COUNTERED", session }));
270
+ else console.log(`✓ Counter of $${price} sent for ${sessionId}`);
271
+ }
272
+ } catch (e) { console.log(opts.direct ? JSON.stringify({ error: e.message }) : `❌ Error: ${e.message}`); }
273
+ });
662
274
 
663
- core.importSessionState(sessions[sessionId].state);
275
+ program.command('cancel <sessionId>').description('Cleanly exit a negotiation')
276
+ .option('--direct', 'JSON output mode')
277
+ .action(async (sessionId, opts) => {
278
+ const vaultRes = await SecureVault.unlock(opts.direct);
279
+ const core = await getInitializedCore(vaultRes.parsed, opts.direct);
280
+ const save = syncState(core);
664
281
 
665
- core.on('callback_received', ({ id }) => saveSessionState(id, core));
666
- core.on('session_closed', ({ outcome, finalPrice }) => {
667
- saveSessionState(sessionId, core);
668
- if (outcome === 'deal') console.log(c.green(c.bold(`\n🎉 DEAL SECURED at $${finalPrice}`)));
669
- process.exit(0);
282
+ try {
283
+ const session = await core.cancelSession(sessionId);
284
+ save();
285
+ if (opts.direct) console.log(JSON.stringify({ status: "CANCELLED", session }));
286
+ else console.log(`✓ Session ${sessionId} cleanly cancelled.`);
287
+ } catch (e) { console.log(opts.direct ? JSON.stringify({ error: e.message }) : `❌ Error: ${e.message}`); }
670
288
  });
671
289
 
672
- console.log(c.green(' State rehydrated. Listening for webhooks/callbacks...\n'));
673
- if (!opts.auto) {
674
- console.log(c.dim('Awaiting remote updates. Press Ctrl+C to detach.'));
675
- }
676
- });
677
-
678
- // ── KEY VAULT COMMANDS (Blind Key Pass Management) ───────────
679
- program
680
- .command('key')
681
- .description('Manage third-party API credentials (Blind Key Pass vault)')
682
- .option('--set', 'Interactively save a new API key credential')
683
- .option('--list', 'List domains with registered local credentials')
684
- .option('--remove <domain>', 'Delete a credential from your local vault')
685
- .option('--show', 'Display the raw API keys when listing')
686
- .action(async (opts) => {
687
- const cfg = requireConfig();
688
- const secrets = loadSecrets();
689
-
690
- if (opts.remove) {
691
- const domain = opts.remove.toLowerCase().trim();
692
- if (secrets[domain]) {
693
- delete secrets[domain];
694
- saveSecrets(secrets);
695
- console.log(c.green(`✓ Credential vault cleared for domain: ${domain}`));
696
- } else {
697
- console.log(c.yellow(`No credential found for domain: ${domain}`));
698
- }
699
- process.exit(0);
700
- }
701
-
702
- if (opts.list) {
703
- const entries = Object.entries(secrets);
704
- if (entries.length === 0) {
705
- console.log(c.yellow('Your Blind Key Pass vault is empty.'));
706
- process.exit(0);
707
- }
708
- console.log(c.bold('\n🔑 Registered Blind Key Credentials:\n'));
709
- entries.forEach(([domain, s]) => {
710
- if (opts.show) {
711
- console.log(` - ${c.cyan(domain)} (${c.dim(s.name || 'unnamed')}) -> ${c.yellow(s.key)}`);
712
- } else {
713
- console.log(` - ${c.cyan(domain)} (${c.dim(s.name || 'unnamed')}) -> ${c.dim('••••••••••••')}`);
714
- }
715
- });
716
- console.log(c.dim(opts.show ? '' : '\n(Run with --show to view raw keys)'));
717
- process.exit(0);
718
- }
290
+ program.command('approve <sessionId>').description('Cryptographically sign a CONFIRMED deal')
291
+ .option('--direct', 'JSON output mode')
292
+ .action(async (sessionId, opts) => {
293
+ const vaultRes = await SecureVault.unlock(opts.direct);
294
+ const core = await getInitializedCore(vaultRes.parsed, opts.direct);
295
+ const save = syncState(core);
719
296
 
720
- // Default: Interactive configuration
721
- console.log(c.bold('\n🔑 Register a local Blind Key Pass credential'));
722
- console.log(c.dim(' Your credentials are AES-GCM encrypted and bound to this hardware locally.\n'));
297
+ try {
298
+ const artifact = await core.approveAndSign(sessionId);
299
+ save();
300
+ if (opts.direct) console.log(JSON.stringify({ status: "SIGNED", artifact }));
301
+ else console.log(`\n🔐 DEAL SIGNED AND COMMITTED!\nArtifact ID: ${artifact.sessionId}\n`);
302
+ } catch (e) { console.log(opts.direct ? JSON.stringify({ error: e.message }) : `\n❌ Approval Failed: ${e.message}\n`); }
303
+ });
723
304
 
724
- const domain = await prompt('👉 Target Domain (e.g. apify.anp): ');
725
- if (!domain) process.exit(0);
726
- const normalizedDomain = domain.toLowerCase().trim();
305
+ // --- SELLER MODE ---
306
+ const nodeCmd = program.command('node').description('Node management commands');
307
+ nodeCmd.command('register <endpoint>').description('Register your seller endpoint to the Registry')
308
+ .option('--categories <list>', 'Comma separated categories', 'general')
309
+ .action(async (endpoint, opts) => {
310
+ const vaultRes = await SecureVault.unlock(false);
311
+ const core = await getInitializedCore(vaultRes.parsed, false);
312
+ await core.registerNode(endpoint, opts.categories.split(','), ['http-webhook']);
313
+ console.log(`✓ Node registered at ${endpoint} for categories: ${opts.categories}`);
314
+ });
727
315
 
728
- const name = await prompt('👉 Key Label (e.g. Apify Production Token): ');
729
- const value = await prompt('👉 Secret Value / API Key: ');
730
- if (!value) process.exit(0);
316
+ program.command('serve').description('Start Seller HTTP server')
317
+ .option('--port <p>', 'Port to listen on', 8080)
318
+ .option('--config <file>', 'Path to seller config JSON')
319
+ .option('--direct', 'JSON output mode for background agent handling')
320
+ .action(async (opts) => {
321
+ const vaultRes = await SecureVault.unlock(opts.direct);
322
+ const core = await getInitializedCore(vaultRes.parsed, opts.direct);
323
+ const save = syncState(core);
324
+
325
+ const sellerCfg = opts.config && fs.existsSync(opts.config)
326
+ ? JSON.parse(fs.readFileSync(opts.config))
327
+ : { defaultFloor: 45, defaultApprove: 100, maxTurns: 5 };
328
+
329
+ const app = express();
330
+ app.use(express.json());
331
+
332
+ app.post('/handshake', async (req, res) => {
333
+ const { session_id, constraints, buyer_pub_key } = req.body;
334
+ core.registerIncomingSession(session_id, buyer_pub_key, constraints);
335
+
336
+ if (opts.direct) console.log(JSON.stringify({ event: "INCOMING_PROPOSAL", session_id, constraints }));
337
+ else console.log(`\n🔔 Incoming Proposal: ${session_id} wants ${constraints.item} for ${constraints.max_budget !== null ? '$'+constraints.max_budget : 'unspecified'}`);
338
+
339
+ const categoryCfg = sellerCfg.categories?.[constraints.item] || sellerCfg;
340
+ const maxTurns = categoryCfg.maxTurns || 5;
341
+
342
+ if (constraints.max_budget !== null && constraints.max_budget >= categoryCfg.defaultApprove) {
343
+ core.updateSessionStateLocally(session_id, NegotiationState.CONFIRMED, categoryCfg.defaultApprove, 1);
344
+ res.json({ type: 'CONFIRM', price: categoryCfg.defaultApprove });
345
+ } else {
346
+ const counter = Math.max(categoryCfg.defaultFloor, (categoryCfg.defaultFloor + categoryCfg.defaultApprove) / 2);
347
+ core.updateSessionStateLocally(session_id, NegotiationState.COUNTERED, counter, 2);
348
+ res.json({ type: 'COUNTER', price: counter, turn: 2 });
349
+ }
350
+ save();
351
+ });
731
352
 
732
- secrets[normalizedDomain] = { key: value, name: name || 'Unnamed Key' };
733
- saveSecrets(secrets);
353
+ app.post('/counter', async (req, res) => {
354
+ const { session_id, price, turn } = req.body;
355
+ const categoryCfg = sellerCfg;
356
+ const currentTurn = turn || 2;
357
+ const nextTurn = currentTurn + 1;
358
+ const maxTurns = categoryCfg.maxTurns || 5;
359
+
360
+ if (price >= categoryCfg.defaultApprove) {
361
+ core.updateSessionStateLocally(session_id, NegotiationState.CONFIRMED, price, currentTurn);
362
+ res.json({ type: 'CONFIRM', price });
363
+ } else if (currentTurn >= maxTurns) {
364
+ if (price >= categoryCfg.defaultFloor) {
365
+ core.updateSessionStateLocally(session_id, NegotiationState.CONFIRMED, price, currentTurn);
366
+ res.json({ type: 'CONFIRM', price });
367
+ } else {
368
+ core.updateSessionStateLocally(session_id, NegotiationState.CANCELLED, price, currentTurn);
369
+ res.json({ type: 'CANCEL', reason: "Max turns reached without meeting floor." });
370
+ }
371
+ } else {
372
+ const counter = Math.max(categoryCfg.defaultFloor, (categoryCfg.defaultFloor + categoryCfg.defaultApprove) / 2);
373
+ core.updateSessionStateLocally(session_id, NegotiationState.COUNTERED, counter, nextTurn);
374
+ res.json({ type: 'COUNTER', price: counter, turn: nextTurn });
375
+ }
376
+ save();
377
+ });
734
378
 
735
- console.log(c.green(`\n✓ Key registered! Handshakes targeting ${normalizedDomain} will silently inject this token.`));
736
- process.exit(0);
737
- });
379
+ app.post('/sign_request', (req, res) => {
380
+ try { res.json({ sellerSignature: core.signAsSeller(req.body.artifact) }); }
381
+ catch (e) { res.status(400).json({ error: e.message }); }
382
+ });
738
383
 
739
- program
740
- .name('clinch')
741
- .description('Clinch Protocol Agent Negotiation CLI')
742
- .version('0.1.0');
384
+ app.listen(opts.port, () => { if (!opts.direct) console.log(`\n🛒 Seller Node active on port ${opts.port}`); });
385
+ process.on('SIGINT', () => { console.log('\n👋 Shutting down seller node...'); process.exit(0); });
386
+ process.on('SIGTERM', () => { console.log('\n👋 Shutting down seller node...'); process.exit(0); });
387
+ });
743
388
 
744
389
  program.parse();
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
 
8
8
  "name": "agent-clinch",
9
- "version": "0.7.7",
9
+ "version": "0.8.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.7.4"
22
+ "clinch-core": "0.8.0"
23
23
  }
24
24
  }