clanker-wallet 0.4.2 → 0.6.0

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.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Bech32 / Bech32m decoding — a port of the BIP-173 / BIP-350 reference
3
+ * implementation (github.com/sipa/bech32 ref/python/segwit_addr.py).
4
+ * Used to check transfer recipients: bitcoin segwit addresses and chia
5
+ * puzzle-hash addresses (bech32m, hrp xch / txch).
6
+ */
7
+ export type Bech32Encoding = 'bech32' | 'bech32m';
8
+ /** validate a bech32/bech32m string; returns its hrp, data (checksum stripped) and encoding */
9
+ export declare function bech32Decode(bech: string): {
10
+ hrp: string;
11
+ data: number[];
12
+ encoding: Bech32Encoding;
13
+ } | null;
14
+ /** general power-of-2 base conversion */
15
+ export declare function convertBits(data: number[], fromBits: number, toBits: number, pad: boolean): number[] | null;
16
+ /** decode a segwit address for the given hrp ('bc' mainnet, 'tb' testnet/signet) */
17
+ export declare function decodeSegwitAddress(hrp: string, addr: string): {
18
+ version: number;
19
+ program: number[];
20
+ } | null;
21
+ /**
22
+ * a segwit address a wallet can pay today: v0 p2wpkh/p2wsh (20/32 bytes) or v1 p2tr (32 bytes).
23
+ * other versions and v1 lengths decode but are unspendable or anyone-can-spend, so transfers refuse them
24
+ */
25
+ export declare function isStandardSegwitAddress(hrp: string, addr: string): boolean;
26
+ /** decode a chia address (bech32m of a 32-byte puzzle hash) for the given hrp ('xch' / 'txch') */
27
+ export declare function decodeChiaAddress(hrp: string, addr: string): number[] | null;
package/dist/bech32.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Bech32 / Bech32m decoding — a port of the BIP-173 / BIP-350 reference
3
+ * implementation (github.com/sipa/bech32 ref/python/segwit_addr.py).
4
+ * Used to check transfer recipients: bitcoin segwit addresses and chia
5
+ * puzzle-hash addresses (bech32m, hrp xch / txch).
6
+ */
7
+ const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
8
+ const BECH32M_CONST = 0x2bc830a3;
9
+ const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
10
+ function polymod(values) {
11
+ let chk = 1;
12
+ for (const value of values) {
13
+ const top = chk >>> 25;
14
+ chk = (((chk & 0x1ffffff) << 5) ^ value) >>> 0;
15
+ for (let i = 0; i < 5; i++) {
16
+ if ((top >>> i) & 1)
17
+ chk = (chk ^ GENERATOR[i]) >>> 0;
18
+ }
19
+ }
20
+ return chk;
21
+ }
22
+ function hrpExpand(hrp) {
23
+ const codes = Array.from(hrp, (c) => c.charCodeAt(0));
24
+ return [...codes.map((c) => c >> 5), 0, ...codes.map((c) => c & 31)];
25
+ }
26
+ /** validate a bech32/bech32m string; returns its hrp, data (checksum stripped) and encoding */
27
+ export function bech32Decode(bech) {
28
+ for (const c of bech) {
29
+ const code = c.charCodeAt(0);
30
+ if (code < 33 || code > 126)
31
+ return null;
32
+ }
33
+ if (bech.toLowerCase() !== bech && bech.toUpperCase() !== bech)
34
+ return null;
35
+ const lower = bech.toLowerCase();
36
+ const pos = lower.lastIndexOf('1');
37
+ if (pos < 1 || pos + 7 > lower.length || lower.length > 90)
38
+ return null;
39
+ const data = [];
40
+ for (const c of lower.slice(pos + 1)) {
41
+ const d = CHARSET.indexOf(c);
42
+ if (d === -1)
43
+ return null;
44
+ data.push(d);
45
+ }
46
+ const hrp = lower.slice(0, pos);
47
+ const check = polymod([...hrpExpand(hrp), ...data]);
48
+ const encoding = check === 1 ? 'bech32' : check === BECH32M_CONST ? 'bech32m' : null;
49
+ if (!encoding)
50
+ return null;
51
+ return { hrp, data: data.slice(0, -6), encoding };
52
+ }
53
+ /** general power-of-2 base conversion */
54
+ export function convertBits(data, fromBits, toBits, pad) {
55
+ let acc = 0;
56
+ let bits = 0;
57
+ const ret = [];
58
+ const maxv = (1 << toBits) - 1;
59
+ const maxAcc = (1 << (fromBits + toBits - 1)) - 1;
60
+ for (const value of data) {
61
+ if (value < 0 || value >> fromBits)
62
+ return null;
63
+ acc = ((acc << fromBits) | value) & maxAcc;
64
+ bits += fromBits;
65
+ while (bits >= toBits) {
66
+ bits -= toBits;
67
+ ret.push((acc >> bits) & maxv);
68
+ }
69
+ }
70
+ if (pad) {
71
+ if (bits)
72
+ ret.push((acc << (toBits - bits)) & maxv);
73
+ }
74
+ else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv)) {
75
+ return null;
76
+ }
77
+ return ret;
78
+ }
79
+ /** decode a segwit address for the given hrp ('bc' mainnet, 'tb' testnet/signet) */
80
+ export function decodeSegwitAddress(hrp, addr) {
81
+ const decoded = bech32Decode(addr);
82
+ if (!decoded || decoded.hrp !== hrp || decoded.data.length === 0)
83
+ return null;
84
+ const [version, ...rest] = decoded.data;
85
+ const program = convertBits(rest, 5, 8, false);
86
+ if (!program || program.length < 2 || program.length > 40)
87
+ return null;
88
+ if (version > 16)
89
+ return null;
90
+ if (version === 0 && program.length !== 20 && program.length !== 32)
91
+ return null;
92
+ if ((version === 0) !== (decoded.encoding === 'bech32'))
93
+ return null;
94
+ return { version, program };
95
+ }
96
+ /**
97
+ * a segwit address a wallet can pay today: v0 p2wpkh/p2wsh (20/32 bytes) or v1 p2tr (32 bytes).
98
+ * other versions and v1 lengths decode but are unspendable or anyone-can-spend, so transfers refuse them
99
+ */
100
+ export function isStandardSegwitAddress(hrp, addr) {
101
+ const decoded = decodeSegwitAddress(hrp, addr);
102
+ return !!decoded && (decoded.version === 0 || (decoded.version === 1 && decoded.program.length === 32));
103
+ }
104
+ /** decode a chia address (bech32m of a 32-byte puzzle hash) for the given hrp ('xch' / 'txch') */
105
+ export function decodeChiaAddress(hrp, addr) {
106
+ const decoded = bech32Decode(addr);
107
+ if (!decoded || decoded.hrp !== hrp || decoded.encoding !== 'bech32m')
108
+ return null;
109
+ const puzzleHash = convertBits(decoded.data, 5, 8, false);
110
+ return puzzleHash && puzzleHash.length === 32 ? puzzleHash : null;
111
+ }
package/dist/client.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import { TypedEmitter } from './events.js';
2
- import type { PairingData, RequestTxOptions } from './types.js';
2
+ import type { PairingData, RequestTransferOptions, RequestTxOptions } from './types.js';
3
+ /** the relay answered with an error frame (rate limited, message too large, ...): it refused the request instead of passing it on */
4
+ export declare class RelayError extends Error {
5
+ constructor(message: string);
6
+ }
3
7
  export interface ClankerWalletOptions {
4
8
  /** agent display name (informational) */
5
9
  agentName?: string;
@@ -27,4 +31,13 @@ export declare class ClankerWallet extends TypedEmitter {
27
31
  * throws on rejection, timeout, or connection error.
28
32
  */
29
33
  requestTx(options: RequestTxOptions): Promise<string>;
34
+ /**
35
+ * send a bitcoin / chia transfer to the paired human and wait for their response.
36
+ * the family comes from the pairing; the human's wallet builds, signs and broadcasts
37
+ * the send. resolves with the txid on approval.
38
+ * throws InvalidTxError (before anything is sent), or on rejection, timeout, or connection error.
39
+ */
40
+ requestTransfer(options: RequestTransferOptions): Promise<string>;
41
+ /** encrypt a signed request to the paired human, send it through the relay, and wait for the answer */
42
+ private send;
30
43
  }
package/dist/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex } from './crypto.js';
2
2
  import { TypedEmitter } from './events.js';
3
- import { validateTx } from './validate.js';
3
+ import { InvalidTxError, validateTransfer, validateTx } from './validate.js';
4
4
  const DEFAULT_TIMEOUT_MS = 3_600_000; // 1 hour
5
5
  const UNREACHABLE_HINT = 'if you run in a sandbox or behind a firewall, allow this host; otherwise the relay may be down or rate limiting, retry in a few seconds';
6
6
  /** node's WebSocket ignores HTTPS_PROXY unless NODE_USE_ENV_PROXY is set at startup, and sandboxes (claude code) only let traffic out through that proxy */
@@ -12,6 +12,13 @@ function proxyHint() {
12
12
  ? '; HTTPS_PROXY is set but node ignores it, rerun with NODE_USE_ENV_PROXY=1 (node 24+)'
13
13
  : '';
14
14
  }
15
+ /** the relay answered with an error frame (rate limited, message too large, ...): it refused the request instead of passing it on */
16
+ export class RelayError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'RelayError';
20
+ }
21
+ }
15
22
  export class ClankerWallet extends TypedEmitter {
16
23
  publicKey;
17
24
  secretKey;
@@ -55,6 +62,8 @@ export class ClankerWallet extends TypedEmitter {
55
62
  relayUrl: data.relay,
56
63
  humanWallet: data.wallet || null,
57
64
  mode: data.mode || 'encrypted',
65
+ chainFamily: data.chain_family || null,
66
+ chainNetwork: data.chain_network || null,
58
67
  };
59
68
  this.emit('paired', { relay: data.relay, wallet: data.wallet || '' });
60
69
  return data;
@@ -68,13 +77,16 @@ export class ClankerWallet extends TypedEmitter {
68
77
  if (!this.pairing) {
69
78
  throw new Error('not paired — call pair() first');
70
79
  }
80
+ // a bitcoin or chia pairing takes transfers only: its wallet can't sign an evm tx
81
+ const { chainFamily } = this.pairing;
82
+ if (chainFamily && chainFamily !== 'evm') {
83
+ throw new InvalidTxError('chain_family', `this pairing is for ${chainFamily}: use requestTransfer for bitcoin or chia`);
84
+ }
71
85
  // reject malformed requests before anything reaches the relay
72
86
  const { chain_id, transaction } = validateTx(options.chain_id, options.transaction);
73
- const { humanPubkey, humanPubkeyHex, relayUrl, humanWallet, mode } = this.pairing;
74
- const timeoutMs = options.timeout_ms ?? DEFAULT_TIMEOUT_MS;
75
- const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
87
+ const { humanWallet, mode } = this.pairing;
76
88
  const txRequest = {
77
- request_id: requestId,
89
+ request_id: newRequestId(),
78
90
  agent_id: this.agentId,
79
91
  timestamp: new Date().toISOString(),
80
92
  chain_id,
@@ -83,28 +95,90 @@ export class ClankerWallet extends TypedEmitter {
83
95
  signature: '', // set below after signing
84
96
  };
85
97
  txRequest.signature = signMessage(JSON.stringify(txRequest), this.signingSecretKey);
98
+ // managed mode: relay gets plaintext tx params so it can simulate
99
+ const simulate = mode === 'managed'
100
+ ? { from: humanWallet || '', to: transaction.to, value: transaction.value, data: transaction.data, chainId: chain_id }
101
+ : undefined;
102
+ return this.send(txRequest, options, simulate);
103
+ }
104
+ /**
105
+ * send a bitcoin / chia transfer to the paired human and wait for their response.
106
+ * the family comes from the pairing; the human's wallet builds, signs and broadcasts
107
+ * the send. resolves with the txid on approval.
108
+ * throws InvalidTxError (before anything is sent), or on rejection, timeout, or connection error.
109
+ */
110
+ async requestTransfer(options) {
111
+ if (!this.pairing) {
112
+ throw new Error('not paired — call pair() first');
113
+ }
114
+ const { chainFamily, chainNetwork } = this.pairing;
115
+ // reject malformed requests before anything reaches the relay
116
+ const { chain_family, chain_network, transfer } = validateTransfer(chainFamily, options.chain_network, options.transfer);
117
+ if (chainNetwork && chainNetwork !== chain_network) {
118
+ throw new InvalidTxError('chain_network', `this pairing is for ${chain_family} ${chainNetwork}, got "${chain_network}"`);
119
+ }
120
+ const request = {
121
+ request_id: newRequestId(),
122
+ agent_id: this.agentId,
123
+ timestamp: new Date().toISOString(),
124
+ chain_family,
125
+ chain_network,
126
+ transfer,
127
+ context: options.context,
128
+ signature: '', // set below after signing
129
+ };
130
+ request.signature = signMessage(JSON.stringify(request), this.signingSecretKey);
131
+ // the relay simulator is evm-only: no simulate object for transfers, even on a managed pairing
132
+ return this.send(request, options);
133
+ }
134
+ /** encrypt a signed request to the paired human, send it through the relay, and wait for the answer */
135
+ send(request, options, simulate) {
136
+ const { humanPubkey, humanPubkeyHex, relayUrl } = this.pairing;
137
+ const requestId = request.request_id;
138
+ const timeoutMs = options.timeout_ms ?? DEFAULT_TIMEOUT_MS;
139
+ const { join_timeout_ms: joinTimeoutMs, signal } = options;
86
140
  return new Promise((resolve, reject) => {
87
141
  let ws;
88
142
  let timer;
143
+ let joinTimer;
89
144
  let settled = false;
145
+ const onAbort = () => {
146
+ if (!settled)
147
+ fail(`request ${requestId} cancelled`);
148
+ };
90
149
  const cleanup = () => {
91
150
  settled = true;
92
151
  clearTimeout(timer);
152
+ clearTimeout(joinTimer);
153
+ signal?.removeEventListener('abort', onAbort);
93
154
  if (ws && ws.readyState <= WebSocket.OPEN) {
94
155
  ws.close();
95
156
  }
96
157
  };
97
- const fail = (message) => {
158
+ const fail = (error) => {
159
+ const err = typeof error === 'string' ? new Error(error) : error;
98
160
  cleanup();
99
- this.emit('error', { message });
100
- reject(new Error(message));
161
+ this.emit('error', { message: err.message });
162
+ reject(err);
101
163
  };
164
+ if (signal?.aborted) {
165
+ onAbort();
166
+ return;
167
+ }
168
+ signal?.addEventListener('abort', onAbort);
102
169
  timer = setTimeout(() => {
103
170
  if (!settled) {
104
171
  fail(`request ${requestId} timed out after ${timeoutMs}ms`);
105
172
  }
106
173
  }, timeoutMs);
107
174
  this.emit('connecting', { relay: relayUrl });
175
+ // the request goes out only once joined, so failing before then guarantees nothing was sent
176
+ if (joinTimeoutMs !== undefined) {
177
+ joinTimer = setTimeout(() => {
178
+ if (!settled)
179
+ fail(`relay ${relayUrl} did not answer join within ${joinTimeoutMs}ms; request ${requestId} was not sent`);
180
+ }, joinTimeoutMs);
181
+ }
108
182
  try {
109
183
  ws = new WebSocket(relayUrl);
110
184
  }
@@ -138,8 +212,9 @@ export class ClankerWallet extends TypedEmitter {
138
212
  return;
139
213
  }
140
214
  if (msg.type === 'joined') {
215
+ clearTimeout(joinTimer);
141
216
  this.emit('connected', { room: humanPubkeyHex });
142
- const ciphertext = encryptMessage(JSON.stringify(txRequest), humanPubkey, this.secretKey);
217
+ const ciphertext = encryptMessage(JSON.stringify(request), humanPubkey, this.secretKey);
143
218
  const envelope = {
144
219
  sender_pubkey: this.agentPubkeyHex,
145
220
  ciphertext,
@@ -150,16 +225,8 @@ export class ClankerWallet extends TypedEmitter {
150
225
  room: humanPubkeyHex,
151
226
  payload: JSON.stringify(envelope),
152
227
  };
153
- // managed mode: relay gets plaintext tx params so it can simulate
154
- if (mode === 'managed') {
155
- wsMsg.simulate = {
156
- from: humanWallet || '',
157
- to: transaction.to,
158
- value: transaction.value,
159
- data: transaction.data,
160
- chainId: chain_id,
161
- };
162
- }
228
+ if (simulate)
229
+ wsMsg.simulate = simulate;
163
230
  ws.send(JSON.stringify(wsMsg));
164
231
  this.emit('request_sent', { request_id: requestId });
165
232
  return;
@@ -201,9 +268,12 @@ export class ClankerWallet extends TypedEmitter {
201
268
  return;
202
269
  }
203
270
  if (msg.type === 'error') {
204
- fail(`relay error: ${msg.message}`);
271
+ fail(new RelayError(`relay error: ${msg.message}`));
205
272
  }
206
273
  });
207
274
  });
208
275
  }
209
276
  }
277
+ function newRequestId() {
278
+ return `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
279
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- export { ClankerWallet } from './client.js';
1
+ export { ClankerWallet, RelayError } from './client.js';
2
2
  export type { ClankerWalletOptions } from './client.js';
3
3
  export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
4
4
  export { TypedEmitter } from './events.js';
5
5
  export type { EventMap } from './events.js';
6
- export { InvalidTxError, isChecksumAddress, validateTx } from './validate.js';
6
+ export { InvalidTxError, isChecksumAddress, validateTransfer, validateTx } from './validate.js';
7
7
  export type { TxField } from './validate.js';
8
- export type { AgentEnvelope, PairingData, RequestTxOptions, Transaction, TxContext, TxInput, TxRequest, TxResponse, } from './types.js';
8
+ export type { AgentEnvelope, PairingData, RequestTransferOptions, RequestTxOptions, Transaction, Transfer, TransferFamily, TransferInput, TransferRequest, TxContext, TxInput, TxRequest, TxResponse, } from './types.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { ClankerWallet } from './client.js';
1
+ export { ClankerWallet, RelayError } from './client.js';
2
2
  export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
3
3
  export { TypedEmitter } from './events.js';
4
- export { InvalidTxError, isChecksumAddress, validateTx } from './validate.js';
4
+ export { InvalidTxError, isChecksumAddress, validateTransfer, validateTx } from './validate.js';
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,156 @@
1
+ import { type ClankerWallet } from './client.js';
2
+ /**
3
+ * MCP server for `clanker-wallet mcp`: newline-delimited JSON-RPC 2.0 over stdio, hand-written so
4
+ * tweetnacl stays the only runtime dependency. the cli owns stdin/stdout and the identity and pairing
5
+ * files; this module answers one line at a time and keeps each request's state in memory.
6
+ */
7
+ /** newest first; a client asking for another version gets the newest. not 2025-03-26: it requires receiving batches, which we refuse */
8
+ export declare const SUPPORTED_PROTOCOL_VERSIONS: string[];
9
+ /** get_request waits at most this long per call; assistants time tool calls out after about a minute */
10
+ export declare const MAX_WAIT_SECONDS = 50;
11
+ /** a pairing as the cli saves it in ~/.clanker-wallet/pairings/<family>.json */
12
+ export interface SavedPairing {
13
+ pubkey: string;
14
+ relay: string;
15
+ wallet: string | null;
16
+ mode: 'encrypted' | 'managed' | null;
17
+ chain_family?: string;
18
+ chain_network?: string;
19
+ }
20
+ /** the families the tools request on; chia waits until the web app shows it again */
21
+ type RequestFamily = 'evm' | 'bitcoin';
22
+ export interface McpServerOptions {
23
+ /** the package version, reported in initialize */
24
+ version: string;
25
+ identity: {
26
+ agent_id: string;
27
+ fingerprint: string;
28
+ };
29
+ /** the saved pairing for a family, or null when that family isn't paired */
30
+ loadPairing: (family: RequestFamily) => SavedPairing | null;
31
+ /** a new ClankerWallet with the agent identity, paired from `pairing` */
32
+ wallet: (pairing: SavedPairing) => ClankerWallet;
33
+ /** how long a request waits for the human (default 1 h, the relay's buffer) */
34
+ requestTtlMs?: number;
35
+ /** how long a request waits for the relay to confirm the join before failing unsent (default 20 s) */
36
+ joinTimeoutMs?: number;
37
+ }
38
+ export declare const MCP_TOOLS: ({
39
+ name: string;
40
+ description: string;
41
+ inputSchema: {
42
+ type: string;
43
+ properties: {
44
+ reason: {
45
+ type: string;
46
+ description: string;
47
+ };
48
+ urgency: {
49
+ type: string;
50
+ enum: string[];
51
+ description: string;
52
+ };
53
+ expected_outcome: {
54
+ type: string;
55
+ description: string;
56
+ };
57
+ chain_id: {
58
+ type: string;
59
+ description: string;
60
+ };
61
+ to: {
62
+ type: string;
63
+ description: string;
64
+ };
65
+ value: {
66
+ type: string;
67
+ description: string;
68
+ };
69
+ data: {
70
+ type: string;
71
+ description: string;
72
+ };
73
+ request_id?: undefined;
74
+ wait_seconds?: undefined;
75
+ };
76
+ required: string[];
77
+ };
78
+ } | {
79
+ name: string;
80
+ description: string;
81
+ inputSchema: {
82
+ type: string;
83
+ properties: {
84
+ reason: {
85
+ type: string;
86
+ description: string;
87
+ };
88
+ urgency: {
89
+ type: string;
90
+ enum: string[];
91
+ description: string;
92
+ };
93
+ expected_outcome: {
94
+ type: string;
95
+ description: string;
96
+ };
97
+ family: {
98
+ type: string;
99
+ enum: string[];
100
+ description: string;
101
+ };
102
+ network: {
103
+ type: string;
104
+ enum: string[];
105
+ description: string;
106
+ };
107
+ to: {
108
+ type: string;
109
+ description: string;
110
+ };
111
+ amount: {
112
+ type: string;
113
+ description: string;
114
+ };
115
+ request_id?: undefined;
116
+ wait_seconds?: undefined;
117
+ };
118
+ required: string[];
119
+ };
120
+ } | {
121
+ name: string;
122
+ description: string;
123
+ inputSchema: {
124
+ type: string;
125
+ properties: {
126
+ request_id: {
127
+ type: string;
128
+ description: string;
129
+ };
130
+ wait_seconds: {
131
+ type: string;
132
+ minimum: number;
133
+ maximum: number;
134
+ description: string;
135
+ };
136
+ };
137
+ required: string[];
138
+ };
139
+ } | {
140
+ name: string;
141
+ description: string;
142
+ inputSchema: {
143
+ type: string;
144
+ properties: {
145
+ request_id?: undefined;
146
+ wait_seconds?: undefined;
147
+ };
148
+ required?: undefined;
149
+ };
150
+ })[];
151
+ /** what get_request reports, in the order the docs list them */
152
+ export declare const REQUEST_STATUSES: readonly ["pending", "approved", "rejected", "failed", "unknown", "expired"];
153
+ export declare function createMcpServer(options: McpServerOptions): {
154
+ handleLine: (line: string) => Promise<string | null>;
155
+ };
156
+ export {};