@vultisig/cli 2.8.0 → 2.8.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/index.js +201 -53
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # @vultisig/cli
2
2
 
3
+ ## 2.8.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#869](https://github.com/vultisig/vultisig-sdk/pull/869) [`2623ca0`](https://github.com/vultisig/vultisig-sdk/commit/2623ca0dd04c5ca3c65126b6008a93c1d7559793) Thanks [@neavra](https://github.com/neavra)! - Harden agent conversation auth across every path. A revoked-but-unexpired
8
+ cached token now recovers uniformly: a new `withAuthRetry` helper does
9
+ clear→reauth→retry-once and wraps the fresh-conversation `createConversation`
10
+ (previously unguarded — it hard-threw `Authentication failed`), the resume
11
+ `getConversation`, the send-message stream, and the `agent sessions
12
+ list`/`delete` commands.
13
+
14
+ A `--session-id` resume whose 401 survives the single retry — or that fails for
15
+ any other reason — now falls back to a fresh conversation instead of throwing
16
+ uncaught, and emits a typed, non-fatal `SESSION_NOT_FOUND` signal carrying the
17
+ new conversation id so a headless caller knows prior context was dropped. In
18
+ `agent ask`, that initialize-time signal is now preserved into the first turn's
19
+ result envelope (it was previously cleared at turn start, so a stale-session
20
+ `ask --json` returned a false-success envelope instead of the promised non-zero
21
+ `SESSION_NOT_FOUND`).
22
+
23
+ Models the backend's `refresh_token`/`access_token` in `AuthTokenResponse` and
24
+ persists the refresh token in the token cache (0o600) for a future
25
+ `POST /auth/refresh` exchange; the retry path re-auths via MPC re-sign today.
26
+ The retry preserves a previously cached refresh token when the re-auth response
27
+ omits one, and the cache directory is chmod'd to 0o700 on every write (not only
28
+ on create) so a pre-existing `~/.vultisig` can't retain looser perms on upgrade.
29
+
30
+ Adds the first `auth.ts` unit tests (EIP-191 hash cross-checked against viem,
31
+ DER→65-byte formatting, signing-retry classification) plus session
32
+ auth-retry/fallback coverage.
33
+
34
+ - Updated dependencies [[`0dc1620`](https://github.com/vultisig/vultisig-sdk/commit/0dc16206bedcdde8832a068b15383565a6b98896)]:
35
+ - @vultisig/sdk@2.8.2
36
+
37
+ ## 2.8.1
38
+
39
+ ### Patch Changes
40
+
41
+ - [#867](https://github.com/vultisig/vultisig-sdk/pull/867) [`ddd08af`](https://github.com/vultisig/vultisig-sdk/commit/ddd08af883a1b2ee2f72dac4d406782de9090672) Thanks [@neavra](https://github.com/neavra)! - Agent: poll for final on-chain confirmation after broadcasting a signed tx
42
+ (audit F1). A `pending` `tx_status` only means "broadcast accepted" — the tx can
43
+ still revert, expire, or be dropped. After broadcast the session now polls
44
+ `vault.getTxStatus` until the tx reaches a final state and emits `confirmed` /
45
+ `failed`, or `timeout` when the bounded poll budget (~120s) is exhausted. The
46
+ `ask` result records the latest per-tx `status` (deduped by hash), and the pipe
47
+ `tx_status` event gains a `timeout` status. Best-effort and non-fatal: when the
48
+ chain can't be resolved or the vault can't poll status, the existing `pending`
49
+ status stands. The blocking confirmation wait is scoped to the top of the
50
+ message loop (depth 0 — the single-tx ask/pipe case); inside a multi-turn tool
51
+ loop a leg keeps its honest `pending` instead of stacking the poll budget per
52
+ tx. The shared `pending | confirmed | failed | timeout` union is now threaded
53
+ through the ask result, pipe event, and UI callback without unchecked casts.
54
+
3
55
  ## 2.8.0
4
56
 
5
57
  ### Patch Changes
package/dist/index.js CHANGED
@@ -8074,6 +8074,7 @@ var AgentErrorCode = /* @__PURE__ */ ((AgentErrorCode3) => {
8074
8074
  AgentErrorCode3["TRANSACTION_FAILED"] = "TRANSACTION_FAILED";
8075
8075
  AgentErrorCode3["SIGNING_FAILED"] = "SIGNING_FAILED";
8076
8076
  AgentErrorCode3["SESSION_NOT_INITIALIZED"] = "SESSION_NOT_INITIALIZED";
8077
+ AgentErrorCode3["SESSION_NOT_FOUND"] = "SESSION_NOT_FOUND";
8077
8078
  AgentErrorCode3["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
8078
8079
  return AgentErrorCode3;
8079
8080
  })(AgentErrorCode || {});
@@ -8185,6 +8186,11 @@ var AskInterface = class {
8185
8186
  transactions = [];
8186
8187
  cards = [];
8187
8188
  error;
8189
+ // Whether ask() has run at least once. initialize() drives getCallbacks()
8190
+ // BEFORE the first ask() — a stale --session fallback fires
8191
+ // onError(SESSION_NOT_FOUND) there. ask() must NOT clear that initialize-time
8192
+ // error on its first turn, or the result envelope reports false success.
8193
+ hasAsked = false;
8188
8194
  constructor(session, verbose = false, autoApprove = false) {
8189
8195
  this.session = session;
8190
8196
  this.verbose = verbose;
@@ -8224,9 +8230,15 @@ var AskInterface = class {
8224
8230
  onSuggestions: (_suggestions) => {
8225
8231
  },
8226
8232
  onTxStatus: (txHash, chain, status, explorerUrl) => {
8227
- this.transactions.push({ hash: txHash, chain, ...status ? { status } : {}, explorerUrl });
8233
+ const existing = this.transactions.find((t) => t.hash === txHash);
8234
+ if (existing) {
8235
+ existing.status = status;
8236
+ if (explorerUrl) existing.explorerUrl = explorerUrl;
8237
+ } else {
8238
+ this.transactions.push({ hash: txHash, chain, explorerUrl, status });
8239
+ }
8228
8240
  if (this.verbose) {
8229
- process.stderr.write(`[tx] ${chain}: ${txHash}
8241
+ process.stderr.write(`[tx] ${chain}: ${txHash} (${status})
8230
8242
  `);
8231
8243
  }
8232
8244
  },
@@ -8263,7 +8275,10 @@ var AskInterface = class {
8263
8275
  this.toolCalls = [];
8264
8276
  this.transactions = [];
8265
8277
  this.cards = [];
8266
- this.error = void 0;
8278
+ if (this.hasAsked) {
8279
+ this.error = void 0;
8280
+ }
8281
+ this.hasAsked = true;
8267
8282
  const callbacks = this.getCallbacks();
8268
8283
  await this.session.sendMessage(message, callbacks);
8269
8284
  return this.partialResult();
@@ -8618,7 +8633,13 @@ async function authenticateVault(client, vault, password, maxAttempts = 3) {
8618
8633
  });
8619
8634
  return {
8620
8635
  token: authResponse.token,
8621
- expiresAt: authResponse.expires_at
8636
+ expiresAt: authResponse.expires_at,
8637
+ // Captured + persisted by the session token cache. The backend exposes
8638
+ // POST /auth/refresh to exchange it for a fresh access token without a
8639
+ // new MPC round; wiring that exchange is a future enhancement — today
8640
+ // the CLI re-auths via a full MPC re-sign (authenticateVault), which is
8641
+ // always available and avoids depending on refresh-token rotation.
8642
+ refreshToken: authResponse.refresh_token
8622
8643
  };
8623
8644
  } catch (err) {
8624
8645
  lastError = err;
@@ -12066,7 +12087,7 @@ var PipeInterface = class {
12066
12087
  };
12067
12088
 
12068
12089
  // src/agent/session.ts
12069
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
12090
+ import { chmodSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
12070
12091
  import { homedir as homedir2 } from "node:os";
12071
12092
  import { join as join2 } from "node:path";
12072
12093
  import { MemoryStorage, PushNotificationService } from "@vultisig/sdk";
@@ -12080,6 +12101,8 @@ var CLIENT_SIDE_TOOL_DISPATCH = {
12080
12101
  var MAX_MESSAGE_LOOP_DEPTH = 16;
12081
12102
  var RECOVERY_POLL_INTERVAL_MS = 2e3;
12082
12103
  var RECOVERY_MAX_POLLS = 90;
12104
+ var TX_CONFIRM_POLL_INTERVAL_MS = 3e3;
12105
+ var TX_CONFIRM_MAX_POLLS = 40;
12083
12106
  var AgentSession = class {
12084
12107
  client;
12085
12108
  vault;
@@ -12097,6 +12120,10 @@ var AgentSession = class {
12097
12120
  // poll loop without real 2s waits.
12098
12121
  recoveryPollIntervalMs = RECOVERY_POLL_INTERVAL_MS;
12099
12122
  recoveryMaxPolls = RECOVERY_MAX_POLLS;
12123
+ // Post-broadcast confirmation poll cadence — instance fields so tests can
12124
+ // drive the loop without real waits.
12125
+ txConfirmPollIntervalMs = TX_CONFIRM_POLL_INTERVAL_MS;
12126
+ txConfirmMaxPolls = TX_CONFIRM_MAX_POLLS;
12100
12127
  constructor(vault, config) {
12101
12128
  this.vault = vault;
12102
12129
  this.config = config;
@@ -12132,7 +12159,7 @@ var AgentSession = class {
12132
12159
  } else {
12133
12160
  const auth = await authenticateVault(this.client, this.vault, this.config.password);
12134
12161
  this.client.setAuthToken(auth.token);
12135
- saveCachedToken(this.publicKey, auth.token, auth.expiresAt);
12162
+ saveCachedToken(this.publicKey, auth.token, auth.expiresAt, auth.refreshToken);
12136
12163
  }
12137
12164
  } catch (err) {
12138
12165
  throw new Error(`Authentication failed: ${err.message}`);
@@ -12140,25 +12167,20 @@ var AgentSession = class {
12140
12167
  if (this.config.sessionId) {
12141
12168
  this.conversationId = this.config.sessionId;
12142
12169
  try {
12143
- const conv = await this.client.getConversation(this.conversationId, this.publicKey);
12170
+ const conv = await this.withAuthRetry(() => this.client.getConversation(this.conversationId, this.publicKey));
12144
12171
  this.historyMessages = conv.messages || [];
12145
12172
  } catch (err) {
12146
- if (err.message?.includes("401") || err.message?.includes("403")) {
12147
- clearCachedToken(this.publicKey);
12148
- const auth = await authenticateVault(this.client, this.vault, this.config.password);
12149
- this.client.setAuthToken(auth.token);
12150
- saveCachedToken(this.publicKey, auth.token, auth.expiresAt);
12151
- const conv = await this.client.getConversation(this.conversationId, this.publicKey);
12152
- this.historyMessages = conv.messages || [];
12153
- } else {
12154
- this.conversationId = null;
12155
- this.historyMessages = [];
12156
- const conv = await this.client.createConversation(this.publicKey);
12157
- this.conversationId = conv.id;
12158
- }
12173
+ this.conversationId = null;
12174
+ this.historyMessages = [];
12175
+ const conv = await this.withAuthRetry(() => this.client.createConversation(this.publicKey));
12176
+ this.conversationId = conv.id;
12177
+ ui.onError(
12178
+ `Session ${this.config.sessionId} could not be resumed (${err?.message ?? "unknown error"}); started a new conversation ${conv.id}`,
12179
+ "SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */
12180
+ );
12159
12181
  }
12160
12182
  } else {
12161
- const conv = await this.client.createConversation(this.publicKey);
12183
+ const conv = await this.withAuthRetry(() => this.client.createConversation(this.publicKey));
12162
12184
  this.conversationId = conv.id;
12163
12185
  }
12164
12186
  this.cachedContext = this.config.viaAgent || this.config.askMode ? await buildMinimalContext(this.vault) : await buildMessageContext(this.vault);
@@ -12192,6 +12214,40 @@ var AgentSession = class {
12192
12214
  }
12193
12215
  }
12194
12216
  }
12217
+ /**
12218
+ * Run an authenticated backend request and, on a 401/403, do a single
12219
+ * clear → re-auth → retry. This is the ONE chokepoint every conversation
12220
+ * request shares (resume fetch, fresh-convo create, error-fallback create,
12221
+ * and the send-message stream) so a revoked-but-unexpired cached token
12222
+ * recovers identically everywhere instead of throwing on some paths.
12223
+ *
12224
+ * The retry replays the EXACT same `request` closure, which matters for the
12225
+ * send-message path: the replayed body must carry the same content +
12226
+ * recent_actions or the LLM re-emits tool calls (runaway loop). Re-auth is a
12227
+ * full MPC re-sign via authenticateVault — the backend also exposes
12228
+ * POST /auth/refresh, but exchanging the refresh token is a future
12229
+ * enhancement (see auth.ts); the re-sign is always available.
12230
+ *
12231
+ * `onReauth` (optional) fires the instant a re-auth is committed to — BEFORE
12232
+ * authenticateVault runs — so a caller in a retry loop (recoverDisconnectedTurn)
12233
+ * can record that its single re-auth has been spent even if the MPC re-sign
12234
+ * itself then throws. Without this hook a re-auth that fails with a non-auth
12235
+ * error would let the caller re-enter and re-sign on every iteration.
12236
+ */
12237
+ async withAuthRetry(request, onReauth) {
12238
+ try {
12239
+ return await request();
12240
+ } catch (err) {
12241
+ if (!isAuthError(err)) throw err;
12242
+ onReauth?.();
12243
+ const previousRefreshToken = readTokenStore()[this.publicKey]?.refreshToken;
12244
+ clearCachedToken(this.publicKey);
12245
+ const auth = await authenticateVault(this.client, this.vault, this.config.password);
12246
+ this.client.setAuthToken(auth.token);
12247
+ saveCachedToken(this.publicKey, auth.token, auth.expiresAt, auth.refreshToken ?? previousRefreshToken);
12248
+ return await request();
12249
+ }
12250
+ }
12195
12251
  getConversationId() {
12196
12252
  return this.conversationId;
12197
12253
  }
@@ -12316,31 +12372,15 @@ var AgentSession = class {
12316
12372
  }
12317
12373
  };
12318
12374
  let streamResult;
12319
- let authRetried = false;
12320
- while (true) {
12321
- try {
12322
- streamResult = await this.client.sendMessageStream(
12323
- this.conversationId,
12324
- request,
12325
- callbacks,
12326
- this.abortController?.signal
12327
- );
12328
- break;
12329
- } catch (err) {
12330
- const isAuthErr = err.message?.includes("401") || err.message?.includes("403");
12331
- if (isAuthErr && !authRetried) {
12332
- authRetried = true;
12333
- clearCachedToken(this.publicKey);
12334
- const auth = await authenticateVault(this.client, this.vault, this.config.password);
12335
- this.client.setAuthToken(auth.token);
12336
- saveCachedToken(this.publicKey, auth.token, auth.expiresAt);
12337
- continue;
12338
- }
12339
- if (flushedThisCall.length > 0) {
12340
- this.pendingToolResults = [...flushedThisCall, ...this.pendingToolResults];
12341
- }
12342
- throw err;
12375
+ try {
12376
+ streamResult = await this.withAuthRetry(
12377
+ () => this.client.sendMessageStream(this.conversationId, request, callbacks, this.abortController?.signal)
12378
+ );
12379
+ } catch (err) {
12380
+ if (flushedThisCall.length > 0) {
12381
+ this.pendingToolResults = [...flushedThisCall, ...this.pendingToolResults];
12343
12382
  }
12383
+ throw err;
12344
12384
  }
12345
12385
  if (pendingDispatches.length > 0) {
12346
12386
  await Promise.all(pendingDispatches);
@@ -12375,7 +12415,9 @@ var AgentSession = class {
12375
12415
  const txHash = recent.data.tx_hash;
12376
12416
  const chain = recent.data.chain;
12377
12417
  const explorerUrl = recent.data.explorer_url;
12378
- if (txHash) ui.onTxStatus(txHash, chain || "", "pending", explorerUrl);
12418
+ if (txHash) {
12419
+ await this.emitAndConfirmTx(txHash, chain, explorerUrl, depth, ui);
12420
+ }
12379
12421
  }
12380
12422
  await this.processMessageLoop(null, ui, depth + 1);
12381
12423
  return;
@@ -12401,10 +12443,16 @@ var AgentSession = class {
12401
12443
  const since = serverAnchor ?? new Date(Date.now() - 2e3).toISOString();
12402
12444
  const replaySignableCards = serverAnchor !== null;
12403
12445
  let cursor;
12446
+ let authRecovered = false;
12404
12447
  for (let attempt = 0; attempt < this.recoveryMaxPolls; attempt++) {
12405
12448
  let resp;
12406
12449
  try {
12407
- resp = await this.client.messagesSince(this.conversationId, cursor ? { cursor } : { since });
12450
+ resp = authRecovered ? await this.client.messagesSince(this.conversationId, cursor ? { cursor } : { since }) : await this.withAuthRetry(
12451
+ () => this.client.messagesSince(this.conversationId, cursor ? { cursor } : { since }),
12452
+ () => {
12453
+ authRecovered = true;
12454
+ }
12455
+ );
12408
12456
  } catch (err) {
12409
12457
  if (this.config.verbose) {
12410
12458
  process.stderr.write(`[session] recovery poll ${attempt + 1} failed: ${err?.message ?? err}
@@ -12434,6 +12482,75 @@ var AgentSession = class {
12434
12482
  recoverySleep() {
12435
12483
  return new Promise((resolve) => setTimeout(resolve, this.recoveryPollIntervalMs));
12436
12484
  }
12485
+ /**
12486
+ * Post-broadcast confirmation polling (audit F1). A bare `pending` status only
12487
+ * means "broadcast accepted"; the tx can still revert, expire, or be dropped,
12488
+ * so a headless caller that stops at `pending` may mark a later-reverted
12489
+ * operation complete. Poll vault.getTxStatus until the tx reaches a final
12490
+ * state and emit the matching lifecycle status (`confirmed`/`failed`), or
12491
+ * `timeout` when the bounded poll budget is exhausted (the tx may still
12492
+ * confirm later — callers can re-check with `vultisig tx-status`).
12493
+ *
12494
+ * Transient RPC/network errors are treated as "not final yet" and retried
12495
+ * until the budget is spent. Best-effort and non-fatal: if the chain can't be
12496
+ * resolved or the vault doesn't expose getTxStatus, the caller's already-
12497
+ * emitted `pending` status stands and this returns quietly.
12498
+ *
12499
+ * Scoped to headless callers (ask/pipe) that need machine-readable finality.
12500
+ * The interactive TUI already shows `pending` + an explorer link immediately
12501
+ * and has the dedicated `vultisig tx-status` command, so blocking its prompt
12502
+ * for the full poll budget would be a UX regression the audit didn't scope.
12503
+ * The poll also bails on cancel (Ctrl-C aborts the controller) so a long wait
12504
+ * is interruptible.
12505
+ *
12506
+ * The caller only invokes this at message-loop depth 0 (see the call site):
12507
+ * inside a multi-turn tool loop the broadcast result already drives the next
12508
+ * turn, so blocking here would stack the poll budget per leg without feeding
12509
+ * the server any extra signal. Those deeper legs keep their honest `pending`.
12510
+ */
12511
+ async emitAndConfirmTx(txHash, chain, explorerUrl, depth, ui) {
12512
+ ui.onTxStatus(txHash, chain || "", "pending", explorerUrl);
12513
+ if (depth === 0) {
12514
+ await this.confirmBroadcastedTx(txHash, chain, explorerUrl, ui);
12515
+ }
12516
+ }
12517
+ async confirmBroadcastedTx(txHash, chainName, explorerUrl, ui) {
12518
+ if (!this.config.askMode && !this.config.viaAgent) return;
12519
+ const chain = resolveChain(chainName ?? "");
12520
+ if (!chain || typeof this.vault?.getTxStatus !== "function") return;
12521
+ for (let attempt = 0; attempt < this.txConfirmMaxPolls; attempt++) {
12522
+ if (this.abortController?.signal?.aborted) return;
12523
+ try {
12524
+ const result = await this.vault.getTxStatus({ chain, txHash });
12525
+ if (result.status === "success") {
12526
+ ui.onTxStatus(txHash, chainName ?? "", "confirmed", explorerUrl);
12527
+ return;
12528
+ }
12529
+ if (result.status === "error") {
12530
+ ui.onTxStatus(txHash, chainName ?? "", "failed", explorerUrl);
12531
+ return;
12532
+ }
12533
+ } catch (err) {
12534
+ if (this.config.verbose) {
12535
+ process.stderr.write(`[session] tx confirm poll ${attempt + 1} failed: ${err?.message ?? err}
12536
+ `);
12537
+ }
12538
+ }
12539
+ if (attempt < this.txConfirmMaxPolls - 1) await this.txConfirmSleep();
12540
+ }
12541
+ if (this.abortController?.signal?.aborted) return;
12542
+ if (this.config.verbose) {
12543
+ process.stderr.write(
12544
+ `[session] tx ${txHash} not confirmed within ${this.txConfirmMaxPolls} polls; emitting timeout
12545
+ `
12546
+ );
12547
+ }
12548
+ ui.onTxStatus(txHash, chainName ?? "", "timeout", explorerUrl);
12549
+ }
12550
+ /** Sleep between confirmation polls. Separate method so tests can stub it out. */
12551
+ txConfirmSleep() {
12552
+ return new Promise((resolve) => setTimeout(resolve, this.txConfirmPollIntervalMs));
12553
+ }
12437
12554
  /**
12438
12555
  * Fold a recovered assistant message back into the live stream result: the
12439
12556
  * authoritative message wins over any partial deltas, and any persisted
@@ -12625,6 +12742,10 @@ function serverNowToIso(serverNow) {
12625
12742
  function hasTxReadyPart(parts) {
12626
12743
  return !!parts?.some((p) => p.type === "data-tx_ready" && !!p.data);
12627
12744
  }
12745
+ function isAuthError(err) {
12746
+ const msg = err instanceof Error ? err.message : String(err ?? "");
12747
+ return /\b(401|403)\b/.test(msg);
12748
+ }
12628
12749
  function getTokenCachePath() {
12629
12750
  const dir = process.env.VULTISIG_CONFIG_DIR ?? join2(homedir2(), ".vultisig");
12630
12751
  return join2(dir, "agent-tokens.json");
@@ -12641,8 +12762,16 @@ function readTokenStore() {
12641
12762
  function writeTokenStore(store) {
12642
12763
  const path4 = getTokenCachePath();
12643
12764
  const dir = join2(path4, "..");
12644
- if (!existsSync(dir)) mkdirSync2(dir, { recursive: true });
12765
+ if (!existsSync(dir)) mkdirSync2(dir, { recursive: true, mode: 448 });
12766
+ try {
12767
+ chmodSync(dir, 448);
12768
+ } catch {
12769
+ }
12645
12770
  writeFileSync2(path4, JSON.stringify(store, null, 2), { mode: 384 });
12771
+ try {
12772
+ chmodSync(path4, 384);
12773
+ } catch {
12774
+ }
12646
12775
  }
12647
12776
  function loadCachedToken(publicKey) {
12648
12777
  const store = readTokenStore();
@@ -12660,9 +12789,13 @@ function loadCachedToken(publicKey) {
12660
12789
  }
12661
12790
  return entry.token;
12662
12791
  }
12663
- function saveCachedToken(publicKey, token, expiresAt) {
12792
+ function saveCachedToken(publicKey, token, expiresAt, refreshToken) {
12664
12793
  const store = readTokenStore();
12665
- store[publicKey] = { token, expiresAt };
12794
+ store[publicKey] = {
12795
+ token,
12796
+ expiresAt,
12797
+ refreshToken: refreshToken ?? store[publicKey]?.refreshToken
12798
+ };
12666
12799
  try {
12667
12800
  writeTokenStore(store);
12668
12801
  } catch {
@@ -13210,7 +13343,12 @@ async function executeAgentSessionsList(ctx2, options) {
13210
13343
  let totalCount = 0;
13211
13344
  let skip = 0;
13212
13345
  while (true) {
13213
- const page = await client.listConversations(publicKey, skip, PAGE_SIZE);
13346
+ const page = await withClientAuthRetry(
13347
+ client,
13348
+ vault,
13349
+ options.password,
13350
+ () => client.listConversations(publicKey, skip, PAGE_SIZE)
13351
+ );
13214
13352
  totalCount = page.total_count;
13215
13353
  allConversations.push(...page.conversations);
13216
13354
  if (allConversations.length >= totalCount || page.conversations.length < PAGE_SIZE) break;
@@ -13252,7 +13390,7 @@ async function executeAgentSessionsDelete(ctx2, sessionId, options) {
13252
13390
  const backendUrl = options.backendUrl || process.env.VULTISIG_AGENT_URL || "https://abe.vultisig.com";
13253
13391
  const client = await createAuthenticatedClient(backendUrl, vault, options.password);
13254
13392
  const publicKey = vault.publicKeys.ecdsa;
13255
- await client.deleteConversation(sessionId, publicKey);
13393
+ await withClientAuthRetry(client, vault, options.password, () => client.deleteConversation(sessionId, publicKey));
13256
13394
  if (isJsonOutput()) {
13257
13395
  outputJson({ deleted: sessionId });
13258
13396
  return;
@@ -13265,6 +13403,16 @@ async function createAuthenticatedClient(backendUrl, vault, password) {
13265
13403
  client.setAuthToken(auth.token);
13266
13404
  return client;
13267
13405
  }
13406
+ async function withClientAuthRetry(client, vault, password, request) {
13407
+ try {
13408
+ return await request();
13409
+ } catch (err) {
13410
+ if (!isAuthError(err)) throw err;
13411
+ const auth = await authenticateVault(client, vault, password);
13412
+ client.setAuthToken(auth.token);
13413
+ return await request();
13414
+ }
13415
+ }
13268
13416
  function formatDate(iso) {
13269
13417
  try {
13270
13418
  const d = new Date(iso);
@@ -13283,7 +13431,7 @@ var cachedVersion = null;
13283
13431
  function getVersion() {
13284
13432
  if (cachedVersion) return cachedVersion;
13285
13433
  if (true) {
13286
- cachedVersion = "2.8.0";
13434
+ cachedVersion = "2.8.2";
13287
13435
  return cachedVersion;
13288
13436
  }
13289
13437
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vultisig/cli",
3
- "version": "2.8.0",
3
+ "version": "2.8.2",
4
4
  "description": "The self-custody MPC wallet CLI for AI coding agents (Claude Code, Cursor, OpenCode). Natural-language agent mode, 36+ chains, DKLS23 threshold signatures. Seedless.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -76,7 +76,7 @@
76
76
  "@vultisig/client-shared": "^0.2.16",
77
77
  "@vultisig/core-chain": "^2.17.8",
78
78
  "@vultisig/rujira": "^41.0.0",
79
- "@vultisig/sdk": "^2.8.0",
79
+ "@vultisig/sdk": "^2.8.2",
80
80
  "chalk": "^5.6.2",
81
81
  "cli-table3": "^0.6.5",
82
82
  "commander": "^15.0.0",