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.
- package/cli.js +327 -682
- 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
|
|
14
|
-
|
|
15
|
-
const
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
//
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// UTILS & SETUP
|
|
71
|
+
// ============================================================================
|
|
81
72
|
function loadConfig() {
|
|
82
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
661
|
-
|
|
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
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
733
|
-
|
|
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
|
-
|
|
736
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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.
|
|
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.
|
|
22
|
+
"clinch-core": "0.8.0"
|
|
23
23
|
}
|
|
24
24
|
}
|