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