Skill: jameseball/clawdiocomms (version 2.2.0)

--- name: clawdio version: 2.2.0 description: Secure P2P communication for AI agents via Telegram. Simple onboarding: create "Clawdio Hub" group, add bot, share keys. Noise XX handshake, XChaCha20-Poly1305 encryption, connection consent, human verification. --- # Clawdio Secure peer-to-peer communication for AI agents using Telegram as transport. **Create a "Clawdio Hub" group, add your bot, and you're ready to connect with other agents securely.** Agents perform Noise XX handshake over Telegram, then communicate via encrypted channels. ## When to Use - **Secure agent-to-agent communication** via Telegram - **Task delegation between agents** on different machines - **Distributed AI workflows** requiring encrypted P2P messaging - **Cross-platform agent coordination** without port forwarding ## Why Telegram - ✅ **Always online** — No port forwarding, NAT, or server setup - ✅ **Reliable delivery** — Messages queue when peers offline - ✅ **Universal access** — Works from anywhere with internet - ✅ **Battle-tested** — Handles billions of messages daily - ✅ **OpenClaw integration** — Simple `message` tool for send/receive - ✅ **Offline resilience** — Messages delivered when peer comes back online ## Quick Onboarding ### First Time Setup (Your Agent) 1. Install Clawdio: `clawhub install clawdiocomms` 2. Create a Telegram group called "Clawdio Hub" 3. Add your OpenClaw bot to the group 4. Send your agent the group ID (or agent auto-detects it) 5. Your agent generates your identity and you're ready ### Connecting to a Peer 1. Share your public key with your friend 2. Your friend does the same setup on their end 3. Your friend shares their public key + their Clawdio Hub group ID with you 4. Your agent creates a topic/thread in your Clawdio Hub for this peer (e.g. "James <> Alex") 5. Connection request sent — friend's agent asks for consent 6. Friend approves 7. Noise XX handshake happens over Telegram messages in the dedicated topics 8. You're connected — encrypted comms flowing ### Human Verification (Optional but Recommended) 1. Meet in person 2. Both run: compare 6-word verification codes 3. If they match, mark as human-verified ## Technical Setup ```bash cd projects/clawdio && npm install && npx tsc ``` ## Quick Start ```javascript const { Clawdio } = require('./projects/clawdio/dist/index.js'); // Create two nodes const alice = await Clawdio.create({ autoAccept: true }); const bob = await Clawdio.create({ autoAccept: true }); // Wire transport (agents decide HOW to send) alice.onSend((peerId, msg) => bob.receive(alice.publicKey, msg)); bob.onSend((peerId, msg) => alice.receive(bob.publicKey, msg)); // Connect (Noise XX handshake) const aliceId = await bob.connect(alice.publicKey); // Send messages await bob.send(aliceId, { task: "What's the weather?" }); alice.onMessage((msg, from) => console.log(msg.task)); ``` ## Using with Telegram (via OpenClaw) ```javascript const node = await Clawdio.create({ owner: 'James' }); // Send via Telegram node.onSend((peerId, base64Message) => { // Use OpenClaw's message tool to send to peer's Telegram sendTelegramMessage(peerId, base64Message); }); // When receiving a Telegram message from peer: node.receive(peerId, base64EncodedMessage); ``` ## Connection Consent Unknown inbound peers require explicit consent: ```javascript node.on('connectionRequest', (req) => { console.log(`Peer: ${req.id}, Fingerprint: ${req.fingerprint}`); node.acceptPeer(req.id); // or node.rejectPeer(req.id) }); ``` ## Human Verification ```javascript const code = node.getVerificationCode(peerId); // "torch lemon onyx prism jade index" // Compare codes in person, then: node.verifyPeer(peerId); ``` ## Persistent Identity ```javascript const node = await Clawdio.create({ identityPath: '.clawdio-identity.json' }); ``` ## API Reference | Method | Description | |--------|-------------| | `Clawdio.create(opts)` | Create and initialize a node | | `node.onSend(handler)` | Register send handler (transport layer) | | `node.receive(from, b64)` | Feed incoming message from transport | | `node.connect(peerId)` | Initiate Noise XX handshake | | `node.send(peerId, msg)` | Send encrypted message | | `node.onMessage(handler)` | Listen for decrypted messages | | `node.acceptPeer(id)` | Accept pending connection | | `node.rejectPeer(id)` | Reject pending connection | | `node.getVerificationCode(id)` | Get 6-word verification code | | `node.verifyPeer(id)` | Mark peer as human-verified | | `node.getPeerTrust(id)` | Get trust level | | `node.getFingerprint(id)` | Emoji fingerprint | | `node.getPeerStatus(id)` | alive/stale/down | | `node.stop()` | Shutdown | ## Security Properties - Forward secrecy (ephemeral X25519 keys) - Mutual authentication (Noise XX) - Replay protection (monotonic counters) - XChaCha20-Poly1305 AEAD encryption - Connection consent for inbound peers - Human verification via 6-word codes ## Dependencies Single production dependency: `libsodium-wrappers`.

path: package.json
content: {
  "name": "clawdio",
  "version": "2.0.0",
  "description": "Transport-agnostic secure P2P communication for AI agents",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "test": "node --test dist/__tests__/*.js"
  },
  "files": [
    "dist"
  ],
  "keywords": [
    "p2p",
    "agent",
    "communication",
    "encrypted",
    "noise-protocol"
  ],
  "license": "MIT",
  "dependencies": {
    "libsodium-wrappers": "^0.7.15"
  },
  "devDependencies": {
    "@types/libsodium-wrappers": "^0.7.14",
    "@types/node": "^25.2.2",
    "typescript": "^5.7.0"
  }
}


path: scripts/start.js
content: #!/usr/bin/env node
/**
 * OpenClaw Skill: Start Clawdio Node
 * 
 * This script starts a Clawdio node and returns connection information
 * that can be used by the OpenClaw agent.
 */

const { exec } = require('child_process');
const path = require('path');
const fs = require('fs');

const CLAWDIO_DIR = '/Users/jamesball/.openclaw/workspace/projects/clawdio';
const IDENTITY_FILE = path.join(CLAWDIO_DIR, '.clawdio-identity');

async function startClawdioNode() {
  try {
    // Check if identity exists
    if (!fs.existsSync(IDENTITY_FILE)) {
      throw new Error('Clawdio identity not found. Please set up Clawdio first.');
    }

    // Read the identity to get public key
    const identity = JSON.parse(fs.readFileSync(IDENTITY_FILE, 'utf8'));
    const publicKey = identity.publicKey;

    // Check if already running (basic check)
    const isRunning = await new Promise((resolve) => {
      exec('pgrep -f "node.*run.js"', (error) => {
        resolve(!error); // If no error, process exists
      });
    });

    if (isRunning) {
      console.log('⚠️  Clawdio appears to already be running');
    } else {
      console.log('🚀 Starting Clawdio node...');
      
      // Start the node in the background
      const child = exec('node run.js', {
        cwd: CLAWDIO_DIR,
        detached: true,
        stdio: 'ignore'
      });

      // Detach the child process so it continues running
      child.unref();
      
      // Give it a moment to start
      await new Promise(resolve => setTimeout(resolve, 2000));
    }

    // Return connection information
    const connectionInfo = {
      status: 'running',
      publicKey: publicKey,
      connectionString: `clawdio://${publicKey}@115.85.17.196:3579`,
      port: 3579,
      host: '115.85.17.196',
      owner: 'James'
    };

    console.log('✅ Clawdio node connection info:');
    console.log(JSON.stringify(connectionInfo, null, 2));
    
    return connectionInfo;

  } catch (error) {
    console.error('❌ Error starting Clawdio node:', error.message);
    throw error;
  }
}

// If called directly from command line
if (require.main === module) {
  startClawdioNode()
    .then((info) => {
      console.log('\n🔗 Connection string for friends:');
      console.log(info.connectionString);
    })
    .catch((error) => {
      console.error('Failed to start Clawdio:', error.message);
      process.exit(1);
    });
}

module.exports = { startClawdioNode };

path: src/__tests__/basic.test.ts
content: import { describe, it } from 'node:test';
import assert from 'node:assert';
import { Clawdio } from '../index.js';

/** Wire two nodes together: A's sends go to B's receive and vice versa */
function wire(a: Clawdio, b: Clawdio) {
  a.onSend((peerId, msg) => b.receive(a.publicKey, msg));
  b.onSend((peerId, msg) => a.receive(b.publicKey, msg));
}

describe('Clawdio P2P', () => {
  it('should perform Noise XX handshake and send encrypted messages', async () => {
    const alice = await Clawdio.create({ autoAccept: true });
    const bob = await Clawdio.create({ autoAccept: true });
    wire(alice, bob);

    const aliceId = await bob.connect(alice.publicKey);

    // Fingerprints should match
    const fpBob = bob.getFingerprint(aliceId);
    const fpAlice = alice.getFingerprint(bob.publicKey);
    assert.strictEqual(fpBob, fpAlice, 'Fingerprints should match');
    assert.ok(fpBob.length > 0, 'Fingerprint should not be empty');

    // Safety numbers should match
    const snBob = bob.getSafetyNumber(aliceId);
    const snAlice = alice.getSafetyNumber(bob.publicKey);
    assert.strictEqual(snBob, snAlice, 'Safety numbers should match');

    // Send message bob → alice
    const received = new Promise<{ msg: any; from: string }>((resolve) => {
      alice.onMessage((msg, from) => resolve({ msg, from }));
    });
    await bob.send(aliceId, { task: 'ping', status: 'ok' });
    const { msg, from } = await received;
    assert.strictEqual(msg.task, 'ping');
    assert.strictEqual(msg.status, 'ok');
    assert.strictEqual(from, bob.publicKey);

    // Reverse: alice → bob
    const received2 = new Promise<{ msg: any; from: string }>((resolve) => {
      bob.onMessage((msg, from) => resolve({ msg, from }));
    });
    await alice.send(bob.publicKey, { result: 'pong', status: 'ok' });
    const r2 = await received2;
    assert.strictEqual(r2.msg.result, 'pong');

    await alice.stop();
    await bob.stop();
  });
});


path: src/cli.ts
content: #!/usr/bin/env node
import { Command } from 'commander';
import { Clawdio } from './index.js';

const program = new Command();

program
  .name('clawdio')
  .description('Minimal secure P2P agent communication')
  .version('0.1.0');

program
  .command('start')
  .description('Start a Clawdio node and print connection string')
  .option('-p, --port <port>', 'Port to listen on', '9090')
  .option('-h, --host <host>', 'External host/IP for connection string', '127.0.0.1')
  .action(async (opts) => {
    const node = await Clawdio.create({ port: parseInt(opts.port) });
    const connStr = node.getConnectionString(opts.host);
    console.log(`\n🔑 Your connection string:\n${connStr}\n`);
    console.log('Waiting for connections... (Ctrl+C to stop)\n');
    node.on('peer', (id: string) => {
      console.log(`✅ Peer connected: ${id.slice(0, 16)}...`);
      console.log(`🔒 Fingerprint: ${node.getFingerprint(id)}`);
    });
    node.onMessage((msg, from) => {
      console.log(`📨 [${from.slice(0, 8)}...]: ${JSON.stringify(msg)}`);
    });
  });

program
  .command('connect <connectionString>')
  .description('Connect to a peer')
  .option('-p, --port <port>', 'Local port to listen on', '9091')
  .action(async (connStr, opts) => {
    const node = await Clawdio.create({ port: parseInt(opts.port) });
    console.log(`\n🔑 Your ID: ${node.publicKey.slice(0, 16)}...`);
    const peerId = await node.exchangeKeys(connStr);
    console.log(`✅ Connected to: ${peerId.slice(0, 16)}...`);
    console.log(`🔒 Fingerprint: ${node.getFingerprint(peerId)}`);
    node.onMessage((msg, from) => {
      console.log(`📨 [${from.slice(0, 8)}...]: ${JSON.stringify(msg)}`);
    });
    process.stdin.setEncoding('utf-8');
    process.stdout.write('\n> ');
    process.stdin.on('data', async (data) => {
      const text = data.toString().trim();
      if (text) { await node.send(peerId, { task: text }); console.log('Sent ✓'); }
      process.stdout.write('> ');
    });
  });

program.parse();


path: src/crypto.ts
content: import sodium from 'libsodium-wrappers';

let ready = false;
export async function init(): Promise<void> {
  if (!ready) { await sodium.ready; ready = true; }
}

export interface KeyPair { publicKey: Uint8Array; secretKey: Uint8Array; }

export function generateKeyPair(): KeyPair {
  const kp = sodium.crypto_box_keypair();
  return { publicKey: kp.publicKey, secretKey: kp.privateKey };
}

/** HKDF-like extract-and-expand using BLAKE2b */
function hkdf(ck: Uint8Array, ikm: Uint8Array): [Uint8Array, Uint8Array] {
  const tempKey = sodium.crypto_generichash(64, ikm, ck);
  const out1 = sodium.crypto_generichash(32, new Uint8Array([0x01]), tempKey);
  const buf = new Uint8Array(33); buf.set(out1); buf[32] = 0x02;
  const out2 = sodium.crypto_generichash(32, buf, tempKey);
  return [out1, out2];
}

function dh([REDACTED_SECRET], pub: Uint8Array): Uint8Array {
  return sodium.crypto_scalarmult(secret, pub);
}

/** Encrypt with XChaCha20-Poly1305 using a zero nonce (single use per key in Noise) */
function aead_encrypt(key: Uint8Array, nonce_counter: number, ad: Uint8Array, plaintext: Uint8Array): Uint8Array {
  const nonce = new Uint8Array(24);
  new DataView(nonce.buffer).setUint32(16, nonce_counter, true); // put counter in last 8 bytes area
  return sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, ad, null, nonce, key);
}

function aead_decrypt(key: Uint8Array, nonce_counter: number, ad: Uint8Array, ciphertext: Uint8Array): Uint8Array {
  const nonce = new Uint8Array(24);
  new DataView(nonce.buffer).setUint32(16, nonce_counter, true);
  return sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, ciphertext, ad, nonce, key);
}

/**
 * Noise_XX handshake: -> e | <- e, ee, s, es | -> s, se
 * Forward secrecy + mutual authentication.
 */
export class NoiseXXHandshake {
  private ck: Uint8Array;     // chaining key
  private h: Uint8Array;      // handshake hash
  private k: Uint8Array | null = null; // cipher key (null = no encryption yet)
  private n = 0;              // nonce counter

  private localStatic: KeyPair;
  private localEphemeral!: KeyPair;
  private remoteStaticPub: Uint8Array | null = null;
  private remoteEphemeralPub: Uint8Array | null = null;

  constructor(staticKeyPair: KeyPair) {
    this.localStatic = staticKeyPair;
    const protocolName = sodium.from_string([REDACTED_SECRET]);
    this.h = sodium.crypto_generichash(32, protocolName);
    this.ck = new Uint8Array(this.h);
  }

  private mixHash(data: Uint8Array): void {
    const buf = new Uint8Array(this.h.length + data.length);
    buf.set(this.h); buf.set(data, this.h.length);
    this.h = sodium.crypto_generichash(32, buf);
  }

  private mixKey(ikm: Uint8Array): void {
    const [ck, k] = hkdf(this.ck, ikm);
    this.ck = ck;
    this.k = k;
    this.n = 0;
  }

  private encryptAndHash(plaintext: Uint8Array): Uint8Array {
    if (!this.k) { this.mixHash(plaintext); return plaintext; }
    const ct = aead_encrypt(this.k, this.n++, this.h, plaintext);
    this.mixHash(ct);
    return ct;
  }

  private decryptAndHash(ciphertext: Uint8Array): Uint8Array {
    if (!this.k) { this.mixHash(ciphertext); return ciphertext; }
    const pt = aead_decrypt(this.k, this.n++, this.h, ciphertext);
    this.mixHash(ciphertext);
    return pt;
  }

  // --- Initiator ---

  /** Msg 1: -> e */
  writeMessage1(): Uint8Array {
    this.localEphemeral = generateKeyPair();
    this.mixHash(this.localEphemeral.publicKey);
    return this.localEphemeral.publicKey; // 32 bytes
  }

  /** Msg 2 received: <- e, ee, s, es */
  readMessage2(msg: Uint8Array): void {
    // e
    this.remoteEphemeralPub = msg.slice(0, 32);
    this.mixHash(this.remoteEphemeralPub);
    // ee
    this.mixKey(dh(this.localEphemeral.secretKey, this.remoteEphemeralPub));
    // s (encrypted)
    const encS = msg.slice(32);
    this.remoteStaticPub = this.decryptAndHash(encS);
    // es
    this.mixKey(dh(this.localEphemeral.secretKey, this.remoteStaticPub));
  }

  /** Msg 3: -> s, se */
  writeMessage3(): Uint8Array {
    const encS = this.encryptAndHash(this.localStatic.publicKey);
    // se
    this.mixKey(dh(this.localStatic.secretKey, this.remoteEphemeralPub!));
    return encS;
  }

  // --- Responder ---

  /** Msg 1 received: -> e */
  readMessage1(msg: Uint8Array): void {
    this.remoteEphemeralPub = msg.slice(0, 32);
    this.mixHash(this.remoteEphemeralPub);
  }

  /** Msg 2: <- e, ee, s, es */
  writeMessage2(): Uint8Array {
    // e
    this.localEphemeral = generateKeyPair();
    this.mixHash(this.localEphemeral.publicKey);
    // ee
    this.mixKey(dh(this.localEphemeral.secretKey, this.remoteEphemeralPub!));
    // s
    const encS = this.encryptAndHash(this.localStatic.publicKey);
    // es
    this.mixKey(dh(this.localStatic.secretKey, this.remoteEphemeralPub!));
    // output: e_pub || encrypted_s
    const out = new Uint8Array(32 + encS.length);
    out.set(this.localEphemeral.publicKey);
    out.set(encS, 32);
    return out;
  }

  /** Msg 3 received: -> s, se */
  readMessage3(msg: Uint8Array): void {
    this.remoteStaticPub = this.decryptAndHash(msg);
    // se
    this.mixKey(dh(this.localEphemeral.secretKey, this.remoteStaticPub));
  }

  /** Split into transport keys. Call after handshake completes. */
  getTransportKeys(isInitiator: boolean): { sendKey: Uint8Array; recvKey: Uint8Array } {
    const [k1, k2] = hkdf(this.ck, new Uint8Array(0));
    return isInitiator ? { sendKey: k1, recvKey: k2 } : { sendKey: k2, recvKey: k1 };
  }

  getRemoteStaticKey(): Uint8Array {
    if (!this.remoteStaticPub) throw new Error('Handshake not complete');
    return this.remoteStaticPub;
  }
}

// --- Fingerprint verification ---

export function fingerprint(key1: Uint8Array, key2: Uint8Array): string {
  const sorted = Buffer.compare(Buffer.from(key1), Buffer.from(key2)) < 0
    ? new Uint8Array([...key1, ...key2]) : new Uint8Array([...key2, ...key1]);
  const hash = sodium.crypto_generichash(8, sorted);
  const emojis = ['🔑','🛡','🔒','🌟','⚡','🎯','🚀','🔥','💎','🌈','🎪','🎭','🦊','🐉','🌙','☀️','🌊','🏔','🎸','🎹','🎲','🏆','🌸','🍀','🔔','🎨','🧭','🦋','🐝','🌻','🍎','🎵'];
  return Array.from(hash).map(b => emojis[b % emojis.length]).join('');
}

export function safetyNumber(key1: Uint8Array, key2: Uint8Array): string {
  const sorted = Buffer.compare(Buffer.from(key1), Buffer.from(key2)) < 0
    ? new Uint8Array([...key1, ...key2]) : new Uint8Array([...key2, ...key1]);
  const hash = sodium.crypto_generichash(16, sorted);
  return sodium.to_hex(hash).match(/.{4}/g)!.join('-');
}

// --- Transport encryption with replay protection ---

export class TransportCipher {
  private sendCounter = 0n;
  private recvCounter = -1n;

  constructor(private sendKey: Uint8Array, private recvKey: Uint8Array) {}

  encrypt(plaintext: string): { ciphertext: Uint8Array; nonce: Uint8Array; counter: string } {
    const counter = this.sendCounter++;
    const ad = new Uint8Array(8);
    new DataView(ad.buffer).setBigUint64(0, counter);
    const nonce = sodium.randombytes_buf(sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
    const ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
      sodium.from_string(plaintext), ad, null, nonce, this.sendKey
    );
    return { ciphertext, nonce, counter: counter.toString() };
  }

  decrypt(ciphertext: Uint8Array, nonce: Uint8Array, counterStr: string): string {
    const counter = BigInt(counterStr);
    if (counter <= this.recvCounter) throw new Error(`Replay: counter ${counter} <= ${this.recvCounter}`);
    const ad = new Uint8Array(8);
    new DataView(ad.buffer).setBigUint64(0, counter);
    const pt = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, ciphertext, ad, nonce, this.recvKey);
    this.recvCounter = counter;
    return sodium.to_string(pt);
  }
}

// --- Encoding helpers ---
export function toHex(buf: Uint8Array): string { return sodium.to_hex(buf); }
export function fromHex(hex: string): Uint8Array { return sodium.from_hex(hex); }
export function toBase64(buf: Uint8Array): string { return sodium.to_base64(buf, sodium.base64_variants.ORIGINAL); }
export function fromBase64(b64: string): Uint8Array { return sodium.from_base64(b64, sodium.base64_variants.ORIGINAL); }


path: src/protocol.ts
content: import { WireMessage } from './types.js';

export function encodeWire(msg: WireMessage): string { return JSON.stringify(msg); }

export function decodeWire(data: string): WireMessage {
  const parsed = JSON.parse(data);
  if (!parsed.type || !parsed.payload) throw new Error('Invalid wire message');
  return parsed as WireMessage;
}

export function parseConnectionString(connStr: string): { publicKey: string; address: string } {
  const match = connStr.match(/^clawdio:\/\/([a-f0-9]+)@(.+)$/i);
  if (!match) throw new Error(`Invalid connection string: ${connStr}`);
  return { publicKey: match[1], address: match[2] };
}

export function buildConnectionString(publicKeyHex: string, host: string, port: number): string {
  return `clawdio://${publicKeyHex}@${host}:${port}`;
}


path: src/transport.ts
content: import WebSocket, { WebSocketServer } from 'ws';
import { EventEmitter } from 'events';

export class Transport extends EventEmitter {
  private server: WebSocketServer | null = null;
  private connections = new Map<string, WebSocket>();

  async listen(port: number, host: string = '0.0.0.0'): Promise<void> {
    return new Promise((resolve, reject) => {
      this.server = new WebSocketServer({ port, host }, () => resolve());
      this.server.on('error', reject);
      this.server.on('connection', (ws) => {
        ws.on('message', (data) => {
          this.emit('data', data.toString(), ws);
        });
      });
    });
  }

  async connect(address: string): Promise<WebSocket> {
    return new Promise((resolve, reject) => {
      const ws = new WebSocket(`ws://${address}`);
      ws.on('open', () => {
        this.connections.set(address, ws);
        ws.on('message', (data) => {
          this.emit('data', data.toString(), ws);
        });
        resolve(ws);
      });
      ws.on('error', reject);
    });
  }

  send(ws: WebSocket, data: string): void {
    if (ws.readyState === WebSocket.OPEN) ws.send(data);
  }

  registerSocket(id: string, ws: WebSocket): void {
    this.connections.set(id, ws);
  }

  getSocket(id: string): WebSocket | undefined {
    return this.connections.get(id);
  }

  async close(): Promise<void> {
    for (const ws of this.connections.values()) ws.close();
    this.connections.clear();
    if (this.server) {
      await new Promise<void>((r) => this.server!.close(() => r()));
      this.server = null;
    }
  }
}


path: src/types.ts
content: import type { TransportCipher } from './crypto.js';

export interface ClawdioOptions {
  identityPath?: string;
  heartbeatMs?: number;
  heartbeatTimeout?: number;
  autoAccept?: boolean;
  owner?: string;
}

export interface AgentMessage {
  task?: string;
  result?: any;
  context?: any;
  status?: 'ok' | 'error';
  [key: string]: any;
}

export interface Peer {
  id: string;
  staticKey: Uint8Array;
  cipher: TransportCipher;
}

export interface WireMessage {
  type: 'noise1' | 'noise2' | 'noise3' | 'message' | 'ping' | 'pong';
  payload: string;
  nonce?: string;
  counter?: string;
}

export type PeerStatus = 'alive' | 'stale' | 'down';
export type TrustLevel = 'pending' | 'accepted' | 'human-verified';

export interface ConnectionRequest {
  id: string;
  fingerprint: string;
  safetyNumber: string;
}

export interface TrustedPeerRecord {
  id: string;
  trust: TrustLevel;
  addedAt: string;
  owner?: string;
}

export interface IdentityData {
  publicKey: string;
  secretKey: string;
  created: string;
  owner?: string;
  trustedPeers: TrustedPeerRecord[];
}

export type MessageHandler = (message: AgentMessage, fromPeerId: string) => void;
export type SendHandler = (peerId: string, base64Message: string) => void;


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "declaration": true,
    "sourceMap": true,
    "resolveJsonModule": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: clean (LOW)