agentq_demo / app.js
Pepguy's picture
Update app.js
4510252 verified
Raw History Blame Contribute Delete
15.3 kB
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { exec, spawn, execSync } = require('child_process');
const util = require('util');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { GoogleGenAI, Type } = require('@google/genai');
const execPromise = util.promisify(exec);
const app = express();
app.use(cors());
app.use(express.json());
// ============================================================================
// GLOBAL CHAT STATE (Persists independently of browser sessions)
// ============================================================================
const globalChatHistory = [];
const geminiContents = [];
// ============================================================================
// SELF-HEALING STARTUP & VAULT INITIALIZATION
// ============================================================================
function ensureClinchInstalled() {
try {
execSync('clinch --version', { stdio: 'ignore' });
console.log("✓ Clinch CLI is available on PATH.");
} catch (e) {
console.log("⚠️ Clinch CLI not found. Running programmatic installation...");
try { execSync('npm install -g agent-clinch', { stdio: 'inherit' }); }
catch (err) { execSync('npm install agent-clinch', { stdio: 'inherit' }); }
}
}
function getClinchExecutable() {
const localBin = path.join(process.cwd(), 'node_modules', '.bin', 'clinch');
return fs.existsSync(localBin) ? localBin : 'clinch';
}
ensureClinchInstalled();
const clinchBin = getClinchExecutable();
function ensureVaultInitialized() {
const VAULT_FILE = path.join(os.homedir(), '.clinch', 'vault.enc');
if (fs.existsSync(VAULT_FILE)) return;
const sellerKey = process.env.SELLER_PRIVATE_KEY || process.env.SELLER_PRIV_KEY;
const pass = process.env.CLINCH_PASSPHRASE || 'test_passphrase';
try {
if (sellerKey) execSync(`"${clinchBin}" init --key ${sellerKey}`, { input: pass + '\n', env: process.env });
else execSync(`"${clinchBin}" init`, { input: pass + '\n', env: process.env });
console.log("✓ Cryptographic identity vault successfully initialized!");
} catch (e) { console.error("❌ Vault initialization failed:", e.message); }
}
ensureVaultInitialized();
// ============================================================================
// 🔒 WEBHOOK AUTOROUTING (Wakes up the agent asynchronously)
// ============================================================================
function configureLocalWebhook(port) {
try {
// Point the CLI Daemon's webhook directly to our local Server
execSync(`"${clinchBin}" config --webhook http://localhost:${port}/api/webhook`, {
env: { ...process.env, CLINCH_PASSPHRASE: process.env.CLINCH_PASSPHRASE || 'test_passphrase' }
});
console.log(`✓ Webhook dynamically routed to local simulator on port ${port}.`);
} catch (e) {
console.error("❌ Failed to automatically register CLI Webhook:", e.message);
}
}
// Build port configuration dynamically before boot
const PORT = process.env.PORT || 7860;
configureLocalWebhook(PORT);
// ============================================================================
// BACKGROUND DAEMON THREAD
// ============================================================================
const daemon = spawn(clinchBin, ['start'], {
env: { ...process.env, CLINCH_PASSPHRASE: process.env.CLINCH_PASSPHRASE || 'test_passphrase' },
shell: true
});
daemon.stdout.on('data', (data) => console.log(`[Daemon]: ${data.toString().trim()}`));
daemon.stderr.on('data', (data) => console.error(`[Daemon Error]: ${data.toString().trim()}`));
// ============================================================================
// SHELL EXECUTOR (With Safeguard Guardrails)
// ============================================================================
async function executeShellCommand(command) {
let cmdToRun = command.trim();
if (cmdToRun.includes('rm ') || cmdToRun.includes('sudo ')) {
return JSON.stringify({ error: "Destructive commands are blocked." });
}
// Intercept semantic drift on register
if (cmdToRun.startsWith('clinch ') && cmdToRun.includes('node register')) {
if (cmdToRun.includes('@') || !cmdToRun.includes('http')) {
return JSON.stringify({
error: "SYSTEM GUARDRAIL TRIGGERED: 'clinch node register' is strictly for server hosts. To request an ID/handle on a P2P node, use 'clinch negotiate' and pass the handle constraints."
});
}
}
if (cmdToRun.startsWith('clinch ')) {
if (cmdToRun.includes(' init')) {
return JSON.stringify({ error: "Vault is already initialized. Skip 'init'." });
}
cmdToRun = `"${clinchBin}" ${cmdToRun.substring(7)}`;
}
console.log(`\n[Shell Execution]: ${cmdToRun}`);
try {
const { stdout, stderr } = await execPromise(cmdToRun, {
env: { ...process.env, CLINCH_PASSPHRASE: process.env.CLINCH_PASSPHRASE || 'test_passphrase' }
});
const output = stdout.trim() || stderr.trim();
console.log(`[Shell Output]: ${output.substring(0, 200)}...`);
return output;
} catch (error) {
const errorOutput = error.stdout || error.stderr || error.message;
console.log(`[Shell Error]: ${errorOutput}`);
return errorOutput;
}
}
// ============================================================================
// GEMINI AI INTEGRATION (Protocol Aware)
// ============================================================================
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const terminalTool = {
name: 'execute_shell_command',
description: 'Executes a command in the terminal. Exclusively use standard "clinch" CLI commands.',
parameters: {
type: Type.OBJECT,
properties: {
command: {
type: Type.STRING,
description: 'The clinch shell command (e.g., "clinch discover p2p_bridge --direct")'
}
},
required: ['command']
}
};
const systemInstruction = `You are an autonomous AI agent equipped with the Clinch Protocol skill.
Your job is to negotiate deals, schedules, and interact securely over the network. You are pragmatic, concise, and business-focused.
You have access to the 'execute_shell_command' tool to run standard 'clinch' CLI commands.
⚠️ CRITICAL ROUTING & DISCOVERY RULES:
1. DYNAMIC DISCOVERY FIRST: Before initiating ANY P2P connection, handle registration, or service command, you MUST first search the network for the correct category of node. Dynamically deduce the category from the user's intent:
- Use 'p2p_bridge' for chat, messaging, and routing nodes.
- Use 'scheduling' for booking, calendar, or scheduling nodes.
- Use 'retail' for e-commerce, shopping, or physical goods.
Command to run first: clinch discover <category> --direct
(Analyze the returned profiles to find the target agent_id and read its "instructions" to understand expected payload parameters).
2. ACCOUNT REGISTRATION: NEVER use 'clinch node register' to create user accounts, handles, or IDs. That command is exclusively for physical server hosts.
To register a user ID/handle on a discovered node, use standard negotiation with intent "register":
clinch negotiate '{"intent": "register", "item": "<service_type>", "max_budget": null, "terms": {"target_handle": "your_email@email.com"}}' --target <agent_id> --direct
3. STARTING A NEW CHAT / WORKFLOW:
Only use 'clinch negotiate' when initiating a brand new conversation or transaction with a node/peer you have NOT spoken to yet:
clinch negotiate '{"intent": "chat", "item": "p2p_message", "max_budget": null, "terms": {"target_handle": "peer@email.com", "message": "your message"}}' --target <agent_id> --direct
4. REPLYING TO INCOMING MESSAGES:
If you receive a "[SYSTEM ALERT]" indicating an incoming message on an existing Session ID, you MUST reply using the 'clinch counter' command on that specific session:
clinch counter <session_id> 0 --reason "<your_reply_message>" --direct
🛑 STRICT CONVERSATION TERMINATION (NO PLEASANTRIES):
DO NOT engage in polite echo loops with other agents.
- If an incoming message is a simple acknowledgment ("thank you", "received", "understood", "you're welcome"), a greeting ("hi", "hello"), or requires no actionable negotiation, DO NOT send a network reply.
- ONLY use 'clinch counter' if you are actively negotiating schedules, prices, details, or answering a direct query.
- It is perfectly acceptable and expected to leave the chat abruptly once information is passed. Just inform the user you have dropped the conversation.
Execute actions one step at a time, read the tool result, and report back to the user. Do not invent commands.`;
async function processAgentTurn(userMessage) {
globalChatHistory.push({ role: 'user', text: userMessage });
geminiContents.push({ role: 'user', parts: [{ text: userMessage }] });
let isDone = false;
let finalResponseText = "";
while (!isDone) {
const response = await ai.models.generateContent({
model: 'gemini-3.1-flash-lite',
contents: geminiContents,
config: {
systemInstruction: [{ text: systemInstruction }],
tools: [{ functionDeclarations: [terminalTool] }],
thinkingConfig: { thinkingLevel: 'MINIMAL' }
}
});
const call = response.functionCalls?.[0];
if (call && call.name === 'execute_shell_command') {
const args = call.args.command;
globalChatHistory.push({ role: 'system', text: `[Executing]: ${args}` });
const cliResult = await executeShellCommand(args);
globalChatHistory.push({ role: 'system', text: `[Result]: ${cliResult}` });
geminiContents.push(response.candidates[0].content);
geminiContents.push({
role: 'tool',
parts: [{
functionResponse: { name: 'execute_shell_command', response: { result: cliResult } }
}]
});
} else {
finalResponseText = response.text;
isDone = true;
}
}
globalChatHistory.push({ role: 'agent', text: finalResponseText });
geminiContents.push({ role: 'model', parts: [{ text: finalResponseText }] });
return finalResponseText;
}
// ============================================================================
// EXPRESS ROUTES & WEB UI
// ============================================================================
// 🤖 LOCAL WEBHOOK LISTENER: Wakes up and drives the agent conversation asynchronously
app.post('/api/webhook', async (req, res) => {
const { event, session } = req.body;
if (!event || !session) return res.status(400).json({ error: "Invalid webhook payload." });
console.log(`[Local Webhook] Received event: ${event} for session: ${session.sessionId}`);
if (event === 'counter_received' && session.lastMessage) {
// Construct a highly strict instruction to push into the running conversation
const systemAlert = `[SYSTEM ALERT]: An incoming P2P message has arrived over the network from node/peer '${session.targetId}' on Session ID '${session.sessionId}'.\nMessage: "${session.lastMessage}"\n\nEvaluate this message. If it contains actionable negotiation data or a direct question, use 'clinch counter' to reply. If it is just a pleasantry, greeting, or acknowledgment, DO NOT execute any commands—simply confirm to the user that the message was received and the chat is concluded.`;
// Wake up the Gemini loop in the background!
processAgentTurn(systemAlert).catch(e => {
console.error("[-] Failed to process background agent wake-up turn:", e.message);
});
}
res.json({ received: true });
});
app.post('/api/chat', async (req, res) => {
const { message } = req.body;
if (!message) return res.status(400).json({ error: "Message required" });
try { await processAgentTurn(message); res.json({ success: true }); }
catch (e) { console.error(e); res.status(500).json({ error: e.message }); }
});
app.get('/api/state', (req, res) => res.json(globalChatHistory));
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Clinch Skill Simulator</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 2rem auto; background: #111; color: #eee; }
#chat { height: 60vh; overflow-y: auto; padding: 1rem; background: #222; border-radius: 8px; margin-bottom: 1rem; }
.msg { padding: 10px; margin-bottom: 10px; border-radius: 5px; }
.user { background: #005bb5; text-align: right; }
.agent { background: #333; }
.system { background: #442222; font-family: monospace; font-size: 0.9em; color: #ff9999; }
input { width: calc(100% - 100px); padding: 10px; background: #333; color: white; border: 1px solid #555; }
button { width: 80px; padding: 10px; cursor: pointer; background: #005bb5; color: white; border: none; }
</style>
</head>
<body>
<h2>Clinch AI Skill Simulator</h2>
<div id="chat"></div>
<form id="form">
<input type="text" id="input" autocomplete="off" placeholder="Instruct the agent..." />
<button type="submit">Send</button>
</form>
<script>
const chat = document.getElementById('chat');
const form = document.getElementById('form');
const input = document.getElementById('input');
async function loadState() {
const res = await fetch('/api/state');
const history = await res.json();
chat.innerHTML = history.map(h =>
\`<div class="msg \${h.role}"><strong>\${h.role.toUpperCase()}:</strong> \${h.text.replace(/\\n/g, '<br>')}</div>\`
).join('');
chat.scrollTop = chat.scrollHeight;
}
form.onsubmit = async (e) => {
e.preventDefault();
const msg = input.value;
input.value = 'Thinking...';
input.disabled = true;
await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: msg }) });
input.value = '';
input.disabled = false;
input.focus();
loadState();
};
setInterval(loadState, 3000);
loadState();
</script>
</body>
</html>
`);
});
app.listen(PORT, () => console.log(`🤖 Clinch Agent Simulator running on http://localhost:${PORT}`));