@agent-wire/terminal 1.7.7 → 1.7.10

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.
Files changed (44) hide show
  1. package/README.md +519 -435
  2. package/dist/api-token.d.ts +9 -0
  3. package/dist/api-token.js +46 -0
  4. package/dist/api-token.js.map +1 -0
  5. package/dist/api.d.ts +50 -10
  6. package/dist/api.js +78 -31
  7. package/dist/api.js.map +1 -1
  8. package/dist/b107-copy.d.ts +68 -0
  9. package/dist/b107-copy.js +69 -0
  10. package/dist/b107-copy.js.map +1 -0
  11. package/dist/catalog-follow.d.ts +32 -0
  12. package/dist/catalog-follow.js +184 -0
  13. package/dist/catalog-follow.js.map +1 -0
  14. package/dist/cli-projection.js +20 -5
  15. package/dist/cli-projection.js.map +1 -1
  16. package/dist/cli.js +32 -19
  17. package/dist/cli.js.map +1 -1
  18. package/dist/config.d.ts +14 -1
  19. package/dist/config.js +54 -9
  20. package/dist/config.js.map +1 -1
  21. package/dist/mcp-server.d.ts +1 -6
  22. package/dist/mcp-server.js +161 -144
  23. package/dist/mcp-server.js.map +1 -1
  24. package/dist/read-tool-input.d.ts +15 -0
  25. package/dist/read-tool-input.js +15 -0
  26. package/dist/read-tool-input.js.map +1 -0
  27. package/dist/response-shaper.js +76 -15
  28. package/dist/response-shaper.js.map +1 -1
  29. package/dist/resume.d.ts +7 -0
  30. package/dist/resume.js +15 -0
  31. package/dist/resume.js.map +1 -0
  32. package/dist/slots.d.ts +1 -0
  33. package/dist/slots.js +1 -1
  34. package/dist/slots.js.map +1 -1
  35. package/dist/sync/diff.js +7 -1
  36. package/dist/sync/diff.js.map +1 -1
  37. package/dist/sync/pull.d.ts +1 -0
  38. package/dist/sync/pull.js +17 -9
  39. package/dist/sync/pull.js.map +1 -1
  40. package/dist/sync/types.d.ts +2 -1
  41. package/dist/tool-projection.d.ts +19 -2
  42. package/dist/tool-projection.js +544 -8
  43. package/dist/tool-projection.js.map +1 -1
  44. package/package.json +58 -58
@@ -0,0 +1,9 @@
1
+ interface TokenConfig {
2
+ apiToken: string;
3
+ apiTokenFile?: string;
4
+ }
5
+ /** Shared saved credentials are never a server's own MCP identity. */
6
+ export declare function hasExplicitApiToken(): boolean;
7
+ /** Explicit environment token > environment file > saved configuration. */
8
+ export declare function resolveApiToken(config: TokenConfig, allowInvalidTokenFile?: boolean): TokenConfig;
9
+ export {};
@@ -0,0 +1,46 @@
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
+ /** Shared saved credentials are never a server's own MCP identity. */
8
+ export function hasExplicitApiToken() {
9
+ return !!envToken || envTokenFile !== undefined;
10
+ }
11
+ /** Explicit environment token > environment file > saved configuration. */
12
+ export function resolveApiToken(config, allowInvalidTokenFile = false) {
13
+ if (envToken)
14
+ return { apiToken: envToken, apiTokenFile: undefined };
15
+ const path = envTokenFile ?? config.apiTokenFile;
16
+ if (path === undefined)
17
+ return { apiToken: config.apiToken, apiTokenFile: undefined };
18
+ let token = fileTokens.get(path);
19
+ if (token === undefined) {
20
+ if (!isAbsolute(path)) {
21
+ token = new Error('WIRE_API_TOKEN_FILE must be an absolute path to a file containing only your token');
22
+ }
23
+ else {
24
+ try {
25
+ token = readFileSync(path, 'utf8').trim();
26
+ if (!token)
27
+ token = new Error('The file referenced by WIRE_API_TOKEN_FILE is empty; it must contain only your token');
28
+ }
29
+ catch (error) {
30
+ // Only expose the OS code, never file contents or an arbitrary error message.
31
+ const code = error.code ?? 'read failed';
32
+ 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`);
33
+ }
34
+ }
35
+ fileTokens.set(path, token);
36
+ }
37
+ if (token instanceof Error) {
38
+ // Public help/discovery may proceed anonymously; authenticated callers retry
39
+ // this same cached result and receive the error without exposing the value.
40
+ if (allowInvalidTokenFile)
41
+ return { apiToken: '', apiTokenFile: path };
42
+ throw token;
43
+ }
44
+ return { apiToken: token, apiTokenFile: path };
45
+ }
46
+ //# 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,sEAAsE;AACtE,MAAM,UAAU,mBAAmB;IACjC,OAAO,CAAC,CAAC,QAAQ,IAAI,YAAY,KAAK,SAAS,CAAC;AAClD,CAAC;AAED,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;
@@ -84,6 +87,7 @@ export interface ContributeParams {
84
87
  }>;
85
88
  }
86
89
  export interface ActionCreateParams {
90
+ scope?: 'fleet' | 'unscoped';
87
91
  title: string;
88
92
  body: string;
89
93
  action_definition: Record<string, unknown>;
@@ -103,6 +107,9 @@ export interface BrowseParams {
103
107
  include_definition?: boolean;
104
108
  definition_purpose?: string;
105
109
  definition_scope?: string;
110
+ cursor?: string;
111
+ limit?: number;
112
+ offset?: number;
106
113
  }
107
114
  export interface FlagParams {
108
115
  item_id: string;
@@ -178,6 +185,15 @@ export interface PinPreview {
178
185
  derivative_count: number;
179
186
  }
180
187
  export interface RetractionResult {
188
+ deposit_fee?: CreditAmount;
189
+ deposit_refunded_amount?: CreditAmount;
190
+ deposit_fee_payers?: Array<{
191
+ operator_id: string;
192
+ refunded_amount: CreditAmount;
193
+ fee: CreditAmount;
194
+ }>;
195
+ deposit_case?: string;
196
+ deposit_message?: string | null;
181
197
  retracted: boolean;
182
198
  contribution_id: string;
183
199
  deposit_refund: CreditAmount;
@@ -293,7 +309,14 @@ export declare const __wireApiTestInternals: {
293
309
  parseSseBlock: typeof parseSseBlock;
294
310
  };
295
311
  declare function parseSseBlock(block: string): WireEventStreamMessage | null;
312
+ /** Keep actionable server refusals intact across MCP and terminal transports. */
313
+ export declare class WireApiRefusal extends Error {
314
+ readonly status: number;
315
+ readonly body: Record<string, unknown>;
316
+ constructor(status: number, body: Record<string, unknown>);
317
+ }
296
318
  export declare class WireAPI {
319
+ private httpFetch?;
297
320
  claims(params: Record<string, unknown>): Promise<unknown>;
298
321
  private baseUrl;
299
322
  private token;
@@ -303,7 +326,7 @@ export declare class WireAPI {
303
326
  private localNodeEnabled;
304
327
  private localNodeToken;
305
328
  private localContributionRouteStatus;
306
- constructor(config: WireNodeConfig, threadId?: string);
329
+ constructor(config: WireNodeConfig, threadId?: string, httpFetch?: typeof fetch | undefined);
307
330
  /** Set or update the thread ID (used for mesh coordination) */
308
331
  setThreadId(id: string): void;
309
332
  request<T>(method: string, path: string, body?: unknown, timeoutMs?: number): Promise<T>;
@@ -314,10 +337,15 @@ export declare class WireAPI {
314
337
  requestUnwrap<T>(method: string, path: string, body?: unknown, timeoutMs?: number): Promise<T>;
315
338
  unwrapEnvelopeOrPlain<T>(response: unknown): T;
316
339
  getMe(): Promise<Record<string, unknown>>;
317
- registerNode(name: string, capabilities: string[]): Promise<{
340
+ registerNode(name: string, capabilities: string[], options?: {
341
+ node_handle?: string;
342
+ app_version?: string;
343
+ }): Promise<{
318
344
  node_id: string;
319
345
  name: string;
320
346
  status: string;
347
+ registration_notice?: string;
348
+ legacy_nodes_not_resumed?: number;
321
349
  }>;
322
350
  /**
323
351
  * Register a new Wire agent (unauthenticated — no token required).
@@ -334,6 +362,8 @@ export declare class WireAPI {
334
362
  pseudo_id: string;
335
363
  agent_id: string;
336
364
  verification_status: string;
365
+ welcome_grant?: Record<string, unknown>;
366
+ pool_credits?: number;
337
367
  }>;
338
368
  pollWork(nodeId: string): Promise<{
339
369
  work: {
@@ -367,13 +397,16 @@ export declare class WireAPI {
367
397
  flag(params: FlagParams): Promise<Record<string, unknown>>;
368
398
  /**
369
399
  * Retract your own contribution (voluntary self-takedown).
370
- * Economics: full deposit refund during grace window; half refund + half forfeit + bounty clawback after grace window.
400
+ * Economics: held deposits use the age-based forfeit and cap; refunded deposits apply the same rule to each actual refund recipient. No refund leg means no fee. Bounty recovery still applies.
371
401
  */
372
402
  retract(contributionId: string): Promise<Record<string, unknown>>;
373
403
  /**
374
404
  * Access a contribution (detail endpoint). Accessing IS purchasing for priced items.
375
405
  */
376
- accessContribution(contributionId: string): Promise<Record<string, unknown>>;
406
+ accessContribution(input: string | {
407
+ contribution_id: string;
408
+ [key: string]: unknown;
409
+ }, acceptPrice?: CreditAmount): Promise<Record<string, unknown>>;
377
410
  private verifyLocalContributionReadRoute;
378
411
  private localHeaders;
379
412
  private localAuthToken;
@@ -547,11 +580,13 @@ export declare class WireAPI {
547
580
  */
548
581
  previewPin(nodeId: string, targetType: string, targetId: string): Promise<PinPreview>;
549
582
  /**
550
- * Retract a contribution as an operator during the grace window.
551
- * Requires operator-level authentication (the API token must belong
552
- * to an operator, not a regular agent).
583
+ * Retract a contribution as an operator at any age. To correct a contribution,
584
+ * supersede it. Retraction uses the current window, rates and late refund cap.
585
+ * After settlement, the same rule applies to each actual refund recipient.
586
+ * Use operator authentication or the specific author agent token;
587
+ * sibling agent tokens cannot retract another agent's contribution.
553
588
  *
554
- * Economics: full deposit refund during grace window; half refund + half forfeit + bounty clawback after grace window.
589
+ * Economics: held deposits use the age-based forfeit and cap; refunded deposits apply the same rule to each actual refund recipient. No refund leg means no fee. Bounty recovery still applies.
555
590
  *
556
591
  * @param agentId - The agent that owns the contribution
557
592
  * @param contributionId - The contribution to retract
@@ -639,6 +674,7 @@ export declare class WireAPI {
639
674
  input?: Record<string, unknown>;
640
675
  chain_id?: string;
641
676
  quote_id?: string;
677
+ idempotency_key?: string;
642
678
  max_credits?: number;
643
679
  entry_mode?: 'start' | 'direct';
644
680
  entry?: {
@@ -651,7 +687,7 @@ export declare class WireAPI {
651
687
  pollChainRun(params: {
652
688
  run_id?: string;
653
689
  run_handle_path?: string;
654
- chain: string;
690
+ chain?: string;
655
691
  include_steps?: boolean;
656
692
  }): Promise<Record<string, unknown>>;
657
693
  resolveHandlePath(handlePath: string): Promise<Record<string, unknown>>;
@@ -728,6 +764,10 @@ export declare class WireAPI {
728
764
  offset?: number;
729
765
  }): Promise<Record<string, unknown>>;
730
766
  getDocumentBody(documentId: string): Promise<string>;
767
+ downloadDocumentBody(documentId: string): Promise<{
768
+ body: string;
769
+ content_sha256: string | null;
770
+ }>;
731
771
  getDocumentGraph(documentId: string): Promise<Record<string, unknown>>;
732
772
  listDocumentSpans(documentId: string): Promise<Record<string, unknown>>;
733
773
  createDocumentSpan(documentId: string, span: {
@@ -745,7 +785,7 @@ export declare class WireAPI {
745
785
  material_class?: string;
746
786
  visibility?: string;
747
787
  themes?: string[];
748
- attestation_tier?: string;
788
+ attestation_tier: 'ownership' | 'authorization' | 'good_faith';
749
789
  authorizing_party?: string;
750
790
  authorization_basis?: string;
751
791
  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' || err.code === 'source_hash_selector_private')
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,
@@ -213,6 +239,8 @@ export class WireAPI {
213
239
  });
214
240
  if (!res.ok) {
215
241
  const err = await res.json().catch(() => ({ error: res.statusText }));
242
+ if (err.code === 'source_hash_selector_private')
243
+ throw new WireApiRefusal(res.status, err);
216
244
  const msg = err.error || res.statusText;
217
245
  if (res.status === 429 && msg.toLowerCase().includes('pending')) {
218
246
  const retryAfter = res.headers.get('Retry-After') || '60';
@@ -246,8 +274,8 @@ export class WireAPI {
246
274
  async getMe() {
247
275
  return this.unwrapEnvelopeOrPlain(await this.request('GET', '/me'));
248
276
  }
249
- async registerNode(name, capabilities) {
250
- return this.request('POST', '/node/register', { name, capabilities });
277
+ async registerNode(name, capabilities, options = {}) {
278
+ return this.request('POST', '/node/register', { name, capabilities, ...options });
251
279
  }
252
280
  /**
253
281
  * Register a new Wire agent (unauthenticated — no token required).
@@ -256,7 +284,7 @@ export class WireAPI {
256
284
  async registerAgent(params) {
257
285
  // Registration doesn't require auth — use a raw fetch
258
286
  const url = `${this.baseUrl}/register`;
259
- const res = await fetch(url, {
287
+ const res = await (this.httpFetch ?? fetch)(url, {
260
288
  method: 'POST',
261
289
  headers: { 'Content-Type': 'application/json' },
262
290
  body: JSON.stringify(params),
@@ -348,6 +376,8 @@ export class WireAPI {
348
376
  return results;
349
377
  }
350
378
  async browse(params) {
379
+ if (params.cursor && (params.item_id || params.entity))
380
+ throw new Error('cursor applies to item-list browsing; omit item_id/entity.');
351
381
  if (params.item_id) {
352
382
  if (params.include_definition) {
353
383
  const qs = new URLSearchParams({ mode: 'inspect', include_definition: 'true' });
@@ -365,17 +395,23 @@ export class WireAPI {
365
395
  qs.set('arc', params.arc);
366
396
  return this.request('GET', `/wire/entities?${qs.toString()}`);
367
397
  }
368
- if (params.arc) {
369
- return this.request('GET', `/wire/items?arc=${encodeURIComponent(params.arc)}`);
370
- }
371
- return this.request('GET', '/wire/items');
398
+ const qs = new URLSearchParams();
399
+ if (params.arc)
400
+ qs.set('arc', params.arc);
401
+ if (params.cursor)
402
+ qs.set('cursor', params.cursor);
403
+ if (params.limit !== undefined)
404
+ qs.set('limit', String(params.limit));
405
+ if (params.offset !== undefined)
406
+ qs.set('offset', String(params.offset));
407
+ return this.request('GET', `/wire/items${qs.size ? '?' + qs : ''}`);
372
408
  }
373
409
  async flag(params) {
374
410
  return this.request('POST', '/flag', params);
375
411
  }
376
412
  /**
377
413
  * Retract your own contribution (voluntary self-takedown).
378
- * Economics: full deposit refund during grace window; half refund + half forfeit + bounty clawback after grace window.
414
+ * Economics: held deposits use the age-based forfeit and cap; refunded deposits apply the same rule to each actual refund recipient. No refund leg means no fee. Bounty recovery still applies.
379
415
  */
380
416
  async retract(contributionId) {
381
417
  return this.request('POST', '/retract', { contribution_id: contributionId });
@@ -383,7 +419,9 @@ export class WireAPI {
383
419
  /**
384
420
  * Access a contribution (detail endpoint). Accessing IS purchasing for priced items.
385
421
  */
386
- async accessContribution(contributionId) {
422
+ async accessContribution(input, acceptPrice) {
423
+ const { contribution_id: contributionId, ...readParams } = typeof input === 'string'
424
+ ? { contribution_id: input, ...(acceptPrice === undefined ? {} : { accept_price: acceptPrice }) } : input;
387
425
  let identifier = contributionId;
388
426
  const localResult = await this.tryLocalContributionRead(identifier);
389
427
  if (localResult) {
@@ -412,7 +450,8 @@ export class WireAPI {
412
450
  return localResolvedResult;
413
451
  }
414
452
  }
415
- return this.request('GET', `/wire/contribution/${encodeURIComponent(identifier)}`);
453
+ const query = new URLSearchParams(Object.entries(readParams).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]));
454
+ return this.request('GET', `/wire/contribution/${encodeURIComponent(identifier)}${query.size ? `?${query}` : ''}`);
416
455
  }
417
456
  async verifyLocalContributionReadRoute() {
418
457
  if (!this.localNodeEnabled)
@@ -422,7 +461,7 @@ export class WireAPI {
422
461
  if (this.localContributionRouteStatus)
423
462
  return this.localContributionRouteStatus;
424
463
  try {
425
- const health = await fetch(`${this.localNodeUrl}/health`, {
464
+ const health = await (this.httpFetch ?? fetch)(`${this.localNodeUrl}/health`, {
426
465
  method: 'GET',
427
466
  signal: AbortSignal.timeout(2_000),
428
467
  });
@@ -430,7 +469,7 @@ export class WireAPI {
430
469
  this.localContributionRouteStatus = 'unavailable';
431
470
  return this.localContributionRouteStatus;
432
471
  }
433
- const probe = await fetch(`${this.localNodeUrl}${localContributionReadPath('__wire_terminal_route_probe__')}`, {
472
+ const probe = await (this.httpFetch ?? fetch)(`${this.localNodeUrl}${localContributionReadPath('__wire_terminal_route_probe__')}`, {
434
473
  method: 'GET',
435
474
  headers: this.localHeaders(),
436
475
  signal: AbortSignal.timeout(2_000),
@@ -464,7 +503,7 @@ export class WireAPI {
464
503
  return null;
465
504
  const path = localContributionReadPath(identifier);
466
505
  try {
467
- const res = await fetch(`${this.localNodeUrl}${path}`, {
506
+ const res = await (this.httpFetch ?? fetch)(`${this.localNodeUrl}${path}`, {
468
507
  method: 'GET',
469
508
  headers: this.localHeaders(),
470
509
  signal: AbortSignal.timeout(10_000),
@@ -498,7 +537,7 @@ export class WireAPI {
498
537
  * parsed JSON body on success, throws a descriptive error on non-2xx.
499
538
  */
500
539
  async localNodeFetch(method, path, body, timeoutMs = 15_000) {
501
- const res = await fetch(`${this.localNodeUrl}${path}`, {
540
+ const res = await (this.httpFetch ?? fetch)(`${this.localNodeUrl}${path}`, {
502
541
  method,
503
542
  headers: this.localHeaders(),
504
543
  body: body !== undefined ? JSON.stringify(body) : undefined,
@@ -796,11 +835,13 @@ export class WireAPI {
796
835
  }
797
836
  // ── Operator Methods ───────────────────────────────────────
798
837
  /**
799
- * Retract a contribution as an operator during the grace window.
800
- * Requires operator-level authentication (the API token must belong
801
- * to an operator, not a regular agent).
838
+ * Retract a contribution as an operator at any age. To correct a contribution,
839
+ * supersede it. Retraction uses the current window, rates and late refund cap.
840
+ * After settlement, the same rule applies to each actual refund recipient.
841
+ * Use operator authentication or the specific author agent token;
842
+ * sibling agent tokens cannot retract another agent's contribution.
802
843
  *
803
- * Economics: full deposit refund during grace window; half refund + half forfeit + bounty clawback after grace window.
844
+ * Economics: held deposits use the age-based forfeit and cap; refunded deposits apply the same rule to each actual refund recipient. No refund leg means no fee. Bounty recovery still applies.
804
845
  *
805
846
  * @param agentId - The agent that owns the contribution
806
847
  * @param contributionId - The contribution to retract
@@ -884,7 +925,7 @@ export class WireAPI {
884
925
  if (this.threadId) {
885
926
  headers['X-Wire-Thread'] = this.threadId;
886
927
  }
887
- const res = await fetch(url.toString(), {
928
+ const res = await (this.httpFetch ?? fetch)(url.toString(), {
888
929
  method: 'GET',
889
930
  headers,
890
931
  signal: options.signal,
@@ -923,6 +964,7 @@ export class WireAPI {
923
964
  }
924
965
  }
925
966
  finally {
967
+ await reader.cancel().catch(() => { });
926
968
  reader.releaseLock();
927
969
  }
928
970
  }
@@ -1047,7 +1089,9 @@ export class WireAPI {
1047
1089
  const runIdentifier = params.run_handle_path ?? params.run_id;
1048
1090
  if (!runIdentifier)
1049
1091
  throw new Error('pollChainRun requires run_handle_path or run_id');
1050
- const qs = new URLSearchParams({ chain: params.chain });
1092
+ const qs = new URLSearchParams();
1093
+ if (params.chain)
1094
+ qs.set('chain', params.chain);
1051
1095
  if (params.include_steps)
1052
1096
  qs.set('include_steps', 'true');
1053
1097
  return this.request('GET', `/wire/chains/runs/${encodeURIComponent(runIdentifier)}?${qs.toString()}`, undefined, 30_000);
@@ -1072,7 +1116,7 @@ export class WireAPI {
1072
1116
  async executeSupersession(targetId, newContributionId, allowTypeChange) {
1073
1117
  // allow_type_change forwards to the REST endpoint (which already honors it) so a CROSS-TYPE
1074
1118
  // supersession is reachable via the CLI/MCP, not only via raw REST (WI playful/170/177).
1075
- const body = { new_contribution_id: newContributionId };
1119
+ const body = { contribution_id: newContributionId };
1076
1120
  if (allowTypeChange)
1077
1121
  body.allow_type_change = true;
1078
1122
  return this.request('POST', `/wire/contributions/${encodeURIComponent(targetId)}/supersede`, body);
@@ -1244,6 +1288,9 @@ export class WireAPI {
1244
1288
  return this.requestUnwrap('GET', `/wire/documents${queryString ? '?' + queryString : ''}`);
1245
1289
  }
1246
1290
  async getDocumentBody(documentId) {
1291
+ return (await this.downloadDocumentBody(documentId)).body;
1292
+ }
1293
+ async downloadDocumentBody(documentId) {
1247
1294
  const url = `${this.baseUrl}/wire/documents/${documentId}/body`;
1248
1295
  const headers = {
1249
1296
  'Authorization': `Bearer ${this.token}`,
@@ -1251,12 +1298,12 @@ export class WireAPI {
1251
1298
  if (this.threadId) {
1252
1299
  headers['X-Wire-Thread'] = this.threadId;
1253
1300
  }
1254
- const res = await fetch(url, { method: 'GET', headers, signal: AbortSignal.timeout(30_000) });
1301
+ const res = await (this.httpFetch ?? fetch)(url, { method: 'GET', headers, signal: AbortSignal.timeout(30_000) });
1255
1302
  if (!res.ok) {
1256
1303
  const err = await res.json().catch(() => ({ error: res.statusText }));
1257
1304
  throw new Error(`API GET /wire/documents/${documentId}/body failed (${res.status}): ${err.error || res.statusText}`);
1258
1305
  }
1259
- return res.text();
1306
+ return { body: await res.text(), content_sha256: res.headers.get('X-Body-Hash') };
1260
1307
  }
1261
1308
  async getDocumentGraph(documentId) {
1262
1309
  return this.request('GET', `/wire/documents/${documentId}/graph`);
@@ -1314,7 +1361,7 @@ export class WireAPI {
1314
1361
  const body = { email, device_fingerprint: deviceFingerprint };
1315
1362
  if (agentMessage)
1316
1363
  body.agent_message = agentMessage;
1317
- const res = await fetch(url, {
1364
+ const res = await (this.httpFetch ?? fetch)(url, {
1318
1365
  method: 'POST',
1319
1366
  headers: { 'Content-Type': 'application/json' },
1320
1367
  body: JSON.stringify(body),
@@ -1330,7 +1377,7 @@ export class WireAPI {
1330
1377
  async verifyDeviceCode(email, deviceFingerprint, code) {
1331
1378
  // No auth required — raw fetch
1332
1379
  const url = `${this.baseUrl}/wire/device-auth`;
1333
- const res = await fetch(url, {
1380
+ const res = await (this.httpFetch ?? fetch)(url, {
1334
1381
  method: 'PUT',
1335
1382
  headers: { 'Content-Type': 'application/json' },
1336
1383
  body: JSON.stringify({ email, device_fingerprint: deviceFingerprint, code }),
@@ -1348,7 +1395,7 @@ export class WireAPI {
1348
1395
  url.searchParams.set('harness', harness);
1349
1396
  if (hintName)
1350
1397
  url.searchParams.set('hint_name', hintName);
1351
- const res = await fetch(url.toString(), {
1398
+ const res = await (this.httpFetch ?? fetch)(url.toString(), {
1352
1399
  headers: { 'X-Wire-Device-Secret': deviceSecret },
1353
1400
  signal: AbortSignal.timeout(15_000),
1354
1401
  });
@@ -1360,7 +1407,7 @@ export class WireAPI {
1360
1407
  return json.data || json;
1361
1408
  }
1362
1409
  async resumeAgent(agentName, deviceSecret, nonce) {
1363
- const res = await fetch(`${this.baseUrl}/wire/agent/resume`, {
1410
+ const res = await (this.httpFetch ?? fetch)(`${this.baseUrl}/wire/agent/resume`, {
1364
1411
  method: 'POST',
1365
1412
  headers: {
1366
1413
  'Content-Type': 'application/json',