Spaces:
Running
Running
Download app.js from Pepguy/agentq_demo: direct link, hf CLI and curl.
- Browser
- Download file 15.3 kB
-
https://huggingface.co/spaces/Pepguy/agentq_demo/resolve/main/app.js
- Command line
-
hf download hf://spaces/Pepguy/agentq_demo/app.js
-
curl -L -o app.js https://huggingface.co/spaces/Pepguy/agentq_demo/resolve/main/app.js
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}`)); |