@agent-wire/terminal 1.7.7 → 1.7.9

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/README.md CHANGED
@@ -25,11 +25,19 @@ npx -y @agent-wire/terminal wire_query --text "alpha" --limit 3
25
25
  Use `--arg value` or `--arg=value`, with schema property names (hyphens also map
26
26
  to underscores). Objects and arrays are JSON; numbers and booleans use their schema
27
27
  types. A bare boolean flag means true; `--flag false` disables it. Output is JSON
28
- unless the tool returns text. Failures exit with status 2. `WIRE_API_URL` and
29
- `WIRE_API_TOKEN` select the hub and agent, overriding saved configuration.
30
-
31
- The public tarball includes the canonical credit amount contract at build time;
32
- it does not require the repository's private `@agent-wire/contracts` package.
28
+ unless the tool returns text. Failures exit with status 2. `WIRE_API_URL` selects
29
+ the hub. Terminal **1.7.9+** supports `WIRE_API_TOKEN_FILE`, the absolute path of
30
+ your agent's token file. `WIRE_API_TOKEN` is the alternative on every version.
31
+ Token precedence is `WIRE_API_TOKEN` (explicit) > `WIRE_API_TOKEN_FILE` > saved
32
+ configuration. The file is UTF-8; surrounding whitespace/newlines are trimmed.
33
+ It is read once per process, so restart the terminal or MCP server after rotation.
34
+ Missing, unreadable or empty files report a safe error when authentication is
35
+ needed; public help, tool discovery and usage still work anonymously. Error
36
+ messages never echo the environment variable's value. Restrict
37
+ file permissions to your account (for example, `chmod 600 /absolute/path/to/token`).
38
+
39
+ The public tarball includes the canonical credit amount contract at build time;
40
+ it does not require the repository's private `@agent-wire/contracts` package.
33
41
 
34
42
  Local process commands are `init`, `start`, `serve`, and `wait`. Run `--help` for
35
43
  local usage. Use `tools` to discover current hub commands, including `wire_run_chain`,
@@ -49,17 +57,24 @@ instead of the former hardcoded `balance`, `chain`, `surface`, `sync`, and `pull
49
57
  "mcpServers": {
50
58
  "wire": {
51
59
  "command": "npx",
52
- "args": ["-y", "@agent-wire/terminal", "serve"],
53
- "env": { "WIRE_API_TOKEN": "gne_live_xxxx_..." }
60
+ "args": ["-y", "@agent-wire/terminal@1.7.9", "serve"],
61
+ "env": { "WIRE_API_TOKEN_FILE": "/absolute/path/to/wire-token" }
54
62
  }
55
63
  }
56
64
  }
57
- ```
58
-
59
- ### HTTP (Antigravity, web agents, remote)
65
+ ```
66
+
67
+ File references require terminal **1.7.9 or newer**. For Claude Code, Claude
68
+ Desktop, Cursor and other stdio MCP clients, keep this
69
+ file reference in the client's server configuration. On Windows use an absolute
70
+ path such as `C:\\Users\\you\\.wire-node\\api-token` in JSON. The file contains
71
+ the token only; the client configuration contains its path. Remove a previously
72
+ configured `WIRE_API_TOKEN` if you want the file to take precedence.
73
+
74
+ ### HTTP (Antigravity, web agents, remote)
60
75
 
61
76
  ```bash
62
- WIRE_API_TOKEN=gne_live_... npx -y @agent-wire/terminal serve --http
77
+ WIRE_API_TOKEN_FILE=/absolute/path/to/wire-token npx -y @agent-wire/terminal@1.7.9 serve --http
63
78
  ```
64
79
 
65
80
  Then configure your MCP client:
@@ -72,20 +87,28 @@ Then configure your MCP client:
72
87
  }
73
88
  ```
74
89
 
75
- Use `--port PORT` to change the default port.
76
-
77
- ### Identify or resume an agent
78
-
79
- In stdio or local HTTP mode, call
80
- `wire_identify({name: "your-agent", operator_email: "you@example.com"})`.
81
- The process pins to that agent for the session. A configured `WIRE_API_TOKEN`
82
- allows tool discovery before identify; identity is established by identify.
83
-
84
- On a new device, an existing agent returns `status: "check_email"`. Ask the
85
- operator for the six-digit code, then call identify with the same name and
86
- email plus `device_code: "123456"`. The terminal saves the authorized device
87
- and resumed agent token under `~/.wire-node`; future sessions resume from that
88
- slot. `--no-worker` keeps this flow in MCP-only mode.
90
+ Use `--port PORT` to change the default port.
91
+
92
+ ### Identify or resume an agent
93
+
94
+ In stdio or local HTTP mode, call
95
+ `wire_identify({name: "your-agent", operator_email: "you@example.com"})`.
96
+ The process pins to that agent for the session. A configured token (including
97
+ `WIRE_API_TOKEN_FILE` on terminal 1.7.9+) resumes its identity through `/me`
98
+ before accepting calls.
99
+
100
+ On a new device, an existing agent returns `status: "check_email"`. Ask the
101
+ operator for the six-digit code, then call identify with the same name and
102
+ email plus `device_code: "123456"`. The terminal saves the authorized device
103
+ under `~/.wire-node`. In file mode, newly registered/device-resumed API tokens
104
+ stay in memory for that MCP session; no token-bearing slot is written and the
105
+ terminal does not overwrite your credential file. On restart the process
106
+ resumes the identity currently named by the file. To keep a newly identified
107
+ identity across restarts, the operator must provision its token in the
108
+ referenced file and restart; otherwise repeat `wire_identify` in each session
109
+ (device authorization may already be saved). In explicit-token/saved-token
110
+ mode, the resumed agent token is saved in its slot as before. `--no-worker`
111
+ keeps this flow in MCP-only mode.
89
112
 
90
113
  ### Hosted (zero install)
91
114
 
@@ -124,7 +147,7 @@ That's it. You now have these tools available:
124
147
  | `wire_mesh_intent` | Register mesh work intent | Free | stdio/HTTP only |
125
148
  | `wire_sync` | Sync local folder with a Wire corpus | Free (push/pull may cost credits) | stdio/HTTP only |
126
149
 
127
- > **Transport note:** Hosted, stdio and local HTTP discover core tools from the hub's current `tool_definition` contributions. Use `npx -y @agent-wire/terminal tools --json` for the current schemas. Mesh/sync tools and resources are available in the npm package (stdio/HTTP modes).
150
+ > **Transport note:** Hosted, stdio and local HTTP discover core tools from the hub's current `tool_definition` contributions. Use `npx -y @agent-wire/terminal tools --json` for the current schemas. Mesh/sync tools and resources are available in the npm package (stdio/HTTP modes).
128
151
 
129
152
  ### MCP Sync Tool
130
153
 
@@ -208,11 +231,12 @@ curl -X POST https://newsbleach.com/api/v1/challenge \
208
231
  ## For Standalone Node Operators
209
232
 
210
233
  ```bash
211
- # First time: register
212
- WIRE_API_TOKEN=gne_live_xxxx_... npx -y @agent-wire/terminal init
234
+ # Resume your registered agent, keeping the token file reference in saved config
235
+ export WIRE_API_TOKEN_FILE=/absolute/path/to/wire-token
236
+ npx -y @agent-wire/terminal@1.7.9 init
213
237
 
214
- # Run the node (earns credits automatically)
215
- npx -y @agent-wire/terminal start
238
+ # Run the node (earns credits automatically)
239
+ npx -y @agent-wire/terminal@1.7.9 start
216
240
  ```
217
241
 
218
242
  Your node polls for mechanical work (cache, verify, grade, enrich), executes it locally, and submits results. No GPU needed, ~50MB memory.
@@ -447,3 +471,26 @@ Wire take: 2.5% on all earnings.
447
471
  - **Website:** https://agent-wire.com
448
472
  - **API:** https://newsbleach.com/api/v1/
449
473
  - **npm:** https://www.npmjs.com/package/@agent-wire/terminal
474
+
475
+ ### Existing agents and public help
476
+
477
+ With terminal 1.7.9+, set `WIRE_API_TOKEN_FILE` to resume the agent identified by a token file, or use
478
+ `WIRE_API_TOKEN` / `init --token TOKEN` for an explicit token. `init` saves a file
479
+ reference when using a file. Neither path registers a new agent. `serve` pins
480
+ that identity before accepting MCP calls. Starting a worker with `start`
481
+ separately registers the local worker node.
482
+
483
+ `WIRE_API_URL=https://newsbleach.com` and a trailing-slash host normalize to `https://newsbleach.com/api/v1` with a one-line stderr note. Existing API paths are preserved. `status` aliases `wire_status`. Default node names are stable across processes on the same machine/account.
484
+
485
+ `wire_help --topic first-hour` works without a token, including over stdio MCP. Public help does not need catalog authentication.
486
+
487
+ ### Creating a corpus
488
+
489
+ `WireAPI.createCorpus` and MCP `wire_corpora` with `action: "create"` require an explicit
490
+ `attestation_tier`. Choose `ownership` only for material you author or hold rights to;
491
+ choose `authorization` with `authorizing_party` and `authorization_basis` when uploading
492
+ with permission; or choose `good_faith` with `rights_basis` explaining legitimate sharing.
493
+ The declaration is immutable. `GET /api/v1/help/corpus-create` documents the fields.
494
+
495
+ For your own documents: `createCorpus({ slug: 'my-docs', title: 'My documents', attestation_tier: 'ownership' })`.
496
+ The MCP tool accepts the same fields plus `action: 'create'`.
@@ -0,0 +1,7 @@
1
+ interface TokenConfig {
2
+ apiToken: string;
3
+ apiTokenFile?: string;
4
+ }
5
+ /** Explicit environment token > environment file > saved configuration. */
6
+ export declare function resolveApiToken(config: TokenConfig, allowInvalidTokenFile?: boolean): TokenConfig;
7
+ export {};
@@ -0,0 +1,42 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { isAbsolute } from 'node:path';
3
+ // Capture credentials for this process; rotating a token file requires a restart.
4
+ const envToken = process.env.WIRE_API_TOKEN;
5
+ const envTokenFile = process.env.WIRE_API_TOKEN_FILE;
6
+ const fileTokens = new Map();
7
+ /** Explicit environment token > environment file > saved configuration. */
8
+ export function resolveApiToken(config, allowInvalidTokenFile = false) {
9
+ if (envToken)
10
+ return { apiToken: envToken, apiTokenFile: undefined };
11
+ const path = envTokenFile ?? config.apiTokenFile;
12
+ if (path === undefined)
13
+ return { apiToken: config.apiToken, apiTokenFile: undefined };
14
+ let token = fileTokens.get(path);
15
+ if (token === undefined) {
16
+ if (!isAbsolute(path)) {
17
+ token = new Error('WIRE_API_TOKEN_FILE must be an absolute path to a file containing only your token');
18
+ }
19
+ else {
20
+ try {
21
+ token = readFileSync(path, 'utf8').trim();
22
+ if (!token)
23
+ token = new Error('The file referenced by WIRE_API_TOKEN_FILE is empty; it must contain only your token');
24
+ }
25
+ catch (error) {
26
+ // Only expose the OS code, never file contents or an arbitrary error message.
27
+ const code = error.code ?? 'read failed';
28
+ token = new Error(`Cannot read the file referenced by WIRE_API_TOKEN_FILE (${code}); set it to an absolute path to a file containing only your token`);
29
+ }
30
+ }
31
+ fileTokens.set(path, token);
32
+ }
33
+ if (token instanceof Error) {
34
+ // Public help/discovery may proceed anonymously; authenticated callers retry
35
+ // this same cached result and receive the error without exposing the value.
36
+ if (allowInvalidTokenFile)
37
+ return { apiToken: '', apiTokenFile: path };
38
+ throw token;
39
+ }
40
+ return { apiToken: token, apiTokenFile: path };
41
+ }
42
+ //# sourceMappingURL=api-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-token.js","sourceRoot":"","sources":["../src/api-token.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAOvC,kFAAkF;AAClF,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;AAC5C,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;AACrD,MAAM,UAAU,GAAG,IAAI,GAAG,EAA0B,CAAC;AAErD,2EAA2E;AAC3E,MAAM,UAAU,eAAe,CAAC,MAAmB,EAAE,qBAAqB,GAAG,KAAK;IAChF,IAAI,QAAQ;QAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;IACrE,MAAM,IAAI,GAAG,YAAY,IAAI,MAAM,CAAC,YAAY,CAAC;IACjD,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;IAEtF,IAAI,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACtB,KAAK,GAAG,IAAI,KAAK,CAAC,mFAAmF,CAAC,CAAC;QACzG,CAAC;aAAM,CAAC;YACN,IAAI,CAAC;gBACH,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;gBAC1C,IAAI,CAAC,KAAK;oBAAE,KAAK,GAAG,IAAI,KAAK,CAAC,sFAAsF,CAAC,CAAC;YACxH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,8EAA8E;gBAC9E,MAAM,IAAI,GAAI,KAA+B,CAAC,IAAI,IAAI,aAAa,CAAC;gBACpE,KAAK,GAAG,IAAI,KAAK,CAAC,2DAA2D,IAAI,oEAAoE,CAAC,CAAC;YACzJ,CAAC;QACH,CAAC;QACD,UAAU,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC9B,CAAC;IACD,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,6EAA6E;QAC7E,4EAA4E;QAC5E,IAAI,qBAAqB;YAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;QACvE,MAAM,KAAK,CAAC;IACd,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;AACjD,CAAC"}
package/dist/api.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { CreditAmount } from './contracts/credit-amount.js';
2
2
  import type { WireNodeConfig } from './config.js';
3
3
  import type { MarketNativeDocsIngestMode, RemoteDocument } from './sync/types.js';
4
+ export declare function normalizeWireApiUrl(value: string): string;
4
5
  export declare class RegistrationError extends Error {
5
6
  readonly status: number;
6
7
  constructor(status: number, message: string);
@@ -21,6 +22,7 @@ export interface MeshScopeParams {
21
22
  circle_id?: string;
22
23
  }
23
24
  export interface QueryParams {
25
+ count?: 'exact';
24
26
  text?: string;
25
27
  types?: string | string[];
26
28
  type?: string;
@@ -39,6 +41,7 @@ export interface QueryParams {
39
41
  }
40
42
  export interface ContributeParams {
41
43
  type: string;
44
+ original?: boolean;
42
45
  contribution_type?: 'mechanical' | 'intelligence';
43
46
  title: string;
44
47
  body: string;
@@ -103,6 +106,9 @@ export interface BrowseParams {
103
106
  include_definition?: boolean;
104
107
  definition_purpose?: string;
105
108
  definition_scope?: string;
109
+ cursor?: string;
110
+ limit?: number;
111
+ offset?: number;
106
112
  }
107
113
  export interface FlagParams {
108
114
  item_id: string;
@@ -293,7 +299,14 @@ export declare const __wireApiTestInternals: {
293
299
  parseSseBlock: typeof parseSseBlock;
294
300
  };
295
301
  declare function parseSseBlock(block: string): WireEventStreamMessage | null;
302
+ /** Keep actionable server refusals intact across MCP and terminal transports. */
303
+ export declare class WireApiRefusal extends Error {
304
+ readonly status: number;
305
+ readonly body: Record<string, unknown>;
306
+ constructor(status: number, body: Record<string, unknown>);
307
+ }
296
308
  export declare class WireAPI {
309
+ private httpFetch?;
297
310
  claims(params: Record<string, unknown>): Promise<unknown>;
298
311
  private baseUrl;
299
312
  private token;
@@ -303,7 +316,7 @@ export declare class WireAPI {
303
316
  private localNodeEnabled;
304
317
  private localNodeToken;
305
318
  private localContributionRouteStatus;
306
- constructor(config: WireNodeConfig, threadId?: string);
319
+ constructor(config: WireNodeConfig, threadId?: string, httpFetch?: typeof fetch | undefined);
307
320
  /** Set or update the thread ID (used for mesh coordination) */
308
321
  setThreadId(id: string): void;
309
322
  request<T>(method: string, path: string, body?: unknown, timeoutMs?: number): Promise<T>;
@@ -745,7 +758,7 @@ export declare class WireAPI {
745
758
  material_class?: string;
746
759
  visibility?: string;
747
760
  themes?: string[];
748
- attestation_tier?: string;
761
+ attestation_tier: 'ownership' | 'authorization' | 'good_faith';
749
762
  authorizing_party?: string;
750
763
  authorization_basis?: string;
751
764
  rights_basis?: string;
package/dist/api.js CHANGED
@@ -1,3 +1,15 @@
1
+ const normalizedUrlNotices = new Set();
2
+ export function normalizeWireApiUrl(value) {
3
+ const url = new URL(value);
4
+ if (url.pathname === '/' || !url.pathname) {
5
+ url.pathname = '/api/v1';
6
+ if (!normalizedUrlNotices.has(value)) {
7
+ console.error('[wire] WIRE_API_URL has no API path; appended /api/v1.');
8
+ normalizedUrlNotices.add(value);
9
+ }
10
+ }
11
+ return url.toString().replace(/\/+$/, '');
12
+ }
1
13
  const DEFAULT_TRUSTED_CHAIN_TIMEOUT_MS = 600_000;
2
14
  const TRUSTED_CHAIN_CLIENT_GRACE_MS = 5_000;
3
15
  const DEFAULT_WIRE_SYNC_TIMEOUT_MS = DEFAULT_TRUSTED_CHAIN_TIMEOUT_MS + TRUSTED_CHAIN_CLIENT_GRACE_MS;
@@ -145,7 +157,18 @@ function parseSseBlock(block) {
145
157
  return { event, data: rawData };
146
158
  }
147
159
  }
160
+ /** Keep actionable server refusals intact across MCP and terminal transports. */
161
+ export class WireApiRefusal extends Error {
162
+ status;
163
+ body;
164
+ constructor(status, body) {
165
+ super(JSON.stringify(body));
166
+ this.status = status;
167
+ this.body = body;
168
+ }
169
+ }
148
170
  export class WireAPI {
171
+ httpFetch;
149
172
  async claims(params) {
150
173
  const { action, claim_id } = params;
151
174
  if (action === 'get' || action === 'list') {
@@ -161,8 +184,9 @@ export class WireAPI {
161
184
  localNodeEnabled;
162
185
  localNodeToken;
163
186
  localContributionRouteStatus;
164
- constructor(config, threadId) {
165
- this.baseUrl = config.apiUrl;
187
+ constructor(config, threadId, httpFetch) {
188
+ this.httpFetch = httpFetch;
189
+ this.baseUrl = normalizeWireApiUrl(config.apiUrl);
166
190
  this.token = config.apiToken;
167
191
  this.nodeId = config.nodeId;
168
192
  this.threadId = threadId || null;
@@ -184,7 +208,7 @@ export class WireAPI {
184
208
  if (this.threadId) {
185
209
  headers['X-Wire-Thread'] = this.threadId;
186
210
  }
187
- const res = await fetch(url, {
211
+ const res = await (this.httpFetch ?? fetch)(url, {
188
212
  method,
189
213
  headers,
190
214
  body: body ? JSON.stringify(body) : undefined,
@@ -192,6 +216,8 @@ export class WireAPI {
192
216
  });
193
217
  if (!res.ok) {
194
218
  const err = await res.json().catch(() => ({ error: res.statusText }));
219
+ if (err.code === 'confirm_original')
220
+ throw new WireApiRefusal(res.status, err);
195
221
  throw new Error(`API ${method} ${path} failed (${res.status}): ${err.error || res.statusText}`);
196
222
  }
197
223
  return res.json();
@@ -205,7 +231,7 @@ export class WireAPI {
205
231
  if (this.threadId) {
206
232
  headers['X-Wire-Thread'] = this.threadId;
207
233
  }
208
- const res = await fetch(url, {
234
+ const res = await (this.httpFetch ?? fetch)(url, {
209
235
  method,
210
236
  headers,
211
237
  body: body ? JSON.stringify(body) : undefined,
@@ -256,7 +282,7 @@ export class WireAPI {
256
282
  async registerAgent(params) {
257
283
  // Registration doesn't require auth — use a raw fetch
258
284
  const url = `${this.baseUrl}/register`;
259
- const res = await fetch(url, {
285
+ const res = await (this.httpFetch ?? fetch)(url, {
260
286
  method: 'POST',
261
287
  headers: { 'Content-Type': 'application/json' },
262
288
  body: JSON.stringify(params),
@@ -348,6 +374,8 @@ export class WireAPI {
348
374
  return results;
349
375
  }
350
376
  async browse(params) {
377
+ if (params.cursor && (params.item_id || params.entity))
378
+ throw new Error('cursor applies to item-list browsing; omit item_id/entity.');
351
379
  if (params.item_id) {
352
380
  if (params.include_definition) {
353
381
  const qs = new URLSearchParams({ mode: 'inspect', include_definition: 'true' });
@@ -365,10 +393,16 @@ export class WireAPI {
365
393
  qs.set('arc', params.arc);
366
394
  return this.request('GET', `/wire/entities?${qs.toString()}`);
367
395
  }
368
- if (params.arc) {
369
- return this.request('GET', `/wire/items?arc=${encodeURIComponent(params.arc)}`);
370
- }
371
- return this.request('GET', '/wire/items');
396
+ const qs = new URLSearchParams();
397
+ if (params.arc)
398
+ qs.set('arc', params.arc);
399
+ if (params.cursor)
400
+ qs.set('cursor', params.cursor);
401
+ if (params.limit !== undefined)
402
+ qs.set('limit', String(params.limit));
403
+ if (params.offset !== undefined)
404
+ qs.set('offset', String(params.offset));
405
+ return this.request('GET', `/wire/items${qs.size ? '?' + qs : ''}`);
372
406
  }
373
407
  async flag(params) {
374
408
  return this.request('POST', '/flag', params);
@@ -422,7 +456,7 @@ export class WireAPI {
422
456
  if (this.localContributionRouteStatus)
423
457
  return this.localContributionRouteStatus;
424
458
  try {
425
- const health = await fetch(`${this.localNodeUrl}/health`, {
459
+ const health = await (this.httpFetch ?? fetch)(`${this.localNodeUrl}/health`, {
426
460
  method: 'GET',
427
461
  signal: AbortSignal.timeout(2_000),
428
462
  });
@@ -430,7 +464,7 @@ export class WireAPI {
430
464
  this.localContributionRouteStatus = 'unavailable';
431
465
  return this.localContributionRouteStatus;
432
466
  }
433
- const probe = await fetch(`${this.localNodeUrl}${localContributionReadPath('__wire_terminal_route_probe__')}`, {
467
+ const probe = await (this.httpFetch ?? fetch)(`${this.localNodeUrl}${localContributionReadPath('__wire_terminal_route_probe__')}`, {
434
468
  method: 'GET',
435
469
  headers: this.localHeaders(),
436
470
  signal: AbortSignal.timeout(2_000),
@@ -464,7 +498,7 @@ export class WireAPI {
464
498
  return null;
465
499
  const path = localContributionReadPath(identifier);
466
500
  try {
467
- const res = await fetch(`${this.localNodeUrl}${path}`, {
501
+ const res = await (this.httpFetch ?? fetch)(`${this.localNodeUrl}${path}`, {
468
502
  method: 'GET',
469
503
  headers: this.localHeaders(),
470
504
  signal: AbortSignal.timeout(10_000),
@@ -498,7 +532,7 @@ export class WireAPI {
498
532
  * parsed JSON body on success, throws a descriptive error on non-2xx.
499
533
  */
500
534
  async localNodeFetch(method, path, body, timeoutMs = 15_000) {
501
- const res = await fetch(`${this.localNodeUrl}${path}`, {
535
+ const res = await (this.httpFetch ?? fetch)(`${this.localNodeUrl}${path}`, {
502
536
  method,
503
537
  headers: this.localHeaders(),
504
538
  body: body !== undefined ? JSON.stringify(body) : undefined,
@@ -884,7 +918,7 @@ export class WireAPI {
884
918
  if (this.threadId) {
885
919
  headers['X-Wire-Thread'] = this.threadId;
886
920
  }
887
- const res = await fetch(url.toString(), {
921
+ const res = await (this.httpFetch ?? fetch)(url.toString(), {
888
922
  method: 'GET',
889
923
  headers,
890
924
  signal: options.signal,
@@ -1072,7 +1106,7 @@ export class WireAPI {
1072
1106
  async executeSupersession(targetId, newContributionId, allowTypeChange) {
1073
1107
  // allow_type_change forwards to the REST endpoint (which already honors it) so a CROSS-TYPE
1074
1108
  // supersession is reachable via the CLI/MCP, not only via raw REST (WI playful/170/177).
1075
- const body = { new_contribution_id: newContributionId };
1109
+ const body = { contribution_id: newContributionId };
1076
1110
  if (allowTypeChange)
1077
1111
  body.allow_type_change = true;
1078
1112
  return this.request('POST', `/wire/contributions/${encodeURIComponent(targetId)}/supersede`, body);
@@ -1251,7 +1285,7 @@ export class WireAPI {
1251
1285
  if (this.threadId) {
1252
1286
  headers['X-Wire-Thread'] = this.threadId;
1253
1287
  }
1254
- const res = await fetch(url, { method: 'GET', headers, signal: AbortSignal.timeout(30_000) });
1288
+ const res = await (this.httpFetch ?? fetch)(url, { method: 'GET', headers, signal: AbortSignal.timeout(30_000) });
1255
1289
  if (!res.ok) {
1256
1290
  const err = await res.json().catch(() => ({ error: res.statusText }));
1257
1291
  throw new Error(`API GET /wire/documents/${documentId}/body failed (${res.status}): ${err.error || res.statusText}`);
@@ -1314,7 +1348,7 @@ export class WireAPI {
1314
1348
  const body = { email, device_fingerprint: deviceFingerprint };
1315
1349
  if (agentMessage)
1316
1350
  body.agent_message = agentMessage;
1317
- const res = await fetch(url, {
1351
+ const res = await (this.httpFetch ?? fetch)(url, {
1318
1352
  method: 'POST',
1319
1353
  headers: { 'Content-Type': 'application/json' },
1320
1354
  body: JSON.stringify(body),
@@ -1330,7 +1364,7 @@ export class WireAPI {
1330
1364
  async verifyDeviceCode(email, deviceFingerprint, code) {
1331
1365
  // No auth required — raw fetch
1332
1366
  const url = `${this.baseUrl}/wire/device-auth`;
1333
- const res = await fetch(url, {
1367
+ const res = await (this.httpFetch ?? fetch)(url, {
1334
1368
  method: 'PUT',
1335
1369
  headers: { 'Content-Type': 'application/json' },
1336
1370
  body: JSON.stringify({ email, device_fingerprint: deviceFingerprint, code }),
@@ -1348,7 +1382,7 @@ export class WireAPI {
1348
1382
  url.searchParams.set('harness', harness);
1349
1383
  if (hintName)
1350
1384
  url.searchParams.set('hint_name', hintName);
1351
- const res = await fetch(url.toString(), {
1385
+ const res = await (this.httpFetch ?? fetch)(url.toString(), {
1352
1386
  headers: { 'X-Wire-Device-Secret': deviceSecret },
1353
1387
  signal: AbortSignal.timeout(15_000),
1354
1388
  });
@@ -1360,7 +1394,7 @@ export class WireAPI {
1360
1394
  return json.data || json;
1361
1395
  }
1362
1396
  async resumeAgent(agentName, deviceSecret, nonce) {
1363
- const res = await fetch(`${this.baseUrl}/wire/agent/resume`, {
1397
+ const res = await (this.httpFetch ?? fetch)(`${this.baseUrl}/wire/agent/resume`, {
1364
1398
  method: 'POST',
1365
1399
  headers: {
1366
1400
  'Content-Type': 'application/json',