open-agents-ai 0.83.0 → 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11223,12 +11223,13 @@ var init_nexus = __esm({
11223
11223
  /**
11224
11224
  * nexus-daemon.mjs \u2014 Standalone nexus process with real TCP/UDP sockets.
11225
11225
  * Spawned by the open-agents nexus tool. Communicates via JSON files.
11226
+ * v1.5.0: capability registration, peer blocking, metering, room members.
11226
11227
  *
11227
11228
  * Usage: node nexus-daemon.mjs <nexus-dir> <agent-name> [agent-type]
11228
11229
  */
11229
11230
 
11230
- import { NexusClient } from 'open-agents-nexus';
11231
- import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, readdirSync } from 'node:fs';
11231
+ import { NexusClient, createFileAuditHook } from 'open-agents-nexus';
11232
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, readdirSync, appendFileSync } from 'node:fs';
11232
11233
  import { join } from 'node:path';
11233
11234
 
11234
11235
  const nexusDir = process.argv[2];
@@ -11239,8 +11240,11 @@ const respFile = join(nexusDir, 'resp.json');
11239
11240
  const statusFile = join(nexusDir, 'status.json');
11240
11241
  const inboxDir = join(nexusDir, 'inbox');
11241
11242
  const pidFile = join(nexusDir, 'daemon.pid');
11243
+ const invocationsDir = join(nexusDir, 'invocations');
11244
+ const meteringFile = join(nexusDir, 'metering.jsonl');
11242
11245
 
11243
11246
  mkdirSync(inboxDir, { recursive: true });
11247
+ mkdirSync(invocationsDir, { recursive: true });
11244
11248
 
11245
11249
  // Write PID so the agent can kill us
11246
11250
  writeFileSync(pidFile, String(process.pid));
@@ -11257,12 +11261,16 @@ const nexus = new NexusClient({
11257
11261
  natsServers: ['wss://demo.nats.io:8443'],
11258
11262
  enableCircuitRelay: true,
11259
11263
  usePublicBootstrap: true,
11264
+ trustPolicy: { denylist: [], allowlist: [] },
11260
11265
  });
11261
11266
 
11262
11267
  const rooms = new Map();
11263
11268
  let connected = false;
11269
+ const blockedPeers = [];
11264
11270
 
11265
11271
  function writeStatus(extra = {}) {
11272
+ const caps = typeof nexus.getRegisteredCapabilities === 'function'
11273
+ ? nexus.getRegisteredCapabilities() : [];
11266
11274
  const data = {
11267
11275
  pid: process.pid,
11268
11276
  connected,
@@ -11270,6 +11278,8 @@ function writeStatus(extra = {}) {
11270
11278
  agentName,
11271
11279
  agentType,
11272
11280
  rooms: [...rooms.keys()],
11281
+ capabilities: caps,
11282
+ blockedPeers,
11273
11283
  connectedAt: connected ? new Date().toISOString() : null,
11274
11284
  ...extra,
11275
11285
  };
@@ -11289,6 +11299,7 @@ async function handleCmd(cmd) {
11289
11299
  if (rooms.has(roomId)) { writeResp(id, { ok: true, output: 'Already in room: ' + roomId }); return; }
11290
11300
  const room = await nexus.joinRoom(roomId);
11291
11301
  rooms.set(roomId, room);
11302
+ // Per-room message listener (also captured by client-level event)
11292
11303
  room.on('message', (msg) => {
11293
11304
  const roomInbox = join(inboxDir, roomId);
11294
11305
  mkdirSync(roomInbox, { recursive: true });
@@ -11302,6 +11313,15 @@ async function handleCmd(cmd) {
11302
11313
  };
11303
11314
  try { writeFileSync(join(roomInbox, fname), JSON.stringify(entry, null, 2)); } catch {}
11304
11315
  });
11316
+ // v1.5.0: Room member tracking
11317
+ if (room.on) {
11318
+ room.on('member:join', (member) => {
11319
+ console.log('Member joined ' + roomId + ': ' + (member.agentName || member.peerId));
11320
+ });
11321
+ room.on('member:leave', (member) => {
11322
+ console.log('Member left ' + roomId + ': ' + (member.agentName || member.peerId));
11323
+ });
11324
+ }
11305
11325
  writeStatus();
11306
11326
  writeResp(id, { ok: true, output: 'Joined room: ' + roomId });
11307
11327
  break;
@@ -11372,6 +11392,366 @@ async function handleCmd(cmd) {
11372
11392
  writeResp(id, { ok: true, output: JSON.stringify(data, null, 2) });
11373
11393
  break;
11374
11394
  }
11395
+
11396
+ // \u2500\u2500 v1.5.0: Capability Registration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11397
+ case 'register_capability': {
11398
+ const name = args.capability;
11399
+ if (!name) { writeResp(id, { ok: false, output: 'capability name is required' }); return; }
11400
+ if (typeof nexus.registerCapability !== 'function') {
11401
+ writeResp(id, { ok: false, output: 'registerCapability not available (nexus version too old)' });
11402
+ return;
11403
+ }
11404
+ nexus.registerCapability(name, async (request, stream) => {
11405
+ // Log inbound invocation
11406
+ const logEntry = {
11407
+ ts: Date.now(),
11408
+ from: request.from || 'unknown',
11409
+ capability: request.capability || name,
11410
+ requestId: request.requestId,
11411
+ };
11412
+ const logFile = join(invocationsDir, Date.now() + '-' + name + '.json');
11413
+ try { writeFileSync(logFile, JSON.stringify(logEntry, null, 2)); } catch {}
11414
+
11415
+ // Collect input chunks
11416
+ let inputData = '';
11417
+ stream.onData((msg) => {
11418
+ if (msg.type === 'invoke.chunk') {
11419
+ inputData += (typeof msg.data === 'string' ? msg.data : JSON.stringify(msg.data));
11420
+ }
11421
+ });
11422
+
11423
+ // Accept the invocation
11424
+ await stream.write({
11425
+ type: 'invoke.accept', version: 1,
11426
+ requestId: request.requestId, accepted: true,
11427
+ });
11428
+
11429
+ // Send event with acknowledgment
11430
+ await stream.write({
11431
+ type: 'invoke.event', version: 1,
11432
+ requestId: request.requestId, seq: 0,
11433
+ event: 'ack', data: agentName + ' received invocation for ' + name,
11434
+ });
11435
+
11436
+ // Complete
11437
+ await stream.write({
11438
+ type: 'invoke.done', version: 1,
11439
+ requestId: request.requestId,
11440
+ usage: { inputBytes: inputData.length, outputBytes: 0 },
11441
+ });
11442
+ stream.close();
11443
+ });
11444
+ writeStatus();
11445
+ writeResp(id, { ok: true, output: 'Capability registered: ' + name + '. Now advertised via NATS.' });
11446
+ break;
11447
+ }
11448
+ case 'unregister_capability': {
11449
+ const name = args.capability;
11450
+ if (!name) { writeResp(id, { ok: false, output: 'capability name is required' }); return; }
11451
+ if (typeof nexus.unregisterCapability === 'function') {
11452
+ nexus.unregisterCapability(name);
11453
+ }
11454
+ writeStatus();
11455
+ writeResp(id, { ok: true, output: 'Capability unregistered: ' + name });
11456
+ break;
11457
+ }
11458
+ case 'list_capabilities': {
11459
+ const caps = typeof nexus.getRegisteredCapabilities === 'function'
11460
+ ? nexus.getRegisteredCapabilities() : [];
11461
+ writeResp(id, { ok: true, output: caps.length ? 'Capabilities: ' + caps.join(', ') : 'No capabilities registered.' });
11462
+ break;
11463
+ }
11464
+
11465
+ // \u2500\u2500 v1.5.0: Peer Blocking \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11466
+ case 'block_peer': {
11467
+ const peerId = args.target_peer || args.peer_id;
11468
+ if (!peerId) { writeResp(id, { ok: false, output: 'peer_id or target_peer is required' }); return; }
11469
+ if (typeof nexus.blockPeer === 'function') {
11470
+ nexus.blockPeer(peerId);
11471
+ }
11472
+ if (!blockedPeers.includes(peerId)) blockedPeers.push(peerId);
11473
+ writeStatus();
11474
+ writeResp(id, { ok: true, output: 'Blocked peer: ' + peerId.slice(0, 20) + '...' });
11475
+ break;
11476
+ }
11477
+ case 'unblock_peer': {
11478
+ const peerId = args.target_peer || args.peer_id;
11479
+ if (!peerId) { writeResp(id, { ok: false, output: 'peer_id or target_peer is required' }); return; }
11480
+ if (typeof nexus.unblockPeer === 'function') {
11481
+ nexus.unblockPeer(peerId);
11482
+ }
11483
+ const idx = blockedPeers.indexOf(peerId);
11484
+ if (idx >= 0) blockedPeers.splice(idx, 1);
11485
+ writeStatus();
11486
+ writeResp(id, { ok: true, output: 'Unblocked peer: ' + peerId.slice(0, 20) + '...' });
11487
+ break;
11488
+ }
11489
+
11490
+ // \u2500\u2500 v1.5.0: Metering \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11491
+ case 'metering_status': {
11492
+ if (!nexus.metering) {
11493
+ writeResp(id, { ok: true, output: 'Metering not available (nexus version too old).' });
11494
+ return;
11495
+ }
11496
+ const filter = {};
11497
+ if (args.peer_id) filter.peerId = args.peer_id;
11498
+ if (args.capability) filter.service = args.capability;
11499
+
11500
+ // Try per-peer summary first
11501
+ if (args.peer_id && typeof nexus.metering.getSummary === 'function') {
11502
+ const summary = nexus.metering.getSummary(args.peer_id);
11503
+ if (summary) {
11504
+ writeResp(id, { ok: true, output: JSON.stringify(summary, null, 2) });
11505
+ return;
11506
+ }
11507
+ }
11508
+
11509
+ // All summaries
11510
+ if (typeof nexus.metering.getAllSummaries === 'function') {
11511
+ const all = nexus.metering.getAllSummaries();
11512
+ if (all && (all.size > 0 || Object.keys(all).length > 0)) {
11513
+ const entries = all instanceof Map ? Object.fromEntries(all) : all;
11514
+ writeResp(id, { ok: true, output: 'Metering summaries:\\n' + JSON.stringify(entries, null, 2) });
11515
+ return;
11516
+ }
11517
+ }
11518
+
11519
+ // Fall back to raw records
11520
+ if (typeof nexus.metering.getRecords === 'function') {
11521
+ const records = nexus.metering.getRecords(filter);
11522
+ writeResp(id, { ok: true, output: 'Records: ' + records.length + '\\n' + JSON.stringify(records.slice(-10), null, 2) });
11523
+ return;
11524
+ }
11525
+
11526
+ writeResp(id, { ok: true, output: 'No metering data yet.' });
11527
+ break;
11528
+ }
11529
+
11530
+ // \u2500\u2500 v1.5.0: Room Members \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11531
+ case 'room_members': {
11532
+ const roomId = args.room_id;
11533
+ if (!roomId) { writeResp(id, { ok: false, output: 'room_id is required' }); return; }
11534
+ const room = rooms.get(roomId);
11535
+ if (!room) { writeResp(id, { ok: false, output: 'Not in room: ' + roomId }); return; }
11536
+
11537
+ const members = room.members || [];
11538
+ if (members.length === 0) {
11539
+ writeResp(id, { ok: true, output: 'No members tracked in room: ' + roomId });
11540
+ return;
11541
+ }
11542
+ const lines = ['Members in ' + roomId + ' (' + members.length + '):'];
11543
+ for (const m of members) {
11544
+ const name = m.agentName || 'unknown';
11545
+ const type = m.agentType || '?';
11546
+ const caps = m.capabilities?.join(', ') || 'none';
11547
+ const status = m.status || 'active';
11548
+ lines.push(' ' + m.peerId.slice(0, 16) + '... ' + name + ' (' + type + ') [' + status + '] caps: ' + caps);
11549
+ }
11550
+ writeResp(id, { ok: true, output: lines.join('\\n') });
11551
+ break;
11552
+ }
11553
+
11554
+ // \u2500\u2500 Metered Inference Exposure \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
11555
+ case 'expose': {
11556
+ // Query Ollama for available models
11557
+ const ollamaUrl = args.ollama_url || process.env.OLLAMA_URL || 'http://localhost:11434';
11558
+ let models = [];
11559
+ try {
11560
+ const tagsResp = await fetch(ollamaUrl + '/api/tags');
11561
+ if (tagsResp.ok) {
11562
+ const tagsData = await tagsResp.json();
11563
+ models = (tagsData.models || []).map(m => ({
11564
+ name: m.name,
11565
+ size: m.size || 0,
11566
+ parameterSize: m.details?.parameter_size || '',
11567
+ family: m.details?.family || '',
11568
+ quantization: m.details?.quantization_level || '',
11569
+ }));
11570
+ }
11571
+ } catch (e) {
11572
+ writeResp(id, { ok: false, output: 'Cannot reach Ollama at ' + ollamaUrl + ': ' + e.message });
11573
+ return;
11574
+ }
11575
+
11576
+ if (models.length === 0) {
11577
+ writeResp(id, { ok: false, output: 'No models found on Ollama. Pull a model first.' });
11578
+ return;
11579
+ }
11580
+
11581
+ // Fetch market rates from OpenRouter (free, no auth)
11582
+ let marketRates = {};
11583
+ try {
11584
+ const orResp = await fetch('https://openrouter.ai/api/v1/models');
11585
+ if (orResp.ok) {
11586
+ const orData = await orResp.json();
11587
+ for (const m of (orData.data || [])) {
11588
+ if (m.pricing) {
11589
+ marketRates[m.id] = {
11590
+ input: parseFloat(m.pricing.prompt || '0') * 1_000_000,
11591
+ output: parseFloat(m.pricing.completion || '0') * 1_000_000,
11592
+ };
11593
+ }
11594
+ }
11595
+ }
11596
+ } catch { /* offline \u2014 use zero rates */ }
11597
+
11598
+ // Build pricing menu \u2014 match local models to market rates
11599
+ const margin = parseFloat(args.margin || '0.5'); // default 50% of market rate
11600
+ const pricingMenu = [];
11601
+
11602
+ for (const model of models) {
11603
+ // Try to match to OpenRouter model ID
11604
+ const baseName = model.name.replace(/:latest$/, '').toLowerCase();
11605
+ let matched = null;
11606
+ for (const [orId, rates] of Object.entries(marketRates)) {
11607
+ const orBase = orId.toLowerCase();
11608
+ if (orBase.includes(baseName) || baseName.includes(orBase.split('/').pop())) {
11609
+ matched = { orId, ...rates };
11610
+ break;
11611
+ }
11612
+ }
11613
+
11614
+ const entry = {
11615
+ model: model.name,
11616
+ parameterSize: model.parameterSize,
11617
+ family: model.family,
11618
+ quantization: model.quantization,
11619
+ pricing: {
11620
+ input_per_1m_tokens: matched ? +(matched.input * margin).toFixed(4) : 0,
11621
+ output_per_1m_tokens: matched ? +(matched.output * margin).toFixed(4) : 0,
11622
+ currency: 'USD',
11623
+ source: matched ? 'openrouter:' + matched.orId : 'self-hosted:free',
11624
+ margin: margin,
11625
+ },
11626
+ };
11627
+ pricingMenu.push(entry);
11628
+
11629
+ // Register as nexus capability
11630
+ const capName = 'inference:' + model.name.replace(/[^a-zA-Z0-9._-]/g, '_');
11631
+ if (typeof nexus.registerCapability === 'function') {
11632
+ nexus.registerCapability(capName, async (request, stream) => {
11633
+ const logEntry = {
11634
+ ts: Date.now(),
11635
+ from: request.from || 'unknown',
11636
+ capability: capName,
11637
+ model: model.name,
11638
+ requestId: request.requestId,
11639
+ };
11640
+ const logFile = join(invocationsDir, Date.now() + '-' + capName + '.json');
11641
+ try { writeFileSync(logFile, JSON.stringify(logEntry, null, 2)); } catch {}
11642
+
11643
+ // Collect input
11644
+ let prompt = '';
11645
+ stream.onData((msg) => {
11646
+ if (msg.type === 'invoke.chunk') {
11647
+ prompt += (typeof msg.data === 'string' ? msg.data : JSON.stringify(msg.data));
11648
+ }
11649
+ });
11650
+
11651
+ // Accept
11652
+ await stream.write({
11653
+ type: 'invoke.accept', version: 1,
11654
+ requestId: request.requestId, accepted: true,
11655
+ });
11656
+
11657
+ // Forward to Ollama
11658
+ try {
11659
+ const genResp = await fetch(ollamaUrl + '/api/generate', {
11660
+ method: 'POST',
11661
+ headers: { 'Content-Type': 'application/json' },
11662
+ body: JSON.stringify({
11663
+ model: model.name,
11664
+ prompt: prompt || 'Hello',
11665
+ stream: false,
11666
+ }),
11667
+ });
11668
+ const genData = await genResp.json();
11669
+ const output = genData.response || '';
11670
+ const inputTokens = genData.prompt_eval_count || 0;
11671
+ const outputTokens = genData.eval_count || 0;
11672
+
11673
+ // Stream result back
11674
+ await stream.write({
11675
+ type: 'invoke.event', version: 1,
11676
+ requestId: request.requestId, seq: 0,
11677
+ event: 'result',
11678
+ data: JSON.stringify({
11679
+ model: model.name,
11680
+ response: output,
11681
+ usage: { input_tokens: inputTokens, output_tokens: outputTokens },
11682
+ pricing: entry.pricing,
11683
+ }),
11684
+ });
11685
+
11686
+ await stream.write({
11687
+ type: 'invoke.done', version: 1,
11688
+ requestId: request.requestId,
11689
+ usage: {
11690
+ inputBytes: prompt.length,
11691
+ outputBytes: output.length,
11692
+ tokens: inputTokens + outputTokens,
11693
+ },
11694
+ });
11695
+ } catch (e) {
11696
+ await stream.write({
11697
+ type: 'invoke.event', version: 1,
11698
+ requestId: request.requestId, seq: 0,
11699
+ event: 'error', data: 'Inference failed: ' + e.message,
11700
+ });
11701
+ await stream.write({
11702
+ type: 'invoke.done', version: 1,
11703
+ requestId: request.requestId,
11704
+ usage: { inputBytes: 0, outputBytes: 0 },
11705
+ });
11706
+ }
11707
+ stream.close();
11708
+ });
11709
+ }
11710
+ }
11711
+
11712
+ // Write pricing menu to file
11713
+ const pricingFile = join(nexusDir, 'pricing.json');
11714
+ writeFileSync(pricingFile, JSON.stringify({ updated: new Date().toISOString(), models: pricingMenu }, null, 2));
11715
+ writeStatus({ exposedModels: pricingMenu.length });
11716
+
11717
+ const lines = ['Exposed ' + pricingMenu.length + ' model(s) as nexus capabilities:'];
11718
+ for (const p of pricingMenu) {
11719
+ const cost = p.pricing.input_per_1m_tokens === 0
11720
+ ? 'FREE (self-hosted)'
11721
+ : '$' + p.pricing.input_per_1m_tokens + '/$' + p.pricing.output_per_1m_tokens + ' per 1M tokens';
11722
+ lines.push(' inference:' + p.model + ' \u2014 ' + cost);
11723
+ }
11724
+ lines.push('');
11725
+ lines.push('Pricing menu saved to ' + pricingFile);
11726
+ lines.push('Market rates: ' + Object.keys(marketRates).length + ' models from OpenRouter');
11727
+ writeResp(id, { ok: true, output: lines.join('\\n') });
11728
+ break;
11729
+ }
11730
+
11731
+ case 'pricing_menu': {
11732
+ const pricingFile = join(nexusDir, 'pricing.json');
11733
+ if (!existsSync(pricingFile)) {
11734
+ writeResp(id, { ok: false, output: 'No pricing menu. Run expose first.' });
11735
+ return;
11736
+ }
11737
+ try {
11738
+ const menu = JSON.parse(readFileSync(pricingFile, 'utf8'));
11739
+ const lines = ['Inference Pricing Menu (updated: ' + menu.updated + ')'];
11740
+ lines.push('');
11741
+ for (const m of (menu.models || [])) {
11742
+ const cost = m.pricing.input_per_1m_tokens === 0
11743
+ ? 'FREE'
11744
+ : '$' + m.pricing.input_per_1m_tokens + ' in / $' + m.pricing.output_per_1m_tokens + ' out per 1M tokens';
11745
+ lines.push(' ' + m.model + ' (' + m.parameterSize + ', ' + m.quantization + ')');
11746
+ lines.push(' ' + cost + ' [' + m.pricing.source + ']');
11747
+ }
11748
+ writeResp(id, { ok: true, output: lines.join('\\n') });
11749
+ } catch (e) {
11750
+ writeResp(id, { ok: false, output: 'Failed to read pricing menu: ' + e.message });
11751
+ }
11752
+ break;
11753
+ }
11754
+
11375
11755
  case 'ping': {
11376
11756
  writeResp(id, { ok: true, output: 'pong' });
11377
11757
  break;
@@ -11418,6 +11798,41 @@ process.on('unhandledRejection', (reason) => {
11418
11798
  await node.start();
11419
11799
  }
11420
11800
  connected = true;
11801
+
11802
+ // v1.5.0: Setup metering audit hook
11803
+ try {
11804
+ if (nexus.metering && typeof nexus.metering.addHook === 'function') {
11805
+ nexus.metering.addHook(createFileAuditHook(meteringFile));
11806
+ }
11807
+ } catch {}
11808
+
11809
+ // v1.5.0: Client-level events for global message/DM/invoke routing
11810
+ try {
11811
+ if (typeof nexus.on === 'function') {
11812
+ nexus.on('message', ({ roomId, message }) => {
11813
+ // Already handled by per-room listener, but log globally
11814
+ console.log('[msg] ' + roomId + ' from ' + (message?.sender || '?').slice(0, 16));
11815
+ });
11816
+ nexus.on('dm', ({ from, content, format, messageId }) => {
11817
+ // Log DMs to inbox/dm/
11818
+ const dmDir = join(inboxDir, '_dm');
11819
+ mkdirSync(dmDir, { recursive: true });
11820
+ const entry = {
11821
+ sender: from,
11822
+ content: content || '',
11823
+ format: format || 'text/plain',
11824
+ timestamp: Date.now(),
11825
+ id: messageId,
11826
+ };
11827
+ try { writeFileSync(join(dmDir, Date.now() + '.json'), JSON.stringify(entry, null, 2)); } catch {}
11828
+ console.log('[dm] from ' + (from || '?').slice(0, 16));
11829
+ });
11830
+ nexus.on('invoke', ({ from, capability, requestId }) => {
11831
+ console.log('[invoke] ' + capability + ' from ' + (from || '?').slice(0, 16) + ' req=' + requestId);
11832
+ });
11833
+ }
11834
+ } catch {}
11835
+
11421
11836
  writeStatus();
11422
11837
  console.log('Nexus daemon connected as ' + nexus.peerId);
11423
11838
  } catch (err) {
@@ -11445,7 +11860,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11445
11860
  ];
11446
11861
  NexusTool = class {
11447
11862
  name = "nexus";
11448
- description = "Decentralized agent-to-agent communication via open-agents-nexus v1.4.0. Spawns a background Node.js process with real network sockets for libp2p + NATS P2P mesh. Simple flow: connect \u2192 join room \u2192 send messages. Auto-installs/updates nexus if needed. Also supports direct peer invoke (streaming inference), IPFS content storage, direct messages, peer discovery, x402 micropayments, and inference proofs. Onboarding: curl openagents.nexus/llms.txt for full instructions, /.well-known/agent.json for machine-readable manifest.";
11863
+ description = "Decentralized agent-to-agent communication via open-agents-nexus v1.5.0. Spawns a background Node.js process with real network sockets for libp2p + NATS P2P mesh. Simple flow: connect \u2192 join room \u2192 send messages. Auto-installs/updates nexus if needed. EXPOSE: expose action queries Ollama models, fetches OpenRouter market rates, registers metered inference capabilities so peers can invoke your models. pricing_menu shows rates. v1.5.0: register_capability, block_peer/unblock_peer, metering_status, room_members. Also supports direct peer invoke, IPFS storage, DMs, discovery, x402, inference proofs.";
11449
11864
  parameters = {
11450
11865
  type: "object",
11451
11866
  properties: {
@@ -11468,7 +11883,16 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11468
11883
  "retrieve_content",
11469
11884
  "wallet_status",
11470
11885
  "wallet_create",
11471
- "inference_proof"
11886
+ "inference_proof",
11887
+ "register_capability",
11888
+ "unregister_capability",
11889
+ "list_capabilities",
11890
+ "block_peer",
11891
+ "unblock_peer",
11892
+ "metering_status",
11893
+ "room_members",
11894
+ "expose",
11895
+ "pricing_menu"
11472
11896
  ],
11473
11897
  description: "The nexus action to perform"
11474
11898
  },
@@ -11515,6 +11939,14 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11515
11939
  cid: {
11516
11940
  type: "string",
11517
11941
  description: "Content ID for retrieve_content"
11942
+ },
11943
+ ollama_url: {
11944
+ type: "string",
11945
+ description: "Ollama API URL for expose (default: http://localhost:11434)"
11946
+ },
11947
+ margin: {
11948
+ type: "string",
11949
+ description: "Price margin multiplier for expose (0.5 = 50% of market rate, 0 = free). Default: 0.5"
11518
11950
  }
11519
11951
  },
11520
11952
  required: ["action"],
@@ -11588,6 +12020,33 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11588
12020
  case "inference_proof":
11589
12021
  result = await this.doInferenceProof();
11590
12022
  break;
12023
+ case "register_capability":
12024
+ result = await this.sendDaemonCmd("register_capability", args);
12025
+ break;
12026
+ case "unregister_capability":
12027
+ result = await this.sendDaemonCmd("unregister_capability", args);
12028
+ break;
12029
+ case "list_capabilities":
12030
+ result = await this.sendDaemonCmd("list_capabilities", args);
12031
+ break;
12032
+ case "block_peer":
12033
+ result = await this.sendDaemonCmd("block_peer", args);
12034
+ break;
12035
+ case "unblock_peer":
12036
+ result = await this.sendDaemonCmd("unblock_peer", args);
12037
+ break;
12038
+ case "metering_status":
12039
+ result = await this.sendDaemonCmd("metering_status", args);
12040
+ break;
12041
+ case "room_members":
12042
+ result = await this.sendDaemonCmd("room_members", args);
12043
+ break;
12044
+ case "expose":
12045
+ result = await this.sendDaemonCmd("expose", args, 3e4);
12046
+ break;
12047
+ case "pricing_menu":
12048
+ result = await this.sendDaemonCmd("pricing_menu", args);
12049
+ break;
11591
12050
  default:
11592
12051
  return { success: false, output: "", error: `Unknown nexus action: ${action}`, durationMs: Date.now() - start };
11593
12052
  }
@@ -11611,7 +12070,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11611
12070
  return null;
11612
12071
  }
11613
12072
  }
11614
- async sendDaemonCmd(action, args) {
12073
+ async sendDaemonCmd(action, args, timeoutMs = 15e3) {
11615
12074
  const pid = this.getDaemonPid();
11616
12075
  if (!pid)
11617
12076
  throw new Error("Nexus daemon not running. Use action 'connect' first.");
@@ -11622,7 +12081,8 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11622
12081
  await unlink(respFile).catch(() => {
11623
12082
  });
11624
12083
  await writeFile12(cmdFile, JSON.stringify({ id: cmdId, action, args }, null, 2));
11625
- for (let i = 0; i < 30; i++) {
12084
+ const polls = Math.ceil(timeoutMs / 500);
12085
+ for (let i = 0; i < polls; i++) {
11626
12086
  await new Promise((r) => setTimeout(r, 500));
11627
12087
  if (!existsSync21(respFile))
11628
12088
  continue;
@@ -11634,7 +12094,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11634
12094
  } catch {
11635
12095
  }
11636
12096
  }
11637
- return "Daemon did not respond within 15s. It may still be processing.";
12097
+ return `Daemon did not respond within ${Math.round(timeoutMs / 1e3)}s. It may still be processing.`;
11638
12098
  }
11639
12099
  // =========================================================================
11640
12100
  // Actions
@@ -11812,12 +12272,14 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
11812
12272
  try {
11813
12273
  const status = JSON.parse(await readFile13(statusFile, "utf8"));
11814
12274
  const lines = [
11815
- `Nexus Status`,
12275
+ `Nexus Status (v1.5.0)`,
11816
12276
  ` Connected: ${status.connected}`,
11817
12277
  ` Peer ID: ${status.peerId || "n/a"}`,
11818
12278
  ` Agent: ${status.agentName} (${status.agentType})`,
11819
12279
  ` Daemon PID: ${status.pid}`,
11820
- ` Rooms: ${status.rooms?.length || 0} [${(status.rooms || []).join(", ")}]`
12280
+ ` Rooms: ${status.rooms?.length || 0} [${(status.rooms || []).join(", ")}]`,
12281
+ ` Capabilities: ${(status.capabilities || []).length ? (status.capabilities || []).join(", ") : "none"}`,
12282
+ ` Blocked peers: ${(status.blockedPeers || []).length || 0}`
11821
12283
  ];
11822
12284
  const walletPath = join28(this.nexusDir, "wallet.enc");
11823
12285
  if (existsSync21(walletPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.83.0",
3
+ "version": "0.85.0",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -70,6 +70,6 @@
70
70
  },
71
71
  "optionalDependencies": {
72
72
  "moondream": "^0.2.0",
73
- "open-agents-nexus": "^1.4.1"
73
+ "open-agents-nexus": "^1.5.0"
74
74
  }
75
75
  }
@@ -26,7 +26,7 @@ If a tool fails, try a different approach. If you're unsure, explore with your t
26
26
  - web_fetch: Fetch a web page and extract text content (for docs, MDN, w3schools.com, etc.)
27
27
  - memory_read: Read from persistent memory (learned patterns, solutions)
28
28
  - memory_write: Store a fact, pattern, or solution in persistent memory for future tasks
29
- - nexus: P2P agent networking (libp2p + NATS + IPFS). Connect to other agents, join rooms, send messages, discover peers, invoke capabilities, store content, manage wallet. Actions: connect, disconnect, status, join_room, leave_room, send_message, read_messages, discover_peers, list_rooms, send_dm, find_agent, invoke_capability, store_content, retrieve_content, wallet_status, wallet_create, inference_proof.
29
+ - nexus: P2P agent networking (libp2p + NATS + IPFS). Connect to other agents, join rooms, send messages, discover peers, invoke capabilities, store content, manage wallet. Key actions: connect, join_room, send_message, discover_peers, invoke_capability, expose (metered inference), pricing_menu, register_capability, block_peer, metering_status, room_members. Full list: connect, disconnect, status, join_room, leave_room, send_message, read_messages, discover_peers, list_rooms, send_dm, find_agent, invoke_capability, store_content, retrieve_content, wallet_status, wallet_create, inference_proof, register_capability, unregister_capability, list_capabilities, block_peer, unblock_peer, metering_status, room_members, expose, pricing_menu.
30
30
  - task_complete: Signal task completion with a summary
31
31
 
32
32
  ## Parallel Execution & Sub-Agents
@@ -185,7 +185,7 @@ If you notice you're performing the SAME multi-step sequence for the 3rd time or
185
185
  - Test the tool mentally before creating — ensure the steps would work in order
186
186
  - Prefer 'project' scope unless the pattern genuinely applies to all projects
187
187
 
188
- ## Nexus P2P Networking (v1.4.0) — Decentralized Agent Communication
188
+ ## Nexus P2P Networking (v1.5.0) — Decentralized Agent Communication
189
189
 
190
190
  You HAVE the nexus tool. It is one of your registered tools. USE IT when asked about connecting, messaging, or networking with other agents.
191
191
 
@@ -234,11 +234,42 @@ Use invoke_capability for real work (inference, tool calls) — NOT room message
234
234
  nexus(action='wallet_create')
235
235
  nexus(action='inference_proof')
236
236
 
237
+ ### v1.5.0: Serve Capabilities
238
+ nexus(action='register_capability', capability='text-generation') — register handler for incoming invocations
239
+ nexus(action='unregister_capability', capability='text-generation')
240
+ nexus(action='list_capabilities') — list registered capability names
241
+
242
+ ### v1.5.0: Trust & Blocking
243
+ nexus(action='block_peer', target_peer='12D3KooW...') — blocks invoke + DM from peer
244
+ nexus(action='unblock_peer', target_peer='12D3KooW...')
245
+
246
+ ### v1.5.0: Usage Metering
247
+ nexus(action='metering_status') — all peer summaries
248
+ nexus(action='metering_status', peer_id='12D3KooW...') — per-peer summary
249
+ nexus(action='metering_status', capability='chat') — filter by service
250
+
251
+ ### v1.5.0: Room Members
252
+ nexus(action='room_members', room_id='general') — live member list with capabilities
253
+
254
+ ### Metered Inference Exposure
255
+ nexus(action='expose') — expose ALL local Ollama models as nexus capabilities
256
+ nexus(action='expose', margin='0.5') — set pricing at 50% of market rate (default)
257
+ nexus(action='expose', margin='0') — expose for free (self-hosted, no cost)
258
+ nexus(action='expose', margin='1.0') — match market rate
259
+ nexus(action='pricing_menu') — show current pricing menu for exposed models
260
+
261
+ expose queries local Ollama for models, fetches live market rates from OpenRouter
262
+ (https://openrouter.ai/api/v1/models — free, no auth), registers each model as a
263
+ nexus capability (inference:{model_name}), and writes pricing to .oa/nexus/pricing.json.
264
+ Peers can invoke your models via invoke_capability and see metered usage.
265
+
237
266
  SECURITY: Wallet private keys are AES-256-GCM encrypted and NEVER accessible to you.
238
267
  All outbound messages are scanned for key material leaks.
239
268
 
240
269
  When the user asks about expanding capabilities or connecting with other agents, suggest
241
- enabling nexus networking. Use inference_proof to benchmark and advertise capabilities.
270
+ enabling nexus networking. Use expose to share your models with the network, pricing_menu
271
+ to check rates, register_capability to serve custom invocations, and room_members to
272
+ discover who's online.
242
273
 
243
274
  ## Temporal Agency — Scheduling, Reminders & Long-Horizon Tasks
244
275
 
@@ -17,7 +17,7 @@ NEVER say "I can't do that". ALWAYS attempt the task using your tools. If a tool
17
17
  - web_search: Search the web
18
18
  - web_fetch: Fetch a web page's text
19
19
  - memory_read / memory_write: Persistent memory across sessions
20
- - nexus: P2P agent networking (connect, join_room, send_message, discover_peers, invoke_capability, wallet, etc.)
20
+ - nexus: P2P agent networking (connect, join_room, send_message, discover_peers, invoke_capability, register_capability, block_peer, metering_status, room_members, wallet, etc.)
21
21
  - task_complete: Signal completion with a summary
22
22
  - background_run / task_status / task_output / task_stop: Background tasks
23
23
  - batch_edit: Multiple edits across files in one call