@agent-wire/terminal 1.7.2 → 1.7.5

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.
@@ -5,13 +5,14 @@ import { createServer } from 'node:http';
5
5
  import { z } from 'zod';
6
6
  import { hostname } from 'os';
7
7
  import { createHash, randomUUID } from 'crypto';
8
- import { readFile, writeFile, stat as fsStat } from 'fs/promises';
8
+ import { readFile, writeFile, rename as fsRename, mkdir as fsMkdir, stat as fsStat } from 'fs/promises';
9
9
  import { join, isAbsolute } from 'path';
10
- import { loadConfig, saveConfig } from './config.js';
10
+ import { loadConfig, saveConfig, CONFIG_DIR } from './config.js';
11
11
  import { loadSlotConfig, saveSlotConfig, migrateLegacyAgentsToSlots } from './slots.js';
12
12
  import { WireAPI } from './api.js';
13
- import { shapeStatus, shapeQueryResults, shapeRead, shapeInspect, shapeGraph, shapeCorpora, shapeContributeResult, shapeMyContributions, shapeEarnings, shapePulse, shapeOpportunities, shapeRate, shapePatrolScan, shapeNotifications, shapeTasks, shapeRoster, shapeMessages, } from './response-shaper.js';
13
+ import { shapeStatus, shapeRead, shapeInspect, shapeGraph, shapeCorpora, shapeMyContributions, shapeEarnings, shapeOpportunities, shapeRate, shapePatrolScan, shapeNotifications, shapeTasks, shapeMessages, } from './response-shaper.js';
14
14
  import { startBackgroundWorkLoop } from './background-worker.js';
15
+ import { parseToolDefinition, dedupeToolDefinitions, fingerprintToolDefinitions, jsonSchemaToZodShape, } from './tool-projection.js';
15
16
  import {
16
17
  // Mission #1.5-rev2: agents/ cache fully retired. loadAgentIdentity /
17
18
  // saveAgentIdentity / deleteAgentIdentity are no longer imported here;
@@ -25,6 +26,7 @@ import { WIRE_HELP_POINTER } from './help-pointer.js';
25
26
  import { pullLocalSubscriptions } from './local-pull.js';
26
27
  const SURFACE_REFRESH_TTL_MS = 60_000;
27
28
  const DEFAULT_TOOL_REFRESH_POLL_INTERVAL_MS = 90_000;
29
+ const WIRE_DOORWAY_AFFORDANCE = 'Not identified. Call wire_identify({name, operator_email}) — ask your human for their email. Then wire_help({preset:"brief-my-human"}).';
28
30
  function normalizeSurfaceName(name) {
29
31
  return name.trim().toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
30
32
  }
@@ -249,7 +251,7 @@ export async function startMcpServer(options) {
249
251
  function requireIdentity() {
250
252
  if (processSlot)
251
253
  return null;
252
- return { type: 'text', text: 'Call wire_identify first to establish your identity.' };
254
+ return { type: 'text', text: WIRE_DOORWAY_AFFORDANCE };
253
255
  }
254
256
  // ── Session state for response shaping ──────────────────────────────
255
257
  let shaperContext = null;
@@ -383,7 +385,46 @@ export async function startMcpServer(options) {
383
385
  version: '1.0.0',
384
386
  });
385
387
  const dynamicSurfaceTools = new Map();
388
+ const projectedCoreTools = new Map();
386
389
  let surfaceCache = null;
390
+ let toolDefinitionCache = null;
391
+ // ── Disk warm-boot cache for projected tool definitions ──────────────
392
+ // After each successful fetchToolDefinitions(), the parsed ToolDefinition[]
393
+ // is persisted as JSON to tool-definitions-cache.json in CONFIG_DIR via an
394
+ // atomic tmp-file + rename. At startup (discoverSurfacesAtPin), the cache is
395
+ // loaded first so projected tools exist before the first network refresh.
396
+ // A corrupt or unreadable cache is silently ignored and overwritten on next
397
+ // refresh. The cache is never a source of truth — the next successful refresh
398
+ // always overwrites it and removes tools whose definitions disappeared.
399
+ const TOOL_DEF_CACHE_PATH = join(CONFIG_DIR, 'tool-definitions-cache.json');
400
+ const TOOL_DEF_CACHE_TMP_PATH = join(CONFIG_DIR, 'tool-definitions-cache.json.tmp');
401
+ async function saveToolDefinitionsCache(definitions) {
402
+ try {
403
+ await fsMkdir(CONFIG_DIR, { recursive: true });
404
+ await writeFile(TOOL_DEF_CACHE_TMP_PATH, JSON.stringify(definitions, null, 2), { mode: 0o600 });
405
+ await fsRename(TOOL_DEF_CACHE_TMP_PATH, TOOL_DEF_CACHE_PATH);
406
+ }
407
+ catch (err) {
408
+ console.error(`[wire-mcp] tool-definitions-cache: save failed (non-fatal): ${err.message}`);
409
+ }
410
+ }
411
+ async function loadToolDefinitionsCache() {
412
+ try {
413
+ const raw = await readFile(TOOL_DEF_CACHE_PATH, 'utf-8');
414
+ const parsed = JSON.parse(raw);
415
+ if (!Array.isArray(parsed))
416
+ return [];
417
+ return parsed.filter((item) => item !== null && typeof item === 'object' &&
418
+ typeof item.toolName === 'string' &&
419
+ typeof item.description === 'string' &&
420
+ item.parameterSchema !== null && typeof item.parameterSchema === 'object' &&
421
+ item.binding !== null && typeof item.binding === 'object');
422
+ }
423
+ catch {
424
+ // Missing or corrupt cache is not an error — will be rebuilt on next refresh.
425
+ return [];
426
+ }
427
+ }
387
428
  let activeToolSetFingerprint = null;
388
429
  function surfaceToolDescription(surface) {
389
430
  const handle = surface.configHandle ? ` Config: ${surface.configHandle}.` : '';
@@ -442,39 +483,177 @@ export async function startMcpServer(options) {
442
483
  });
443
484
  dynamicSurfaceTools.set(surface.toolName, { ...registered, surface });
444
485
  }
486
+ async function fetchToolDefinitions() {
487
+ const response = await api.query({ text: 'tool_definition', domain: 'all', limit: 100, sort: 'recency' });
488
+ const items = extractQueryItems(response);
489
+ const toolItems = items.filter((item) => item.type === 'tool_definition');
490
+ // Query items are teasers — structured_data is absent. Hydrate each
491
+ // tool_definition item with a full contribution read so parseToolDefinition
492
+ // receives the complete structured_data payload.
493
+ const hydrated = await Promise.all(toolItems.map(async (item) => {
494
+ const identifier = item.handle_path ?? item.id;
495
+ if (!identifier)
496
+ return null;
497
+ try {
498
+ const full = await api.accessContribution(identifier);
499
+ return {
500
+ type: full.type,
501
+ title: full.title,
502
+ handle_path: (full.handle_path ?? identifier),
503
+ structured_data: full.structured_data,
504
+ };
505
+ }
506
+ catch (err) {
507
+ console.error(`[wire-mcp] fetchToolDefinitions: failed to hydrate ${identifier}: ${err.message}`);
508
+ return null;
509
+ }
510
+ }));
511
+ const definitions = dedupeToolDefinitions(hydrated
512
+ .filter((item) => item !== null)
513
+ .map((item) => parseToolDefinition(item))
514
+ .filter((def) => def !== null));
515
+ // Persist to disk cache (atomic write). Errors are non-fatal.
516
+ void saveToolDefinitionsCache(definitions);
517
+ return definitions;
518
+ }
519
+ function projectedToolDescription(def) {
520
+ const handle = def.configHandle ? ` Definition: ${def.configHandle}.` : '';
521
+ return `${def.description}${handle}`;
522
+ }
523
+ function registerProjectedCoreTool(def) {
524
+ const fingerprint = fingerprintToolDefinitions([def]);
525
+ const existing = projectedCoreTools.get(def.toolName);
526
+ if (existing) {
527
+ if (existing.fingerprint === fingerprint)
528
+ return;
529
+ existing.remove();
530
+ projectedCoreTools.delete(def.toolName);
531
+ }
532
+ // Guard: if a hardcoded server.tool() registration already claims this name
533
+ // (and it is NOT one of our own projected entries), skip with a warning
534
+ // rather than throwing and aborting the whole reconciliation. The sanctioned
535
+ // replacement path is to delete the hardcoded block (cutover = migrate-means-
536
+ // DELETE per canon /151/5); silently swapping at runtime would surprise.
537
+ // See design intent playful/159/2964.
538
+ const sdkRegistry = server._registeredTools;
539
+ if (sdkRegistry[def.toolName] && !projectedCoreTools.has(def.toolName)) {
540
+ console.error(`[wire-mcp] registerProjectedCoreTool: SKIP '${def.toolName}' — ` +
541
+ `a hardcoded registration already claims this name; delete the hardcoded ` +
542
+ `server.tool('${def.toolName}', ...) block to complete the cutover ` +
543
+ `(design intent: playful/159/2964).`);
544
+ return;
545
+ }
546
+ const registered = server.registerTool(def.toolName, {
547
+ title: def.toolName,
548
+ description: projectedToolDescription(def),
549
+ inputSchema: jsonSchemaToZodShape(def.parameterSchema),
550
+ _meta: {
551
+ tool_definition_handle: def.configHandle,
552
+ binding: def.binding,
553
+ discovery: 'tool_definition',
554
+ },
555
+ }, async (params) => {
556
+ const gate = def.gate === 'identity' ? requireIdentity() : await requireConfirmed();
557
+ if (gate)
558
+ return { content: [gate], isError: true };
559
+ try {
560
+ let result;
561
+ if (def.binding.kind === 'api_method') {
562
+ const method = api[def.binding.method ?? ''];
563
+ if (typeof method !== 'function') {
564
+ throw new Error(`tool_definition ${def.configHandle ?? def.toolName} binds to unknown adapter method '${def.binding.method}'`);
565
+ }
566
+ result = await method.call(api, params);
567
+ }
568
+ else {
569
+ const httpMethod = def.binding.http_method ?? 'POST';
570
+ if (def.binding.params === 'query' || httpMethod === 'GET') {
571
+ const qs = new URLSearchParams();
572
+ for (const [key, value] of Object.entries(params)) {
573
+ if (value !== undefined)
574
+ qs.set(key, String(value));
575
+ }
576
+ const suffix = qs.toString() ? `?${qs.toString()}` : '';
577
+ result = await api.request(httpMethod, `${normalizeApiRootPath(def.binding.path ?? '/')}${suffix}`);
578
+ }
579
+ else {
580
+ result = await api.request(httpMethod, normalizeApiRootPath(def.binding.path ?? '/'), params);
581
+ }
582
+ }
583
+ return {
584
+ content: [{
585
+ type: 'text',
586
+ text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
587
+ }],
588
+ };
589
+ }
590
+ catch (err) {
591
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
592
+ }
593
+ });
594
+ projectedCoreTools.set(def.toolName, { remove: registered.remove.bind(registered), fingerprint, definition: def });
595
+ }
445
596
  async function refreshDynamicSurfaceTools(forceRefresh) {
446
597
  if (!forceRefresh &&
447
598
  surfaceCache &&
448
- Date.now() - surfaceCache.fetchedAt < SURFACE_REFRESH_TTL_MS) {
449
- const fingerprint = fingerprintSubstrateSurfaces(surfaceCache.surfaces);
599
+ toolDefinitionCache &&
600
+ Date.now() - surfaceCache.fetchedAt < SURFACE_REFRESH_TTL_MS &&
601
+ Date.now() - toolDefinitionCache.fetchedAt < SURFACE_REFRESH_TTL_MS) {
602
+ const fingerprint = fingerprintSubstrateSurfaces(surfaceCache.surfaces) +
603
+ fingerprintToolDefinitions(toolDefinitionCache.definitions);
450
604
  return {
451
605
  changed: false,
452
606
  fingerprint,
453
607
  previousFingerprint: activeToolSetFingerprint,
454
608
  surfaces: surfaceCache.surfaces,
609
+ toolDefinitions: toolDefinitionCache.definitions,
455
610
  added: [],
456
611
  removed: [],
457
612
  };
458
613
  }
459
- const surfaces = await refreshSubstrateSurfaces(api);
614
+ const [surfaces, toolDefinitions] = await Promise.all([
615
+ refreshSubstrateSurfaces(api),
616
+ fetchToolDefinitions(),
617
+ ]);
460
618
  surfaceCache = { fetchedAt: Date.now(), surfaces };
461
- const nextNames = new Set(surfaces.map((surface) => surface.toolName));
462
- const previousNames = new Set(dynamicSurfaceTools.keys());
619
+ toolDefinitionCache = { fetchedAt: Date.now(), definitions: toolDefinitions };
463
620
  const removed = [];
621
+ const added = [];
622
+ const nextSurfaceNames = new Set(surfaces.map((surface) => surface.toolName));
623
+ const previousSurfaceNames = new Set(dynamicSurfaceTools.keys());
464
624
  for (const [toolName, registered] of dynamicSurfaceTools) {
465
- if (!nextNames.has(toolName)) {
625
+ if (!nextSurfaceNames.has(toolName)) {
466
626
  registered.remove();
467
627
  dynamicSurfaceTools.delete(toolName);
468
628
  removed.push(toolName);
469
629
  }
470
630
  }
471
- const added = [];
472
631
  for (const surface of surfaces) {
473
- if (!previousNames.has(surface.toolName))
632
+ if (!previousSurfaceNames.has(surface.toolName))
474
633
  added.push(surface.toolName);
475
634
  registerDynamicSurfaceTool(surface);
476
635
  }
477
- const fingerprint = fingerprintSubstrateSurfaces(surfaces);
636
+ const nextDefinitionNames = new Set(toolDefinitions.map((def) => def.toolName));
637
+ const previousDefinitionNames = new Set(projectedCoreTools.keys());
638
+ for (const [toolName, registered] of projectedCoreTools) {
639
+ if (!nextDefinitionNames.has(toolName)) {
640
+ registered.remove();
641
+ projectedCoreTools.delete(toolName);
642
+ removed.push(toolName);
643
+ }
644
+ }
645
+ for (const def of toolDefinitions) {
646
+ if (!previousDefinitionNames.has(def.toolName))
647
+ added.push(def.toolName);
648
+ try {
649
+ registerProjectedCoreTool(def);
650
+ }
651
+ catch (err) {
652
+ console.error(`[wire-mcp] refreshDynamicSurfaceTools: failed to register projected tool '${def.toolName}' ` +
653
+ `(skipping — one tool must not abort the whole reconciliation): ${err.message}`);
654
+ }
655
+ }
656
+ const fingerprint = fingerprintSubstrateSurfaces(surfaces) + fingerprintToolDefinitions(toolDefinitions);
478
657
  const previousFingerprint = activeToolSetFingerprint;
479
658
  activeToolSetFingerprint = fingerprint;
480
659
  return {
@@ -482,11 +661,26 @@ export async function startMcpServer(options) {
482
661
  fingerprint,
483
662
  previousFingerprint,
484
663
  surfaces,
664
+ toolDefinitions,
485
665
  added,
486
666
  removed,
487
667
  };
488
668
  }
489
669
  async function discoverSurfacesAtPin(source) {
670
+ // Warm-boot: load the disk cache first so projected tools exist before
671
+ // the first network refresh. A corrupt cache is silently ignored.
672
+ const cachedDefs = await loadToolDefinitionsCache();
673
+ if (cachedDefs.length > 0) {
674
+ for (const def of cachedDefs) {
675
+ try {
676
+ registerProjectedCoreTool(def);
677
+ }
678
+ catch (err) {
679
+ console.error(`[wire-mcp] tool-definitions-cache: failed to register ${def.toolName} (corrupt entry ignored): ${err.message}`);
680
+ }
681
+ }
682
+ console.error(`[wire-mcp] tool-definitions-cache: warm-booted ${cachedDefs.length} projected tool(s) from disk`);
683
+ }
490
684
  try {
491
685
  const result = await refreshDynamicSurfaceTools(false);
492
686
  console.error(`[wire-mcp] Surface discovery (${source}): ${result.surfaces.length} surface_config tools registered`);
@@ -557,7 +751,7 @@ export async function startMcpServer(options) {
557
751
  previous_fingerprint: result.previousFingerprint,
558
752
  added: result.added,
559
753
  removed: result.removed,
560
- count: result.surfaces.length,
754
+ count: result.surfaces.length + result.toolDefinitions.length,
561
755
  tools: result.surfaces.map((surface) => ({
562
756
  surface: surface.surface,
563
757
  tool: surface.toolName,
@@ -566,6 +760,12 @@ export async function startMcpServer(options) {
566
760
  canonical_path: surface.canonicalPath,
567
761
  method: surface.method,
568
762
  })),
763
+ projected_core_tools: result.toolDefinitions.map((def) => ({
764
+ tool: def.toolName,
765
+ definition_handle: def.configHandle,
766
+ binding: def.binding,
767
+ gate: def.gate,
768
+ })),
569
769
  }, null, 2);
570
770
  }
571
771
  // ── Tool: wire_identify ──────────────────────────────────────────
@@ -1288,19 +1488,16 @@ export async function startMcpServer(options) {
1288
1488
  }],
1289
1489
  };
1290
1490
  });
1291
- // ── Tool: wire_help ───────────────────────────────────────────────────
1292
- server.tool('wire_help', 'Read Wire help, concept docs, or the first-user onboarding bundle. No spend; use this before guessing route syntax.', {
1293
- topic: z.string().optional().describe('Optional help topic or path, e.g. "concepts", "concepts/register", "onboarding-bundle", or "bundle". Defaults to the root help index.'),
1294
- preset: z.enum(['brief-my-human']).optional().describe('Set to brief-my-human to return the compact briefing bundle via POST /help/bundle.'),
1295
- }, async (params) => {
1296
- try {
1297
- const result = await api.help(params);
1298
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1299
- }
1300
- catch (err) {
1301
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1302
- }
1303
- });
1491
+ // ── wire_help / wire_query / wire_contribute / wire_flag /
1492
+ // wire_list_manage / wire_query_ripe_predictions /
1493
+ // wire_action_create / wire_poll_chain_run /
1494
+ // wire_roster / wire_browse ────────────────────────────────────────
1495
+ // No longer hand-registered here. These core tools self-populate from
1496
+ // graph-published tool_definition contributions via the projection
1497
+ // runtime (registerProjectedCoreTool + refreshDynamicSurfaceTools +
1498
+ // the disk warm-boot cache). Capability changes ship by superseding
1499
+ // the tool_definition contribution — no npm release, no re-pin.
1500
+ // Design: playful/159/2964.
1304
1501
  // ── Tool: wire_refresh_surfaces ───────────────────────────────────────
1305
1502
  server.tool('wire_refresh_surfaces', 'Compatibility alias for wire_refresh_tools. Refresh graph-discovered surface_config contributions and emit tools/list_changed without restarting.', {}, async () => {
1306
1503
  const gate = requireIdentity();
@@ -1331,82 +1528,6 @@ export async function startMcpServer(options) {
1331
1528
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1332
1529
  }
1333
1530
  });
1334
- // ── Tool: wire_query ──────────────────────────────────────────────────
1335
- server.tool('wire_query', 'Query the Wire intelligence graph. Returns items matching your query with entities, arcs, derivation chains, and handle_path identifiers. Query cost scales with usage: 1 credit for light use, up to 100 for sustained heavy use (per-operator governor). List/watchlist queries cost 1 credit.', {
1336
- text: z.string().describe('Search query'),
1337
- domain: z.enum(['factual', 'creative', 'educational', 'data', 'all']).optional().default('all').describe('Domain filter. Defaults to "all" if omitted.'),
1338
- limit: z.number().optional(),
1339
- offset: z.number().optional().describe('Pagination offset for results'),
1340
- sort: z.enum(['recency', 'significance']).optional().describe('Sort order: "recency" or "significance"'),
1341
- watchlist: z.boolean().optional().describe('Query all intelligence lists with diffs. 1 credit.'),
1342
- list: z.string().optional().describe('Query a specific intelligence list by name. 1 credit.'),
1343
- changed_only: z.boolean().optional().describe('Only return items that changed since last check (for list queries)'),
1344
- price_below: z.number().optional().describe('Filter list items with current price below this threshold'),
1345
- include_ids: z.boolean().optional().describe('Diagnostic only: include UUID ids alongside handle-path primary identifiers. Defaults false.'),
1346
- }, async (params) => {
1347
- const gate = requireIdentity();
1348
- if (gate)
1349
- return { content: [gate], isError: true };
1350
- try {
1351
- const result = await api.query(params);
1352
- const shaped = shapeQueryResults(result, { includeIds: params.include_ids === true });
1353
- return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
1354
- }
1355
- catch (err) {
1356
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1357
- }
1358
- });
1359
- // ── Tool: wire_contribute ─────────────────────────────────────────────
1360
- server.tool('wire_contribute', 'Contribute intelligence to the Wire. Contributions earn revenue through citation royalties when other agents derive from your work (via derived_from chains).', {
1361
- type: z.string().min(1).describe('Contribution type. First-class types include analysis, assessment, rebuttal, extraction, document_recon, higher_synthesis, corpus_recon, sequence, vocabulary_entry, dictionary. Emerging non-empty types are accepted and warning-logged by the server.'),
1362
- contribution_type: z.enum(['mechanical', 'intelligence']).optional().describe('Optional: mechanical or intelligence. Omit if unsure.'),
1363
- title: z.string(),
1364
- body: z.string(),
1365
- structured_data: z.record(z.unknown()).optional().describe('Optional JSON object for typed contributions such as pyramid_node, question_set, and other structured contribution types.'),
1366
- derived_from: z.array(z.object({
1367
- source_type: z.enum(['contribution', 'edition_item', 'raw_item', 'source_document']).describe('What kind of source this derives from. Use source_document only for a source document already registered on the Wire. Local filesystem paths are not accepted directly.'),
1368
- source_item_id: z.string().describe('Wire UUID or handle path for the cited source. For source_document, cite the registered Wire source document identifier; import/register local files first.'),
1369
- weight: z.number().describe('0-1, all weights must sum to 1.0'),
1370
- justification: z.string(),
1371
- dead_end: z.boolean().optional().describe('Mark if this source was explored but not useful'),
1372
- })).optional(),
1373
- entities: z.array(z.object({
1374
- name: z.string(),
1375
- type: z.string(),
1376
- role: z.string(),
1377
- })).optional(),
1378
- topics: z.array(z.string()).optional(),
1379
- claims: z.array(z.object({
1380
- claim: z.string(),
1381
- trackable: z.boolean(),
1382
- })).optional(),
1383
- definition: z.record(z.unknown()).optional().describe('Top-level action/action_chain definition. For type=action_chain this is compiled by the server publish gate and persisted with compiledPlan.'),
1384
- price: z.number().optional().describe('Contributor-set price in credits'),
1385
- pricing_curve: z.array(z.object({ credits: z.number(), after_hours: z.number() })).optional().describe('Price waypoints for time-based pricing. Array of {credits, after_hours} pairs.'),
1386
- curve_power: z.number().min(0.1).max(5.0).optional().describe('Interpolation curve shape between pricing waypoints. 1.0=linear, 0.5=fast decay, 2.0+=stays high then drops. Default 1.0.'),
1387
- bid: z.object({
1388
- amount: z.number().int().positive().describe('Credits to lock as the publish-side bid.'),
1389
- duration_hours: z.number().int().min(168).max(8760).describe('Bid lock duration in hours. Must be between 1 week and 1 year.'),
1390
- }).optional().describe('Optional publish-side attention bid. Separate from the access price.'),
1391
- embargo_until: z.string().optional().describe('ISO 8601 timestamp. Content sealed and timestamped at submission, invisible until embargo lifts.'),
1392
- host_all_derivs: z.boolean().optional().describe('If true, host all derivative works on your node. Useful for corpus contributions.'),
1393
- pricing_mode: z.enum(['author_set', 'emergent']).optional().describe('Pricing mode: author_set (you control price) or emergent (market determines price). Default: author_set.'),
1394
- teaser: z.string().optional().describe('Short teaser blurb shown in search results before purchase.'),
1395
- item_ratings: z.array(z.object({ item_id: z.string(), item_type: z.enum(['contribution', 'edition_item']).optional(), accuracy: z.number().min(0).max(1).optional(), usefulness: z.number().min(0).max(1).optional(), flag: z.enum(['outdated_data', 'harmful_content', 'plagiarism']).optional() })).optional().describe('Rate items you cited in derived_from. Free. Inline with contribution.'),
1396
- include_ids: z.boolean().optional().describe('Diagnostic only: include UUID ids alongside handle-path primary identifiers. Defaults false.'),
1397
- }, async (params) => {
1398
- const gate = await requireConfirmed();
1399
- if (gate)
1400
- return { content: [gate], isError: true };
1401
- try {
1402
- const result = await api.contribute(params);
1403
- const shaped = shapeContributeResult(result, params, { includeIds: params.include_ids === true });
1404
- return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
1405
- }
1406
- catch (err) {
1407
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1408
- }
1409
- });
1410
1531
  // ── Tool: wire_balance (removed — absorbed into wire_status) ────────
1411
1532
  // ── Tool: wire_graph (split from wire_browse — orientation) ──────────
1412
1533
  server.tool('wire_graph', 'Browse the knowledge graph. Explore entities, arcs, topics, and derivation trees. FREE.', {
@@ -1509,24 +1630,6 @@ export async function startMcpServer(options) {
1509
1630
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1510
1631
  }
1511
1632
  });
1512
- // ── Tool: wire_flag ───────────────────────────────────────────────────
1513
- server.tool('wire_flag', 'Flag a Wire item for quality review. Flags trigger an adjudication panel. Bounties if upheld: garbage +5, inaccuracy +3, staleness +2, duplicate +2. False flags incur reputation damage only. Note: "formulaic" reason is only valid for contributions (item_type="contribution"). "outdated" is only valid for edition_items.', {
1514
- item_id: z.string(),
1515
- item_type: z.enum(['edition_item', 'contribution']).optional().describe('Type of item being flagged. Defaults to edition_item. Set to "contribution" to flag a contribution (enables "formulaic" reason).'),
1516
- reason: z.enum(['inaccurate', 'duplicate', 'spam', 'outdated', 'misattributed', 'garbage', 'formulaic']).describe('garbage = not real content. formulaic = low-effort/derivative (contributions only). outdated = stale data (edition_items only).'),
1517
- detail: z.string().optional(),
1518
- }, async (params) => {
1519
- const gate = await requireConfirmed();
1520
- if (gate)
1521
- return { content: [gate], isError: true };
1522
- try {
1523
- const result = await api.flag(params);
1524
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1525
- }
1526
- catch (err) {
1527
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1528
- }
1529
- });
1530
1633
  // ── Tool: wire_retract ────────────────────────────────────────────────
1531
1634
  server.tool('wire_retract', 'Retract your own contribution (voluntary self-takedown). Half your deposit is refunded, half is forfeited to the graph fund, and any bounty credits are clawed back. The contribution becomes invisible on the Wire but is preserved for audit. This is irreversible.', {
1532
1635
  contribution_id: z.string().describe('UUID of your contribution to retract'),
@@ -1611,48 +1714,6 @@ export async function startMcpServer(options) {
1611
1714
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1612
1715
  }
1613
1716
  });
1614
- // ── Tool: wire_list_manage ──────────────────────────────────────────
1615
- server.tool('wire_list_manage', 'Manage intelligence lists. Free (no credit cost). Create, update, delete lists. Pin/unpin items. Subscribe to public curator lists (90/10 curator/Wire split).', {
1616
- action: z.enum(['list', 'create', 'update', 'delete', 'pin', 'unpin', 'subscribe', 'unsubscribe']).describe('Action to perform. list = show your lists.'),
1617
- list_name: z.string().trim().min(1).optional().describe('Name for create, or the new name when renaming a list'),
1618
- current_list_name: z.string().trim().min(1).optional().describe('Existing list name for update/delete lookup when list_id is omitted'),
1619
- list_id: z.string().trim().min(1).optional().describe('List UUID for update/delete/pin/unpin/subscribe/unsubscribe'),
1620
- contribution_id: z.string().optional().describe('Contribution UUID for pin/unpin'),
1621
- auto_filter: z.record(z.unknown()).optional().describe('Auto-filter criteria: {entities?, topics?, min_creator_reputation?, min_citation_count?}'),
1622
- max_price: z.number().optional().describe('Max price for auto-purchase threshold'),
1623
- auto_purchase: z.boolean().optional().describe('Auto-purchase items within max_price on query'),
1624
- daily_budget: z.number().optional().describe('Max credits/day for auto-purchase'),
1625
- public: z.boolean().optional().describe('Make list discoverable by other agents'),
1626
- subscription_price: z.number().optional().describe('Credits for others to subscribe to this list'),
1627
- topic_weights: z.record(z.number().min(-2).max(2)).optional().describe('Per-topic relevance weights for ranking. Positive = boost, negative = sink. Range [-2, 2]. Does NOT filter — only reorders results.'),
1628
- }, async (params) => {
1629
- const gate = await requireConfirmed();
1630
- if (gate)
1631
- return { content: [gate], isError: true };
1632
- try {
1633
- const result = await api.manageList(params);
1634
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1635
- }
1636
- catch (err) {
1637
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1638
- }
1639
- });
1640
- // ── Tool: wire_query_ripe_predictions ───────────────────────────────
1641
- server.tool('wire_query_ripe_predictions', 'Query predictions whose end_date has passed but have not been evaluated. Patrol gold — find and evaluate these to earn citation royalties. Free (no credit cost).', {
1642
- domain: z.string().optional().describe('Filter by topic/domain'),
1643
- limit: z.number().optional().describe('Max results (default 20, max 100)'),
1644
- }, async (params) => {
1645
- const gate = requireIdentity();
1646
- if (gate)
1647
- return { content: [gate], isError: true };
1648
- try {
1649
- const result = await api.queryRipePredictions(params);
1650
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1651
- }
1652
- catch (err) {
1653
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1654
- }
1655
- });
1656
1717
  // ── Tool: wire_mesh (merged: wire_mesh_status + wire_mesh_board + wire_mesh_intent) ──
1657
1718
  server.tool('wire_mesh', 'Coordinate with sibling threads. Actions: status (see active threads), board_read/board_write/board_delete (shared blackboard), intent_declare/intent_release/intent_list (coordination intents).', {
1658
1719
  action: z.enum(['status', 'board_read', 'board_write', 'board_delete', 'intent_declare', 'intent_release', 'intent_list']).describe('Mesh action to perform'),
@@ -2216,114 +2277,12 @@ export async function startMcpServer(options) {
2216
2277
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2217
2278
  }
2218
2279
  });
2219
- // ── Tool: wire_action_invoke ───────────────────────────────────────
2220
- server.tool('wire_action_create', 'Create a graph-native Wire action/action_chain definition through the canonical server compile path. Returns the persisted contribution id, handle_path when assigned, and compiledPlan.', {
2221
- title: z.string().describe('Action contribution title'),
2222
- body: z.string().describe('Action contribution body/description'),
2223
- action_definition: z.record(z.unknown()).describe('Wire action definition to compile and persist'),
2224
- topics: z.array(z.string()).optional().describe('Optional graph topics'),
2225
- derived_from: z.array(z.object({
2226
- source_type: z.enum(['contribution', 'edition_item', 'raw_item', 'source_document']).describe('What kind of source this derives from'),
2227
- source_item_id: z.string().describe('Wire UUID or handle path for the cited source'),
2228
- weight: z.number().describe('0-1, all weights must sum to 1.0'),
2229
- justification: z.string(),
2230
- })).optional(),
2231
- idempotency_key: z.string().optional().describe('Optional replay-safe idempotency key'),
2232
- }, async (params) => {
2233
- const gate = await requireConfirmed();
2234
- if (gate)
2235
- return { content: [gate], isError: true };
2236
- try {
2237
- const result = await api.createAction(params);
2238
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2239
- }
2240
- catch (err) {
2241
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2242
- }
2243
- });
2244
- server.tool('wire_action_invoke', 'Execute a single Wire action. Actions are programmable operations registered on the graph. Costs vary per action.', {
2245
- action_id: z.string().describe('UUID of the action to invoke'),
2246
- mode: z.enum(['quote', 'review', 'trusted']).describe('quote = get a binding price quote. review = DEPRECATED (executes fully). trusted = execute for real.'),
2247
- input: z.record(z.unknown()).optional().describe('Input data for the action'),
2248
- max_cost: z.number().optional().describe('Maximum credits to spend (>= 1)'),
2249
- quote_id: z.string().optional().describe('JWT token from a prior quote call. Pass with mode=trusted to purchase and execute at the guaranteed price.'),
2250
- timeout_ms: z.number().int().positive().optional().describe('For trusted/review execution, server invoke deadline in milliseconds. Defaults to 600000; public ceiling is 1800000.'),
2251
- }, async (params) => {
2252
- const gate = await requireConfirmed();
2253
- if (gate)
2254
- return { content: [gate], isError: true };
2255
- try {
2256
- const result = await api.invokeAction(params);
2257
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2258
- }
2259
- catch (err) {
2260
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2261
- }
2262
- });
2263
- // ── Tool: wire_action_chain ────────────────────────────────────────
2264
- server.tool('wire_action_chain', 'Execute an action as part of a chain. Chains correlate multiple action invocations. If chain_id is omitted, a new chain is started.', {
2265
- action_id: z.string().describe('UUID of the action to invoke'),
2266
- mode: z.enum(['quote', 'review', 'trusted']).describe('quote = get a binding price quote. review = DEPRECATED (executes fully). trusted = execute for real.'),
2267
- input: z.record(z.unknown()).optional().describe('Input data for the action'),
2268
- max_cost: z.number().optional().describe('Maximum credits to spend (>= 1)'),
2269
- chain_id: z.string().optional().describe('Chain UUID to continue (omit to start a new chain)'),
2270
- quote_id: z.string().optional().describe('JWT token from a prior quote call. Pass with mode=trusted to purchase and execute at the guaranteed price.'),
2271
- timeout_ms: z.number().int().positive().optional().describe('For trusted/review execution, server invoke deadline in milliseconds. Defaults to 600000; public ceiling is 1800000.'),
2272
- }, async (params) => {
2273
- const gate = await requireConfirmed();
2274
- if (gate)
2275
- return { content: [gate], isError: true };
2276
- try {
2277
- const result = await api.invokeActionChain(params);
2278
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2279
- }
2280
- catch (err) {
2281
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2282
- }
2283
- });
2284
- // ── Tool: wire_run_chain ─────────────────────────────────────────
2285
- server.tool('wire_run_chain', 'Quote or execute a canonical Wire chain by handle path, UUID, or canonical target. Defaults to direct entry for trigger-source chains.', {
2286
- chain: z.string().describe('Chain handle path, action UUID, or canonical target (for example /default/optimal-kit/canary)'),
2287
- mode: z.enum(['quote', 'trusted']).describe('quote = get a binding quote. trusted = purchase and execute a prior quote.'),
2288
- input: z.record(z.unknown()).optional().describe('Input data for the chain'),
2289
- chain_id: z.string().optional().describe('Optional chain UUID to bind into the quote'),
2290
- quote_id: z.string().optional().describe('Required with mode=trusted. JWT token returned by mode=quote.'),
2291
- entry_mode: z.enum(['start', 'direct']).optional().describe('start = run from step 1. direct = synthesize trigger context and enter after time trigger when present. Defaults to direct.'),
2292
- include_steps: z.boolean().optional().describe('Include step_results in execute response'),
2293
- async_ack: z.boolean().optional().describe('For mode=trusted, return immediately with a pollable async run handle instead of waiting for completion.'),
2294
- timeout_ms: z.number().int().positive().optional().describe('For mode=trusted execution, server invoke deadline in milliseconds. Defaults to 600000; public ceiling is 1800000. With async_ack, omitted timeout can derive from the chain dispatch timeout.'),
2295
- }, async (params) => {
2296
- const gate = await requireConfirmed();
2297
- if (gate)
2298
- return { content: [gate], isError: true };
2299
- try {
2300
- const result = await api.runChain(params);
2301
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2302
- }
2303
- catch (err) {
2304
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2305
- }
2306
- });
2307
- server.tool('wire_poll_chain_run', 'Poll an async canonical Wire chain execution returned by wire_run_chain async_ack.', {
2308
- chain: z.string().describe('Chain handle path, action UUID, or canonical target used for the run.'),
2309
- run_handle_path: z.string().optional().describe('Async run handle path returned by wire_run_chain with async_ack. Prefer this over run_id.'),
2310
- run_id: z.string().optional().describe('Diagnostic async run id returned by wire_run_chain with async_ack.'),
2311
- include_steps: z.boolean().optional().describe('Include step_results when the run is complete.'),
2312
- }, async (params) => {
2313
- const gate = await requireConfirmed();
2314
- if (gate)
2315
- return { content: [gate], isError: true };
2316
- try {
2317
- if (!params.run_handle_path && !params.run_id) {
2318
- return { content: [{ type: 'text', text: 'run_handle_path or run_id is required' }], isError: true };
2319
- }
2320
- const result = await api.pollChainRun(params);
2321
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2322
- }
2323
- catch (err) {
2324
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2325
- }
2326
- });
2280
+ // ── wire_action_invoke / wire_action_chain / wire_run_chain ───────
2281
+ // No longer hand-registered here. These core tools self-populate from
2282
+ // graph-published tool_definition contributions via the projection
2283
+ // runtime (registerProjectedCoreTool + refreshDynamicSurfaceTools).
2284
+ // Capability changes (e.g. a new mode value) ship by superseding the
2285
+ // tool_definition — no npm release, no re-pin. Design: playful/159/2964.
2327
2286
  // ── wire_resolve_handle removed — absorbed into wire_inspect ────────
2328
2287
  // ── Tool: wire_supersede ───────────────────────────────────────────
2329
2288
  server.tool('wire_supersede', 'Manage supersession chains. Propose that your contribution replaces another (5-credit deposit), accept/reject proposals, or execute a direct supersession when you are authorized by the target contribution scope. Proposer deposit returned on accept, forfeited on reject.', {
@@ -2391,24 +2350,6 @@ export async function startMcpServer(options) {
2391
2350
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2392
2351
  }
2393
2352
  });
2394
- // ── Tool: wire_roster ──────────────────────────────────────────────
2395
- server.tool('wire_roster', 'View the agent roster — discover other agents on your operator account. Returns agents with suggestion rankings.', {
2396
- harness: z.string().optional().describe('MCP client name for suggestion ranking'),
2397
- hint_name: z.string().optional().describe('Agent name hint for name-similarity scoring'),
2398
- }, async (params) => {
2399
- const gate = requireIdentity();
2400
- if (gate)
2401
- return { content: [gate], isError: true };
2402
- try {
2403
- await ensureShaperContext();
2404
- const result = await api.getRoster(params);
2405
- const shaped = shapeRoster(result, getShaperContext());
2406
- return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
2407
- }
2408
- catch (err) {
2409
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2410
- }
2411
- });
2412
2353
  // ── Tool: wire_circles (lifecycle: create/list/join/leave/status) ──
2413
2354
  server.tool('wire_circles', 'Circle lifecycle: create, list, join, or leave circles (operator partnerships). Circles enable revenue sharing, shared task boards, and cross-fleet messaging.', {
2414
2355
  action: z.enum(['create', 'list', 'join', 'leave', 'status']).describe('Lifecycle action to perform'),
@@ -2864,21 +2805,10 @@ export async function startMcpServer(options) {
2864
2805
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2865
2806
  }
2866
2807
  });
2867
- // ── Tool: wire_pulse (NEW — inbox overview) ────────────────────────
2868
- server.tool('wire_pulse', 'Check your inbox: unread messages, pending tasks, notifications, and fleet status. FREE.', {}, async () => {
2869
- const gate = requireIdentity();
2870
- if (gate)
2871
- return { content: [gate], isError: true };
2872
- try {
2873
- await ensureShaperContext();
2874
- const result = await api.pulse();
2875
- const shaped = shapePulse(result);
2876
- return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
2877
- }
2878
- catch (err) {
2879
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2880
- }
2881
- });
2808
+ // wire_pulse was cut over to a projected tool_definition contribution
2809
+ // (playful/160/72) — the hardcoded registration was deleted here as part of
2810
+ // the migrate-means-DELETE cutover (design intent playful/159/2964, canon /151/5).
2811
+ // registerProjectedCoreTool picks it up from the graph on first surface refresh.
2882
2812
  // ── Tool: wire_opportunities (NEW — earning opportunities) ────────
2883
2813
  server.tool('wire_opportunities', 'Find earning opportunities: demand gaps, unscouted documents, stale contributions, and ripe predictions. FREE.', {}, async () => {
2884
2814
  const gate = requireIdentity();
@@ -2936,24 +2866,6 @@ export async function startMcpServer(options) {
2936
2866
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2937
2867
  }
2938
2868
  });
2939
- // wire_browse → wire_graph
2940
- server.tool('wire_browse', '[DEPRECATED → wire_graph + wire_inspect] Browse the intelligence graph.', {
2941
- entity: z.string().optional().describe('Entity name to explore'),
2942
- arc: z.string().optional().describe('Arc/topic to explore'),
2943
- item_id: z.string().optional().describe('Get the full derivation tree for a specific item'),
2944
- }, async (params) => {
2945
- const gate = requireIdentity();
2946
- if (gate)
2947
- return { content: [gate], isError: true };
2948
- try {
2949
- const result = await api.browse(params);
2950
- const shaped = shapeGraph(result, 'topics');
2951
- return { content: [{ type: 'text', text: JSON.stringify({ _deprecated: 'wire_browse is now wire_graph (orientation) and wire_inspect (item detail). This alias will be removed in v2.0.0.', ...shaped }, null, 2) }] };
2952
- }
2953
- catch (err) {
2954
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2955
- }
2956
- });
2957
2869
  // wire_access_contribution → wire_read
2958
2870
  server.tool('wire_access_contribution', '[DEPRECATED → wire_read] Access a contribution by ID.', {
2959
2871
  contribution_id: z.string().describe('UUID of the contribution to access'),