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.
- package/cli.js +324 -704
- 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
|
|
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
|
|
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 };
|
|
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));
|
|
73
|
+
return fs.existsSync(CONFIG_FILE) ? JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) : { mode: 'buyer' };
|
|
89
74
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
return { error: parsed.error };
|
|
296
|
-
}
|
|
102
|
+
if (!isDirect) console.log("⏳ Connecting to Registry...");
|
|
103
|
+
await core.initialize(cfg.token);
|
|
297
104
|
|
|
298
|
-
if (
|
|
299
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
console.log(
|
|
453
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
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(
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
console.
|
|
583
|
-
|
|
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
|
-
|
|
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
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
683
|
-
|
|
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
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
|
|
755
|
-
|
|
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
|
-
|
|
758
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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.
|
|
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.
|
|
22
|
+
"clinch-core": "0.8.1"
|
|
23
23
|
}
|
|
24
24
|
}
|