@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.
- package/CHANGELOG.md +52 -0
- package/dist/index.js +201 -53
- 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.
|
|
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.
|
|
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
|
-
|
|
12147
|
-
|
|
12148
|
-
|
|
12149
|
-
|
|
12150
|
-
|
|
12151
|
-
|
|
12152
|
-
|
|
12153
|
-
|
|
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
|
-
|
|
12320
|
-
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
|
|
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)
|
|
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] = {
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|