@agent-wire/terminal 1.7.4 → 1.7.6

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.
@@ -10,7 +10,7 @@ import { join, isAbsolute } from 'path';
10
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, shapeRead, shapeInspect, shapeGraph, shapeCorpora, shapeMyContributions, shapeEarnings, shapePulse, shapeOpportunities, shapeRate, shapePatrolScan, shapeNotifications, shapeTasks, 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
15
  import { parseToolDefinition, dedupeToolDefinitions, fingerprintToolDefinitions, jsonSchemaToZodShape, } from './tool-projection.js';
16
16
  import {
@@ -21,11 +21,12 @@ import {
21
21
  // migration window.
22
22
  loadDeviceRecord, saveDeviceRecord, deleteDeviceRecord, loadAuthPending, saveAuthPending, deleteAuthPending, getStableDeviceFingerprint, hasAnyLocalIdentityForEmail, acquireIdentityLock, releaseIdentityLock, } from './identity.js';
23
23
  import { acquireLock, releaseLock, isWorkerRunning } from './lockfile.js';
24
- import { walkDirectory, computeDiff, pushFiles, pullFiles, parseManifest, serializeManifest, updateManifest, paginateAll, } from './sync/index.js';
24
+ import { walkDirectory, computeDiff, pushFiles, pullFiles, parseManifest, serializeManifest, updateManifest, paginateAll, generatePreviewJson, } from './sync/index.js';
25
25
  import { WIRE_HELP_POINTER } from './help-pointer.js';
26
26
  import { pullLocalSubscriptions } from './local-pull.js';
27
27
  const SURFACE_REFRESH_TTL_MS = 60_000;
28
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"}).';
29
30
  function normalizeSurfaceName(name) {
30
31
  return name.trim().toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
31
32
  }
@@ -250,7 +251,7 @@ export async function startMcpServer(options) {
250
251
  function requireIdentity() {
251
252
  if (processSlot)
252
253
  return null;
253
- return { type: 'text', text: 'Call wire_identify first to establish your identity.' };
254
+ return { type: 'text', text: WIRE_DOORWAY_AFFORDANCE };
254
255
  }
255
256
  // ── Session state for response shaping ──────────────────────────────
256
257
  let shaperContext = null;
@@ -528,6 +529,20 @@ export async function startMcpServer(options) {
528
529
  existing.remove();
529
530
  projectedCoreTools.delete(def.toolName);
530
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
+ }
531
546
  const registered = server.registerTool(def.toolName, {
532
547
  title: def.toolName,
533
548
  description: projectedToolDescription(def),
@@ -630,7 +645,13 @@ export async function startMcpServer(options) {
630
645
  for (const def of toolDefinitions) {
631
646
  if (!previousDefinitionNames.has(def.toolName))
632
647
  added.push(def.toolName);
633
- registerProjectedCoreTool(def);
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
+ }
634
655
  }
635
656
  const fingerprint = fingerprintSubstrateSurfaces(surfaces) + fingerprintToolDefinitions(toolDefinitions);
636
657
  const previousFingerprint = activeToolSetFingerprint;
@@ -1570,9 +1591,21 @@ export async function startMcpServer(options) {
1570
1591
  }
1571
1592
  });
1572
1593
  // ── Tool: wire_corpora (split from wire_discover — corpus browser) ──
1573
- server.tool('wire_corpora', 'Browse source corpora. List all corpora, or list documents within a specific corpus. Filter by visibility. FREE.', {
1594
+ server.tool('wire_corpora', 'Browse or create source corpora. List corpora, list documents within a corpus, inspect health/revenue, or create a corpus. FREE except writes require approval.', {
1595
+ action: z.enum(['list', 'documents', 'create', 'health', 'revenue', 'bulk_ingest']).optional().describe('Action to perform. Defaults to documents when slug is present, otherwise list.'),
1574
1596
  slug: z.string().optional().describe('Corpus slug to browse. Omit to list all corpora.'),
1597
+ title: z.string().optional().describe('Corpus title for create.'),
1598
+ description: z.string().optional().describe('Corpus description for create.'),
1599
+ material_class: z.string().optional().describe('Corpus material class for create.'),
1575
1600
  visibility: z.enum(['public', 'private', 'all']).optional().describe('Filter by visibility (default: public)'),
1601
+ themes: z.array(z.string()).optional().describe('Optional corpus themes for create.'),
1602
+ default_author_share: z.number().optional().describe('Default author revenue share for create.'),
1603
+ default_steward_share: z.number().optional().describe('Default steward revenue share for create.'),
1604
+ split_justification: z.string().optional().describe('Revenue split justification for create.'),
1605
+ provenance_note: z.string().optional().describe('Provenance note for create.'),
1606
+ manifest: z.record(z.unknown()).optional().describe('Optional corpus manifest for create.'),
1607
+ days: z.number().optional().describe('Revenue lookback days for revenue action.'),
1608
+ documents: z.array(z.record(z.unknown())).optional().describe('Documents for bulk_ingest action.'),
1576
1609
  limit: z.number().optional().describe('Max results'),
1577
1610
  offset: z.number().optional().describe('Pagination offset'),
1578
1611
  }, async (params) => {
@@ -1581,13 +1614,54 @@ export async function startMcpServer(options) {
1581
1614
  return { content: [gate], isError: true };
1582
1615
  try {
1583
1616
  let result;
1584
- if (params.slug) {
1585
- result = await api.getCorpusDocuments(params.slug, params.limit, params.offset);
1586
- }
1587
- else {
1588
- result = await api.listCorpora();
1617
+ const action = params.action ?? (params.slug ? 'documents' : 'list');
1618
+ switch (action) {
1619
+ case 'list':
1620
+ result = await api.listCorpora();
1621
+ break;
1622
+ case 'documents':
1623
+ if (!params.slug)
1624
+ return { content: [{ type: 'text', text: 'Error: slug is required for documents action' }], isError: true };
1625
+ result = await api.getCorpusDocuments(params.slug, params.limit, params.offset);
1626
+ break;
1627
+ case 'create':
1628
+ if (!params.slug)
1629
+ return { content: [{ type: 'text', text: 'Error: slug is required for create action' }], isError: true };
1630
+ if (!params.title)
1631
+ return { content: [{ type: 'text', text: 'Error: title is required for create action' }], isError: true };
1632
+ result = await api.createCorpus({
1633
+ slug: params.slug,
1634
+ title: params.title,
1635
+ description: params.description,
1636
+ material_class: params.material_class,
1637
+ visibility: params.visibility === 'all' ? undefined : params.visibility,
1638
+ themes: params.themes,
1639
+ default_author_share: params.default_author_share,
1640
+ default_steward_share: params.default_steward_share,
1641
+ split_justification: params.split_justification,
1642
+ provenance_note: params.provenance_note,
1643
+ manifest: params.manifest,
1644
+ });
1645
+ break;
1646
+ case 'health':
1647
+ if (!params.slug)
1648
+ return { content: [{ type: 'text', text: 'Error: slug is required for health action' }], isError: true };
1649
+ result = await api.getCorpusHealth(params.slug);
1650
+ break;
1651
+ case 'revenue':
1652
+ if (!params.slug)
1653
+ return { content: [{ type: 'text', text: 'Error: slug is required for revenue action' }], isError: true };
1654
+ result = await api.getCorpusRevenue(params.slug, params.days);
1655
+ break;
1656
+ case 'bulk_ingest':
1657
+ if (!params.slug)
1658
+ return { content: [{ type: 'text', text: 'Error: slug is required for bulk_ingest action' }], isError: true };
1659
+ if (!params.documents)
1660
+ return { content: [{ type: 'text', text: 'Error: documents is required for bulk_ingest action' }], isError: true };
1661
+ result = await api.bulkIngestDocuments(params.slug, params.documents);
1662
+ break;
1589
1663
  }
1590
- const shaped = shapeCorpora(result, params.slug);
1664
+ const shaped = action === 'list' || action === 'documents' ? shapeCorpora(result, action === 'documents' ? params.slug : undefined) : result;
1591
1665
  return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
1592
1666
  }
1593
1667
  catch (err) {
@@ -1762,6 +1836,7 @@ export async function startMcpServer(options) {
1762
1836
  action: z.enum(['diff', 'push', 'pull', 'preview']).describe('Sync action to perform'),
1763
1837
  publish: z.boolean().optional().describe('Legacy push only: publish new document versions instead of drafts'),
1764
1838
  market_native_docs_ingest: z.enum(['off', 'auto', 'all']).optional().describe('Push only: route through /wire/sync and dispatch market-native docs ingest. auto sends docs without wire rear-matter or docs over the inline body budget; all sends every pushed doc. Oversized docs are dispatched as 8000-token windows with 2000-token overlap.'),
1839
+ include_content: z.boolean().optional().describe('Diff/preview only: include full local file bodies. Defaults false; previews normally return hash + sizeBytes only.'),
1765
1840
  }, async (params) => {
1766
1841
  const gate = await requireConfirmed();
1767
1842
  if (gate)
@@ -1770,6 +1845,7 @@ export async function startMcpServer(options) {
1770
1845
  const { dir, corpus, action } = params;
1771
1846
  const publish = params.publish ?? false;
1772
1847
  const marketNativeDocsIngest = (params.market_native_docs_ingest ?? 'off');
1848
+ const includeContent = params.include_content === true;
1773
1849
  // ── Path traversal protection ──
1774
1850
  if (!isAbsolute(dir)) {
1775
1851
  return { content: [{ type: 'text', text: 'Error: dir must be an absolute path' }], isError: true };
@@ -1807,7 +1883,7 @@ export async function startMcpServer(options) {
1807
1883
  const remoteDocs = await paginateAll(async (page, pageSize) => {
1808
1884
  const offset = (page - 1) * pageSize;
1809
1885
  const result = await api.getCorpusDocuments(corpus, pageSize, offset);
1810
- return result.documents || [];
1886
+ return result.documents || result.items || [];
1811
1887
  });
1812
1888
  // 4. Compute diff
1813
1889
  const diff = await computeDiff(localFiles, remoteDocs, manifest);
@@ -1822,11 +1898,15 @@ export async function startMcpServer(options) {
1822
1898
  conflicts: diff.conflicts.length,
1823
1899
  };
1824
1900
  const output = {
1901
+ ...generatePreviewJson({
1902
+ corpus_slug: corpus,
1903
+ local_dir: dir,
1904
+ diff,
1905
+ summary,
1906
+ }, { includeContent }),
1825
1907
  corpus_slug: corpus,
1826
1908
  local_dir: dir,
1827
1909
  action,
1828
- diff,
1829
- summary,
1830
1910
  };
1831
1911
  return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
1832
1912
  }
@@ -2179,7 +2259,7 @@ export async function startMcpServer(options) {
2179
2259
  server.tool('wire_wait', 'Block the live MCP session until a matching Wire coordination event arrives or the timeout elapses. Layer 1 uses SSE + cursor replay; durable wire_watch is not included.', {
2180
2260
  triggers: z.array(z.string()).max(20).optional().describe('Event triggers to wait for, e.g. message.unread, task.assigned, task.moved, board.changed, circle.message. Supports prefix wildcards such as message.*. Omit to wait on any message.*, task.*, board.*, or circle.* event addressable to you.'),
2181
2261
  filters: z.record(z.unknown()).optional().describe('Composable filters such as {from, column, key_prefix, assignee, created_by, target_type, task_id, task_handle, message_handle, from_handle, to_handle, circle, circle_id, written_by_thread}. Agent filters accept "me", agent UUID, or pseudo-ID when available. Handle filters match the corresponding event payload field exactly.'),
2182
- timeout_seconds: z.number().int().min(0).max(1800).optional().describe('Maximum seconds to block. Defaults to 30; max 1800. Use 0 for immediate replay/query behavior.'),
2262
+ timeout_seconds: z.number().int().min(-1).max(86400).optional().describe('Maximum seconds to block. Defaults to 30; max 86400 (24h). Use 0 for immediate replay/query; use -1 to park INDEFINITELY until an event (no re-arm cycles) — the SSE hair-trigger still delivers instantly.'),
2183
2263
  cursor: z.string().optional().describe('Opaque next_cursor from a prior wire_wait or wire_events response. Legacy integer id cursors are accepted. If omitted, wait starts at the current high-water mark.'),
2184
2264
  limit: z.number().int().min(1).max(200).optional().describe('Maximum events to return when triggered. Defaults to 50.'),
2185
2265
  }, async (params) => {
@@ -2268,6 +2348,7 @@ export async function startMcpServer(options) {
2268
2348
  action: z.enum(['supersede', 'propose', 'accept', 'reject']).describe('supersede = direct supersession authorized by target scope, propose = propose supersession (5-credit deposit), accept = approve a proposal, reject = deny a proposal'),
2269
2349
  target_id: z.string().describe('Target contribution UUID (the one being superseded for supersede/propose, or the proposal UUID for accept/reject)'),
2270
2350
  contribution_id: z.string().optional().describe('Your replacement contribution UUID (required for supersede and propose)'),
2351
+ allow_type_change: z.boolean().optional().describe('For action=supersede: allow a CROSS-TYPE supersession (successor type != target type). Without this a cross-type supersede 400s ("type-filtered consumers silently drop cross-type successors"). Pass true only when the type change is deliberate.'),
2271
2352
  }, async (params) => {
2272
2353
  const gate = await requireConfirmed();
2273
2354
  if (gate)
@@ -2291,7 +2372,7 @@ export async function startMcpServer(options) {
2291
2372
  if (!params.contribution_id) {
2292
2373
  return { content: [{ type: 'text', text: 'contribution_id is required for supersede' }], isError: true };
2293
2374
  }
2294
- result = await api.executeSupersession(params.target_id, params.contribution_id);
2375
+ result = await api.executeSupersession(params.target_id, params.contribution_id, params.allow_type_change);
2295
2376
  break;
2296
2377
  default:
2297
2378
  return { content: [{ type: 'text', text: 'Invalid action' }], isError: true };
@@ -2784,21 +2865,10 @@ export async function startMcpServer(options) {
2784
2865
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2785
2866
  }
2786
2867
  });
2787
- // ── Tool: wire_pulse (NEW — inbox overview) ────────────────────────
2788
- server.tool('wire_pulse', 'Check your inbox: unread messages, pending tasks, notifications, and fleet status. FREE.', {}, async () => {
2789
- const gate = requireIdentity();
2790
- if (gate)
2791
- return { content: [gate], isError: true };
2792
- try {
2793
- await ensureShaperContext();
2794
- const result = await api.pulse();
2795
- const shaped = shapePulse(result);
2796
- return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
2797
- }
2798
- catch (err) {
2799
- return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2800
- }
2801
- });
2868
+ // wire_pulse was cut over to a projected tool_definition contribution
2869
+ // (playful/160/72) — the hardcoded registration was deleted here as part of
2870
+ // the migrate-means-DELETE cutover (design intent playful/159/2964, canon /151/5).
2871
+ // registerProjectedCoreTool picks it up from the graph on first surface refresh.
2802
2872
  // ── Tool: wire_opportunities (NEW — earning opportunities) ────────
2803
2873
  server.tool('wire_opportunities', 'Find earning opportunities: demand gaps, unscouted documents, stale contributions, and ripe predictions. FREE.', {}, async () => {
2804
2874
  const gate = requireIdentity();