@agent-wire/terminal 1.7.0 → 1.7.2

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.
@@ -18,7 +18,7 @@ import {
18
18
  // their replacements live in slots.ts. The legacy functions in
19
19
  // identity.ts remain for backward compat with wait-cli.ts during the
20
20
  // migration window.
21
- loadDeviceRecord, saveDeviceRecord, loadAuthPending, saveAuthPending, deleteAuthPending, getStableDeviceFingerprint, hasAnyLocalIdentityForEmail, acquireIdentityLock, releaseIdentityLock, } from './identity.js';
21
+ loadDeviceRecord, saveDeviceRecord, deleteDeviceRecord, loadAuthPending, saveAuthPending, deleteAuthPending, getStableDeviceFingerprint, hasAnyLocalIdentityForEmail, acquireIdentityLock, releaseIdentityLock, } from './identity.js';
22
22
  import { acquireLock, releaseLock, isWorkerRunning } from './lockfile.js';
23
23
  import { walkDirectory, computeDiff, pushFiles, pullFiles, parseManifest, serializeManifest, updateManifest, paginateAll, } from './sync/index.js';
24
24
  import { WIRE_HELP_POINTER } from './help-pointer.js';
@@ -81,7 +81,7 @@ function normalizeApiRootPath(path) {
81
81
  }
82
82
  async function fetchSurfaceConfigItems(api) {
83
83
  const queries = ['surface_config', 'dispatcher_bound_surface_config', 'lookup_surface'];
84
- const responses = await Promise.all(queries.map((text) => api.query({ text, domain: 'all', limit: 100 })));
84
+ const responses = await Promise.all(queries.map((text) => api.query({ text, domain: 'all', limit: 100, sort: 'recency' })));
85
85
  return responses.flatMap(extractQueryItems);
86
86
  }
87
87
  async function refreshSubstrateSurfaces(api) {
@@ -147,6 +147,46 @@ function parseToolRefreshPollIntervalMs(items) {
147
147
  }
148
148
  return { pollIntervalMs: Math.round(value), source: 'economic_parameter', handle: latest.handle_path };
149
149
  }
150
+ function toolsAvailableMsg(verificationStatus) {
151
+ return verificationStatus === 'confirmed'
152
+ ? 'All tools available.'
153
+ : 'Pending verification: wire_identify, wire_query, wire_graph, wire_inspect, wire_status, wire_corpora, wire_legal, wire_pulse, wire_opportunities, wire_roster available now. Credit-affecting tools (wire_contribute, wire_rate, wire_flag, wire_correct, wire_messages, wire_market, wire_games, wire_supersede) require operator approval — check your operator\'s email.';
154
+ }
155
+ function looksLikeDeviceAuthFailure(error) {
156
+ if (!error)
157
+ return false;
158
+ return /\b(401|410)\b|revoked|device authorization|device secret|unauthorized/i.test(error.message);
159
+ }
160
+ function buildResumeResponse(params) {
161
+ const resumedStatus = typeof params.verificationStatus === 'string'
162
+ ? params.verificationStatus
163
+ : 'unknown';
164
+ const resumeResponse = {
165
+ status: 'resumed',
166
+ identity: `${params.requestedName} (pseudo-ID: ${params.pseudoId})`,
167
+ operator: params.operatorEmail,
168
+ verification_status: resumedStatus,
169
+ help: WIRE_HELP_POINTER,
170
+ tools_available: toolsAvailableMsg(resumedStatus),
171
+ };
172
+ if (params.sinceLastSession && typeof params.sinceLastSession === 'object') {
173
+ const sls = params.sinceLastSession;
174
+ const parts = [];
175
+ if (sls.new_messages > 0)
176
+ parts.push(`${sls.new_messages} new message${sls.new_messages !== 1 ? 's' : ''}`);
177
+ if (sls.new_tasks > 0)
178
+ parts.push(`${sls.new_tasks} new task${sls.new_tasks !== 1 ? 's' : ''}`);
179
+ if (sls.new_contributions > 0)
180
+ parts.push(`${sls.new_contributions} new contribution${sls.new_contributions !== 1 ? 's' : ''}`);
181
+ if (sls.credits_earned > 0)
182
+ parts.push(`${sls.credits_earned} credits earned`);
183
+ if (parts.length > 0) {
184
+ resumeResponse.while_you_were_away = `While you were away: ${parts.join(', ')}.`;
185
+ }
186
+ resumeResponse.since_last_session = sls;
187
+ }
188
+ return resumeResponse;
189
+ }
150
190
  export const __mcpSurfaceTestInternals = {
151
191
  normalizeSurfaceName,
152
192
  surfaceToolName,
@@ -156,6 +196,8 @@ export const __mcpSurfaceTestInternals = {
156
196
  SURFACE_REFRESH_TTL_MS,
157
197
  fingerprintSubstrateSurfaces,
158
198
  parseToolRefreshPollIntervalMs,
199
+ buildResumeResponse,
200
+ looksLikeDeviceAuthFailure,
159
201
  };
160
202
  export async function startMcpServer(options) {
161
203
  const loadedConfig = loadConfig();
@@ -466,6 +508,7 @@ export async function startMcpServer(options) {
466
508
  text: 'refresh_mcp_tools_poll_interval economic_parameter',
467
509
  domain: 'all',
468
510
  limit: 25,
511
+ sort: 'recency',
469
512
  });
470
513
  const policy = parseToolRefreshPollIntervalMs(extractQueryItems(result));
471
514
  if (policy.source === 'default') {
@@ -556,10 +599,6 @@ export async function startMcpServer(options) {
556
599
  create_new: z.boolean().optional().describe('Set true to explicitly create a new agent identity. Without this, unrecognized names return the roster instead of auto-creating.'),
557
600
  agent_message: z.string().optional().describe('Context message included in any emails sent to the operator during this flow.'),
558
601
  }, async (params) => {
559
- // ── Helper: tools_available message based on verification status ──
560
- const toolsAvailableMsg = (verificationStatus) => verificationStatus === 'confirmed'
561
- ? 'All tools available.'
562
- : 'Pending verification: wire_identify, wire_query, wire_graph, wire_inspect, wire_status, wire_corpora, wire_legal, wire_pulse, wire_opportunities, wire_roster available now. Credit-affecting tools (wire_contribute, wire_rate, wire_flag, wire_correct, wire_messages, wire_market, wire_games, wire_supersede) require operator approval — check your operator\'s email.';
563
602
  const nameKey = params.name.trim().toLowerCase();
564
603
  // ── Mission #1.5-rev2: first-call-wins pin guard ──────────
565
604
  // If this MCP process is already pinned and the request is for
@@ -882,20 +921,134 @@ export async function startMcpServer(options) {
882
921
  }, null, 2) }],
883
922
  };
884
923
  }
885
- // ── 5. Have device secret — get roster ────────────────────
924
+ // ── 5. Have device secret — resume/register without requiring roster ─
886
925
  if (deviceRecord) {
926
+ let resumeError = null;
927
+ if (!params.create_new) {
928
+ const nonce = randomUUID();
929
+ let resumed = null;
930
+ try {
931
+ resumed = await bootApi.resumeAgent(params.name, deviceRecord.deviceSecret, nonce);
932
+ }
933
+ catch (err) {
934
+ resumeError = err;
935
+ }
936
+ if (resumed) {
937
+ establishPin(params.name, {
938
+ apiToken: resumed.api_token,
939
+ agentId: resumed.agent_id,
940
+ pseudoId: resumed.pseudo_id,
941
+ operatorEmail: params.operator_email,
942
+ verificationStatus: resumed.verification_status,
943
+ }, 'device-resume');
944
+ await discoverSurfacesAtPin('device-resume');
945
+ await ensureNodeAndMaybeStartWorker();
946
+ const canonicalName = resumed.name || params.name;
947
+ shaperContext = {
948
+ agentName: canonicalName,
949
+ agentHandle: null,
950
+ verificationStatus: normalizeVerificationStatus(resumed.verification_status),
951
+ firstIdentifyInSession: false,
952
+ lastSessionAt: null,
953
+ lastAccessedContributionId: null,
954
+ };
955
+ console.error(`[wire-mcp] Resumed via device secret: ${canonicalName} (${resumed.pseudo_id})`);
956
+ return {
957
+ content: [{ type: 'text', text: JSON.stringify(buildResumeResponse({
958
+ requestedName: params.name,
959
+ pseudoId: resumed.pseudo_id,
960
+ operatorEmail: params.operator_email,
961
+ verificationStatus: resumed.verification_status,
962
+ sinceLastSession: resumed.since_last_session,
963
+ }), null, 2) }],
964
+ };
965
+ }
966
+ console.error(`[wire-mcp] Direct agent resume failed for ${params.name}: ${resumeError?.message ?? 'unknown error'}`);
967
+ }
887
968
  const harness = process.env.MCP_CLIENT_NAME || undefined;
888
969
  let roster;
889
970
  try {
890
971
  roster = await bootApi.getRosterWithDeviceSecret(deviceRecord.deviceSecret, harness, params.name);
891
972
  }
892
973
  catch (err) {
893
- console.error(`[wire-mcp] Roster fetch failed: ${err.message}`);
894
- // Device secret might be revoked — clear it and fall back to device auth
974
+ const rosterError = err;
975
+ console.error(`[wire-mcp] Roster fetch failed: ${rosterError.message}`);
976
+ if (params.create_new) {
977
+ const sessionMetadata = {
978
+ hostname: hostname(),
979
+ pid: process.pid,
980
+ client: process.env.MCP_CLIENT_NAME || 'unknown',
981
+ session_started: new Date().toISOString(),
982
+ };
983
+ let result;
984
+ try {
985
+ result = await bootApi.registerAgent({
986
+ name: params.name,
987
+ operator_email: params.operator_email,
988
+ domains: ['all'],
989
+ session_metadata: sessionMetadata,
990
+ agent_message: params.agent_message,
991
+ });
992
+ }
993
+ catch (registerErr) {
994
+ return {
995
+ content: [{ type: 'text', text: JSON.stringify({
996
+ status: 'error',
997
+ error: `Roster unavailable (${rosterError.message}) and direct agent registration failed: ${registerErr.message}`,
998
+ }, null, 2) }],
999
+ isError: true,
1000
+ };
1001
+ }
1002
+ if (!result.api_token || !result.pseudo_id || !result.agent_id) {
1003
+ return {
1004
+ content: [{ type: 'text', text: JSON.stringify({
1005
+ status: 'error', error: 'Registration response missing critical fields.',
1006
+ }, null, 2) }],
1007
+ isError: true,
1008
+ };
1009
+ }
1010
+ establishPin(params.name, {
1011
+ apiToken: result.api_token,
1012
+ agentId: result.agent_id,
1013
+ pseudoId: result.pseudo_id,
1014
+ operatorEmail: params.operator_email,
1015
+ verificationStatus: result.verification_status,
1016
+ }, 'direct-create-new');
1017
+ await discoverSurfacesAtPin('direct-create-new');
1018
+ await ensureNodeAndMaybeStartWorker();
1019
+ shaperContext = {
1020
+ agentName: params.name,
1021
+ agentHandle: null,
1022
+ verificationStatus: normalizeVerificationStatus(result.verification_status),
1023
+ firstIdentifyInSession: true,
1024
+ lastSessionAt: null,
1025
+ lastAccessedContributionId: null,
1026
+ };
1027
+ return { content: [{ type: 'text', text: JSON.stringify({
1028
+ status: 'registered',
1029
+ identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
1030
+ verification_status: result.verification_status,
1031
+ roster_warning: `Roster lookup failed (${rosterError.message}); registered directly via /register.`,
1032
+ help: WIRE_HELP_POINTER,
1033
+ tools_available: toolsAvailableMsg(result.verification_status),
1034
+ }, null, 2) }] };
1035
+ }
1036
+ if (looksLikeDeviceAuthFailure(resumeError) && looksLikeDeviceAuthFailure(rosterError)) {
1037
+ deleteDeviceRecord(params.operator_email);
1038
+ return {
1039
+ content: [{ type: 'text', text: JSON.stringify({
1040
+ status: 'reauthorization_required',
1041
+ error: `Device authorization appears revoked: direct resume failed (${resumeError?.message ?? 'unknown error'}); roster lookup failed (${rosterError.message}).`,
1042
+ next_step: 'Call wire_identify again without device_code to request a fresh email authorization code.',
1043
+ }, null, 2) }],
1044
+ isError: true,
1045
+ };
1046
+ }
895
1047
  return {
896
1048
  content: [{ type: 'text', text: JSON.stringify({
897
- status: 'error',
898
- error: `Failed to fetch roster: ${err.message}. Your device authorization may have been revoked. Try again to re-authorize.`,
1049
+ status: 'identity_resolution_failed',
1050
+ error: `Failed to resume "${params.name}" directly, and roster lookup is unavailable: ${rosterError.message}`,
1051
+ hint: 'Check the agent name, or retry with create_new:true to register a new identity. Device authorization itself may still be valid.',
899
1052
  }, null, 2) }],
900
1053
  isError: true,
901
1054
  };
@@ -939,33 +1092,14 @@ export async function startMcpServer(options) {
939
1092
  };
940
1093
  const resumedStatus = resumed.verification_status ?? 'unknown';
941
1094
  console.error(`[wire-mcp] Resumed via roster: ${canonicalName} (${resumed.pseudo_id})`);
942
- const resumeResponse = {
943
- status: 'resumed',
944
- identity: `${params.name} (pseudo-ID: ${resumed.pseudo_id})`,
945
- operator: params.operator_email,
946
- verification_status: resumed.verification_status ?? 'unknown',
947
- help: WIRE_HELP_POINTER,
948
- tools_available: toolsAvailableMsg(resumedStatus),
949
- };
950
- // Include "while you were away" summary if available
951
- if (resumed.since_last_session) {
952
- const sls = resumed.since_last_session;
953
- const parts = [];
954
- if (sls.new_messages > 0)
955
- parts.push(`${sls.new_messages} new message${sls.new_messages !== 1 ? 's' : ''}`);
956
- if (sls.new_tasks > 0)
957
- parts.push(`${sls.new_tasks} new task${sls.new_tasks !== 1 ? 's' : ''}`);
958
- if (sls.new_contributions > 0)
959
- parts.push(`${sls.new_contributions} new contribution${sls.new_contributions !== 1 ? 's' : ''}`);
960
- if (sls.credits_earned > 0)
961
- parts.push(`${sls.credits_earned} credits earned`);
962
- if (parts.length > 0) {
963
- resumeResponse.while_you_were_away = `While you were away: ${parts.join(', ')}.`;
964
- }
965
- resumeResponse.since_last_session = sls;
966
- }
967
1095
  return {
968
- content: [{ type: 'text', text: JSON.stringify(resumeResponse, null, 2) }],
1096
+ content: [{ type: 'text', text: JSON.stringify(buildResumeResponse({
1097
+ requestedName: params.name,
1098
+ pseudoId: resumed.pseudo_id,
1099
+ operatorEmail: params.operator_email,
1100
+ verificationStatus: resumedStatus,
1101
+ sinceLastSession: resumed.since_last_session,
1102
+ }), null, 2) }],
969
1103
  };
970
1104
  }
971
1105
  // Case B: Name doesn't match + create_new → register
@@ -1586,16 +1720,23 @@ export async function startMcpServer(options) {
1586
1720
  dir: z.string().describe('Absolute path to local directory'),
1587
1721
  corpus: z.string().describe('Wire corpus slug'),
1588
1722
  action: z.enum(['diff', 'push', 'pull', 'preview']).describe('Sync action to perform'),
1723
+ publish: z.boolean().optional().describe('Legacy push only: publish new document versions instead of drafts'),
1724
+ 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.'),
1589
1725
  }, async (params) => {
1590
1726
  const gate = await requireConfirmed();
1591
1727
  if (gate)
1592
1728
  return { content: [gate], isError: true };
1593
1729
  try {
1594
1730
  const { dir, corpus, action } = params;
1731
+ const publish = params.publish ?? false;
1732
+ const marketNativeDocsIngest = (params.market_native_docs_ingest ?? 'off');
1595
1733
  // ── Path traversal protection ──
1596
1734
  if (!isAbsolute(dir)) {
1597
1735
  return { content: [{ type: 'text', text: 'Error: dir must be an absolute path' }], isError: true };
1598
1736
  }
1737
+ if (marketNativeDocsIngest !== 'off' && action === 'pull') {
1738
+ return { content: [{ type: 'text', text: 'Error: market_native_docs_ingest only applies to push-side sync' }], isError: true };
1739
+ }
1599
1740
  const BLOCKED_PREFIXES = ['/etc', '/var', '/sys', '/proc', '/dev', '/root/.ssh'];
1600
1741
  const normalizedDir = dir.replace(/\/+$/, ''); // strip trailing slashes
1601
1742
  if (BLOCKED_PREFIXES.some(prefix => normalizedDir === prefix || normalizedDir.startsWith(prefix + '/'))) {
@@ -1652,7 +1793,10 @@ export async function startMcpServer(options) {
1652
1793
  if (action === 'push') {
1653
1794
  // Push new + updated files, but surface conflicts separately
1654
1795
  const entriesToPush = [...diff.push_new, ...diff.push_update];
1655
- const pushResults = await pushFiles(api, corpus, entriesToPush);
1796
+ const pushResults = await pushFiles(api, corpus, entriesToPush, {
1797
+ publish,
1798
+ marketNativeDocsIngest,
1799
+ });
1656
1800
  // Build manifest update entries from successful pushes
1657
1801
  const manifestEntries = pushResults
1658
1802
  .filter(r => r.success)
@@ -1664,9 +1808,9 @@ export async function startMcpServer(options) {
1664
1808
  documentId: r.documentId,
1665
1809
  localHash: localFile.hash,
1666
1810
  remoteHash: localFile.hash, // after push, remote matches local
1667
- remoteStatus: 'draft',
1811
+ remoteStatus: (publish || marketNativeDocsIngest !== 'off' ? 'published' : 'draft'),
1668
1812
  remoteAnchored: false,
1669
- remoteVersionNumber: entry.remoteDoc?.version_number ?? 1,
1813
+ remoteVersionNumber: r.version ?? entry.remoteDoc?.version_number ?? 1,
1670
1814
  remoteWordCount: localFile.content.split(/\s+/).length,
1671
1815
  };
1672
1816
  });
@@ -1682,6 +1826,7 @@ export async function startMcpServer(options) {
1682
1826
  const output = {
1683
1827
  action: 'push',
1684
1828
  corpus_slug: corpus,
1829
+ market_native_docs_ingest: marketNativeDocsIngest,
1685
1830
  results: pushResults,
1686
1831
  conflicts: diff.conflicts,
1687
1832
  summary: {
@@ -2102,6 +2247,7 @@ export async function startMcpServer(options) {
2102
2247
  input: z.record(z.unknown()).optional().describe('Input data for the action'),
2103
2248
  max_cost: z.number().optional().describe('Maximum credits to spend (>= 1)'),
2104
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.'),
2105
2251
  }, async (params) => {
2106
2252
  const gate = await requireConfirmed();
2107
2253
  if (gate)
@@ -2122,6 +2268,7 @@ export async function startMcpServer(options) {
2122
2268
  max_cost: z.number().optional().describe('Maximum credits to spend (>= 1)'),
2123
2269
  chain_id: z.string().optional().describe('Chain UUID to continue (omit to start a new chain)'),
2124
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.'),
2125
2272
  }, async (params) => {
2126
2273
  const gate = await requireConfirmed();
2127
2274
  if (gate)
@@ -2143,6 +2290,8 @@ export async function startMcpServer(options) {
2143
2290
  quote_id: z.string().optional().describe('Required with mode=trusted. JWT token returned by mode=quote.'),
2144
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.'),
2145
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.'),
2146
2295
  }, async (params) => {
2147
2296
  const gate = await requireConfirmed();
2148
2297
  if (gate)
@@ -2155,6 +2304,26 @@ export async function startMcpServer(options) {
2155
2304
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2156
2305
  }
2157
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
+ });
2158
2327
  // ── wire_resolve_handle removed — absorbed into wire_inspect ────────
2159
2328
  // ── Tool: wire_supersede ───────────────────────────────────────────
2160
2329
  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.', {