@jaw.id/cli 0.1.25 → 0.2.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.
Files changed (75) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +4 -0
  4. package/dist/base-command.js +3 -1
  5. package/dist/base-command.js.map +1 -1
  6. package/dist/commands/config/set.js +140 -12
  7. package/dist/commands/config/set.js.map +1 -1
  8. package/dist/commands/config/show.js +3 -1
  9. package/dist/commands/config/show.js.map +1 -1
  10. package/dist/commands/config/write.js +6 -4
  11. package/dist/commands/config/write.js.map +1 -1
  12. package/dist/commands/disconnect.js +24 -10
  13. package/dist/commands/disconnect.js.map +1 -1
  14. package/dist/commands/mcp/index.js +2426 -94
  15. package/dist/commands/mcp/index.js.map +1 -1
  16. package/dist/commands/rpc/call.js +197 -45
  17. package/dist/commands/rpc/call.js.map +1 -1
  18. package/dist/commands/session/add.js +1547 -0
  19. package/dist/commands/session/add.js.map +1 -0
  20. package/dist/commands/session/revoke.js +181 -54
  21. package/dist/commands/session/revoke.js.map +1 -1
  22. package/dist/commands/session/setup.js +516 -65
  23. package/dist/commands/session/setup.js.map +1 -1
  24. package/dist/commands/session/status.js +315 -6
  25. package/dist/commands/session/status.js.map +1 -1
  26. package/dist/commands/version.js +3 -1
  27. package/dist/commands/version.js.map +1 -1
  28. package/dist/commands/x402/log.js +344 -0
  29. package/dist/commands/x402/log.js.map +1 -0
  30. package/dist/commands/x402/pay.js +2122 -0
  31. package/dist/commands/x402/pay.js.map +1 -0
  32. package/dist/commands/x402/status.js +1047 -0
  33. package/dist/commands/x402/status.js.map +1 -0
  34. package/dist/index.js +41 -14
  35. package/dist/index.js.map +1 -1
  36. package/dist/lib/bridge-singleton.js +41 -14
  37. package/dist/lib/bridge-singleton.js.map +1 -1
  38. package/dist/lib/config.js +26 -3
  39. package/dist/lib/config.js.map +1 -1
  40. package/dist/lib/keystore.js +13 -2
  41. package/dist/lib/keystore.js.map +1 -1
  42. package/dist/lib/paths.js +3 -1
  43. package/dist/lib/paths.js.map +1 -1
  44. package/dist/lib/payment-lock.js +121 -0
  45. package/dist/lib/payment-lock.js.map +1 -0
  46. package/dist/lib/session-bridge.js +148 -24
  47. package/dist/lib/session-bridge.js.map +1 -1
  48. package/dist/lib/session-config.js +78 -11
  49. package/dist/lib/session-config.js.map +1 -1
  50. package/dist/lib/terminal.js +22 -0
  51. package/dist/lib/terminal.js.map +1 -0
  52. package/dist/lib/validation.js +3 -3
  53. package/dist/lib/validation.js.map +1 -1
  54. package/dist/lib/ws-bridge.js +22 -10
  55. package/dist/lib/ws-bridge.js.map +1 -1
  56. package/dist/mcp/handlers/config.js +73 -6
  57. package/dist/mcp/handlers/config.js.map +1 -1
  58. package/dist/mcp/handlers/daemon.js +43 -12
  59. package/dist/mcp/handlers/daemon.js.map +1 -1
  60. package/dist/mcp/handlers/resources.js +119 -0
  61. package/dist/mcp/handlers/resources.js.map +1 -1
  62. package/dist/mcp/handlers/rpc.js +269 -60
  63. package/dist/mcp/handlers/rpc.js.map +1 -1
  64. package/dist/mcp/helpers.js +50 -3
  65. package/dist/mcp/helpers.js.map +1 -1
  66. package/dist/mcp/server.js +2426 -94
  67. package/dist/mcp/server.js.map +1 -1
  68. package/dist/mcp/tools.js +43 -3
  69. package/dist/mcp/tools.js.map +1 -1
  70. package/dist/x402/log-view.js +160 -0
  71. package/dist/x402/log-view.js.map +1 -0
  72. package/dist/x402/status-report.js +90 -0
  73. package/dist/x402/status-report.js.map +1 -0
  74. package/oclif.manifest.json +398 -4
  75. package/package.json +8 -3
@@ -1,11 +1,11 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
- import * as fs from 'fs';
2
+ import * as fs5 from 'fs';
3
3
  import * as path from 'path';
4
4
  import * as os from 'os';
5
5
  import * as crypto from 'crypto';
6
6
  import WebSocket from 'ws';
7
7
 
8
- // src/base-command.ts
8
+ // src/commands/session/revoke.ts
9
9
  var JAW_DIR = path.join(os.homedir(), ".jaw");
10
10
  var PATHS = {
11
11
  root: JAW_DIR,
@@ -13,7 +13,9 @@ var PATHS = {
13
13
  session: path.join(JAW_DIR, "session.json"),
14
14
  relay: path.join(JAW_DIR, "relay.json"),
15
15
  keystore: path.join(JAW_DIR, "keystore.json"),
16
- sessionConfig: path.join(JAW_DIR, "session-config.json")
16
+ sessionConfig: path.join(JAW_DIR, "session-config.json"),
17
+ x402Log: path.join(JAW_DIR, "x402-log.jsonl"),
18
+ paymentLock: path.join(JAW_DIR, "x402-payment.lock")
17
19
  };
18
20
 
19
21
  // src/lib/validation.ts
@@ -41,8 +43,8 @@ function isValidRelayUrl(url) {
41
43
 
42
44
  // src/lib/config.ts
43
45
  function ensureDir(dir) {
44
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
45
- fs.chmodSync(dir, 448);
46
+ fs5.mkdirSync(dir, { recursive: true, mode: 448 });
47
+ fs5.chmodSync(dir, 448);
46
48
  }
47
49
  function migrateConfig(config) {
48
50
  if (config.paymasterUrl && !config.paymasters) {
@@ -54,10 +56,10 @@ function migrateConfig(config) {
54
56
  return config;
55
57
  }
56
58
  function loadConfig() {
57
- if (!fs.existsSync(PATHS.config)) {
59
+ if (!fs5.existsSync(PATHS.config)) {
58
60
  return {};
59
61
  }
60
- const raw = fs.readFileSync(PATHS.config, "utf-8");
62
+ const raw = fs5.readFileSync(PATHS.config, "utf-8");
61
63
  try {
62
64
  const config = JSON.parse(raw);
63
65
  return migrateConfig(config);
@@ -69,7 +71,7 @@ function loadConfig() {
69
71
  }
70
72
  function saveConfig(config) {
71
73
  ensureDir(PATHS.root);
72
- fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + "\n", {
74
+ fs5.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + "\n", {
73
75
  encoding: "utf-8",
74
76
  mode: 384
75
77
  });
@@ -223,7 +225,18 @@ function bufferToBase64(buf) {
223
225
  }
224
226
 
225
227
  // src/lib/ws-bridge.ts
228
+ function buildInitPayload(config) {
229
+ return {
230
+ type: "init",
231
+ apiKey: config.apiKey,
232
+ chainId: config.chainId,
233
+ ens: config.ens,
234
+ paymasterUrl: config.paymasterUrl,
235
+ ...config.paymasterUrl && config.paymasterContext ? { paymasterContext: config.paymasterContext } : {}
236
+ };
237
+ }
226
238
  var DEFAULT_TIMEOUT_MS = 12e4;
239
+ var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
227
240
  var MAX_MESSAGE_BYTES = 5 * 1024 * 1024;
228
241
  var BROWSER_REOPEN_COOLDOWN_MS = 5e3;
229
242
  var MAX_RECONNECT_ATTEMPTS = 3;
@@ -232,6 +245,7 @@ var WSBridge = class {
232
245
  relayUrl;
233
246
  session;
234
247
  timeout;
248
+ connectTimeout;
235
249
  config;
236
250
  privateKeyHex;
237
251
  publicKeyHex;
@@ -253,6 +267,7 @@ var WSBridge = class {
253
267
  this.relayUrl = options.relayUrl;
254
268
  this.session = options.session;
255
269
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
270
+ this.connectTimeout = options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT_MS;
256
271
  this.config = options.config;
257
272
  this.privateKeyHex = options.privateKeyHex;
258
273
  this.publicKeyHex = options.publicKeyHex;
@@ -281,17 +296,16 @@ var WSBridge = class {
281
296
  let expectingKeyExchange = !this.peerPublicKeyHex;
282
297
  const timer = setTimeout(() => {
283
298
  ws.close();
284
- reject(new Error("Browser did not connect in time.\nRun `jaw disconnect` then try again."));
285
- }, 3e4);
299
+ reject(
300
+ new Error(
301
+ `Browser did not connect within ${Math.round(this.connectTimeout / 1e3)}s.
302
+ Run \`jaw disconnect\` then try again, or raise JAW_BRIDGE_TIMEOUT_MS.`
303
+ )
304
+ );
305
+ }, this.connectTimeout);
286
306
  const sendEncryptedInit = async () => {
287
307
  if (!this.sharedSecret) return;
288
- const envelope = await encryptMessage(this.sharedSecret, {
289
- type: "init",
290
- apiKey: this.config.apiKey,
291
- chainId: this.config.chainId,
292
- ens: this.config.ens,
293
- paymasterUrl: this.config.paymasterUrl
294
- });
308
+ const envelope = await encryptMessage(this.sharedSecret, buildInitPayload(this.config));
295
309
  this.sendRaw(ws, JSON.stringify({ type: "encrypted", ...envelope }));
296
310
  };
297
311
  const waitForReady = () => {
@@ -539,8 +553,8 @@ function safeParse(data) {
539
553
  }
540
554
  function loadRelaySession() {
541
555
  try {
542
- if (!fs.existsSync(PATHS.relay)) return null;
543
- const raw = fs.readFileSync(PATHS.relay, "utf-8");
556
+ if (!fs5.existsSync(PATHS.relay)) return null;
557
+ const raw = fs5.readFileSync(PATHS.relay, "utf-8");
544
558
  const parsed = JSON.parse(raw);
545
559
  if (!parsed.session || !parsed.relayUrl || !parsed.privateKey || !parsed.publicKey) {
546
560
  return null;
@@ -552,14 +566,14 @@ function loadRelaySession() {
552
566
  }
553
567
  function saveRelaySession(info) {
554
568
  ensureDir(PATHS.root);
555
- fs.writeFileSync(PATHS.relay, JSON.stringify(info, null, 2) + "\n", {
569
+ fs5.writeFileSync(PATHS.relay, JSON.stringify(info, null, 2) + "\n", {
556
570
  encoding: "utf-8",
557
571
  mode: 384
558
572
  });
559
573
  }
560
574
  function deleteRelaySession() {
561
575
  try {
562
- if (fs.existsSync(PATHS.relay)) fs.unlinkSync(PATHS.relay);
576
+ if (fs5.existsSync(PATHS.relay)) fs5.unlinkSync(PATHS.relay);
563
577
  } catch {
564
578
  }
565
579
  }
@@ -569,6 +583,10 @@ var DEFAULT_KEYS_URL = "https://keys.jaw.id";
569
583
  var DEFAULT_RELAY_URL = "wss://relay.jaw.id";
570
584
  async function getBridge(options) {
571
585
  const config = loadConfig();
586
+ const envTimeout = Number(process.env["JAW_BRIDGE_TIMEOUT_MS"]);
587
+ const fromEnv = Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : void 0;
588
+ const timeout = options.timeout ?? fromEnv;
589
+ const connectTimeout = options.connectTimeout ?? fromEnv;
572
590
  const keysUrl = options.keysUrl ?? config.keysUrl ?? DEFAULT_KEYS_URL;
573
591
  const relayUrl = options.relayUrl ?? config.relayUrl ?? DEFAULT_RELAY_URL;
574
592
  const chainId = options.chainId ?? config.defaultChain ?? 1;
@@ -581,7 +599,7 @@ async function getBridge(options) {
581
599
  let relaySession = loadRelaySession();
582
600
  if (relaySession && relaySession.relayUrl === relayUrl && relaySession.peerPublicKey) {
583
601
  try {
584
- return await connectBridge(relaySession, options, chainId, keysUrl, relayUrl, false);
602
+ return await connectBridge({ ...options, timeout }, relaySession, chainId, keysUrl, relayUrl, false);
585
603
  } catch {
586
604
  deleteRelaySession();
587
605
  relaySession = null;
@@ -591,7 +609,7 @@ async function getBridge(options) {
591
609
  }
592
610
  const session = await createNewSession(relayUrl);
593
611
  saveRelaySession(session);
594
- return await connectBridge(session, options, chainId, keysUrl, relayUrl, true);
612
+ return await connectBridge({ ...options, timeout, connectTimeout }, session, chainId, keysUrl, relayUrl, true);
595
613
  }
596
614
  async function createNewSession(relayUrl) {
597
615
  const kp = await generateKeyPair();
@@ -606,17 +624,20 @@ async function createNewSession(relayUrl) {
606
624
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
607
625
  };
608
626
  }
609
- async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl, openBrowser) {
627
+ async function connectBridge(options, relaySession, chainId, keysUrl, relayUrl, openBrowser) {
610
628
  const config = loadConfig();
629
+ const paymaster = config.paymasters?.[chainId];
611
630
  const bridge = new WSBridge({
612
631
  relayUrl,
613
632
  session: relaySession.session,
614
633
  timeout: options.timeout,
634
+ connectTimeout: options.connectTimeout,
615
635
  config: {
616
636
  apiKey: options.apiKey,
617
637
  chainId,
618
638
  ens: options.ens ?? config.ens,
619
- paymasterUrl: options.paymasterUrl ?? config.paymasters?.[chainId]?.url
639
+ paymasterUrl: paymaster?.url,
640
+ paymasterContext: paymaster?.context
620
641
  },
621
642
  privateKeyHex: relaySession.privateKey,
622
643
  publicKeyHex: relaySession.publicKey,
@@ -626,6 +647,12 @@ async function connectBridge(relaySession, options, chainId, keysUrl, relayUrl,
626
647
  // onBrowserNeeded — only open a browser for new sessions
627
648
  openBrowser ? async () => {
628
649
  const bridgeUrl = buildBridgeUrl(keysUrl, relaySession.session, relayUrl, relaySession.publicKey);
650
+ if (process.env["JAW_NO_BROWSER"]) {
651
+ process.stderr.write(`Open this URL to approve:
652
+ ${bridgeUrl}
653
+ `);
654
+ return;
655
+ }
629
656
  const { default: open } = await import('open');
630
657
  await open(bridgeUrl);
631
658
  } : void 0,
@@ -645,35 +672,77 @@ function buildBridgeUrl(keysUrl, session, relayUrl, cliPublicKeyHex) {
645
672
  return url.toString();
646
673
  }
647
674
  function deleteKeystore() {
648
- if (fs.existsSync(PATHS.keystore)) {
649
- fs.unlinkSync(PATHS.keystore);
675
+ if (fs5.existsSync(PATHS.keystore)) {
676
+ fs5.unlinkSync(PATHS.keystore);
650
677
  }
651
678
  }
652
679
  function keystoreExists() {
653
- return fs.existsSync(PATHS.keystore);
680
+ return fs5.existsSync(PATHS.keystore);
681
+ }
682
+ function liveOrphans(orphans, now = Date.now() / 1e3) {
683
+ return (orphans ?? []).filter((orphan) => orphan.expiry > now);
684
+ }
685
+ function writeSessionConfig(config) {
686
+ ensureDir(PATHS.root);
687
+ const temp = `${PATHS.sessionConfig}.${process.pid}.tmp`;
688
+ fs5.writeFileSync(temp, JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
689
+ fs5.chmodSync(temp, 384);
690
+ fs5.renameSync(temp, PATHS.sessionConfig);
691
+ }
692
+ function saveRevokeProgress(config, progress) {
693
+ const next = { ...tryLoadSessionConfig() ?? config };
694
+ if (progress.orphans.length > 0) next.orphanedPermissions = progress.orphans;
695
+ else delete next.orphanedPermissions;
696
+ if (progress.ownPermissionRevoked) next.permissionRevoked = true;
697
+ writeSessionConfig(next);
654
698
  }
655
699
  function loadSessionConfig() {
656
- if (!fs.existsSync(PATHS.sessionConfig)) {
700
+ if (!fs5.existsSync(PATHS.sessionConfig)) {
657
701
  throw new Error("No session configured. Run `jaw session setup` first.");
658
702
  }
659
- const raw = fs.readFileSync(PATHS.sessionConfig, "utf-8");
703
+ const raw = fs5.readFileSync(PATHS.sessionConfig, "utf-8");
660
704
  try {
661
705
  return JSON.parse(raw);
662
706
  } catch {
663
707
  throw new Error(`Session config at ${PATHS.sessionConfig} is corrupted. Run \`jaw session setup\` to recreate it.`);
664
708
  }
665
709
  }
710
+ function tryLoadSessionConfig() {
711
+ try {
712
+ return loadSessionConfig();
713
+ } catch {
714
+ return null;
715
+ }
716
+ }
666
717
  function deleteSessionConfig() {
667
- if (fs.existsSync(PATHS.sessionConfig)) {
668
- fs.unlinkSync(PATHS.sessionConfig);
718
+ if (fs5.existsSync(PATHS.sessionConfig)) {
719
+ fs5.unlinkSync(PATHS.sessionConfig);
669
720
  }
670
721
  }
671
722
 
723
+ // src/lib/terminal.ts
724
+ var INVISIBLE_AND_BIDI = /[\u200B-\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069\uFEFF]/g;
725
+ var LINE_CONTROLS = /[\u0000-\u001F\u007F-\u009F]/g;
726
+ var REPLACEMENT = "\uFFFD";
727
+ var DEFAULT_LINE_LENGTH = 200;
728
+ function bound(text, maxLength) {
729
+ if (text.length <= maxLength) return text;
730
+ return `${text.slice(0, maxLength)}\u2026 (${text.length - maxLength} more characters)`;
731
+ }
732
+ function sanitizeLine(value, maxLength = DEFAULT_LINE_LENGTH) {
733
+ const text = typeof value === "string" ? value : String(value);
734
+ return bound(text.replace(LINE_CONTROLS, REPLACEMENT).replace(INVISIBLE_AND_BIDI, REPLACEMENT), maxLength);
735
+ }
736
+
672
737
  // src/commands/session/revoke.ts
673
738
  var SessionRevoke = class _SessionRevoke extends BaseCommand {
674
739
  static description = "Revoke on-chain permission and delete local session key.";
675
740
  static flags = {
676
- ...BaseCommand.baseFlags
741
+ ...BaseCommand.baseFlags,
742
+ force: Flags.boolean({
743
+ description: "Delete the local session even if some permissions could not be revoked. They stay live on chain until they expire, and their ids are printed because deleting the session is what loses them.",
744
+ default: false
745
+ })
677
746
  };
678
747
  async run() {
679
748
  const { flags } = await this.parse(_SessionRevoke);
@@ -683,12 +752,20 @@ var SessionRevoke = class _SessionRevoke extends BaseCommand {
683
752
  return;
684
753
  }
685
754
  const sessionConfig = loadSessionConfig();
686
- const isExpired = sessionConfig.expiry <= Date.now() / 1e3;
687
- if (isExpired) {
755
+ const now = Date.now() / 1e3;
756
+ const orphans = liveOrphans(sessionConfig.orphanedPermissions, now);
757
+ const own = (
758
+ // `permissionRevoked` is set by an earlier run that got this far and then
759
+ // failed on something else. Revoking is not idempotent, so attempting it
760
+ // again spends a browser round trip that can only fail.
761
+ !sessionConfig.permissionRevoked && sessionConfig.expiry > now ? { id: sessionConfig.permissionId, chainId: sessionConfig.chainId, expiry: sessionConfig.expiry } : null
762
+ );
763
+ const total = orphans.length + (own ? 1 : 0);
764
+ if (total === 0) {
688
765
  deleteKeystore();
689
766
  deleteSessionConfig();
690
767
  if (format === "json") {
691
- this.outputResult({ revoked: true, skippedOnChain: true }, format);
768
+ this.outputResult({ revoked: true, skippedOnChain: true, revokedIds: [], failed: [] }, format);
692
769
  } else {
693
770
  this.log("Session already expired. Cleaned up local files.");
694
771
  }
@@ -696,31 +773,81 @@ var SessionRevoke = class _SessionRevoke extends BaseCommand {
696
773
  }
697
774
  const config = loadConfig();
698
775
  const apiKey = this.resolveApiKey(flags);
699
- const pm = config.paymasters?.[sessionConfig.chainId];
700
- if (!flags.quiet) {
701
- this.log("Opening browser to revoke permission...");
776
+ if (!flags.quiet && format !== "json") {
777
+ this.log(
778
+ total === 1 ? "Opening browser to revoke permission..." : `Opening browser to revoke ${total} permissions...`
779
+ );
702
780
  }
703
- const bridge = await getBridge({
704
- keysUrl: config.keysUrl,
705
- apiKey,
706
- chainId: sessionConfig.chainId,
707
- ens: config.ens,
708
- paymasterUrl: pm?.url
709
- });
710
- try {
711
- await bridge.request("wallet_revokePermissions", [{ id: sessionConfig.permissionId }]);
712
- } finally {
713
- bridge.close();
781
+ const revokedIds = [];
782
+ const failed = [];
783
+ let remaining = orphans;
784
+ let ownRevoked = false;
785
+ const revokeOn = async (chainId, targets) => {
786
+ let bridge;
787
+ try {
788
+ bridge = await getBridge({ keysUrl: config.keysUrl, apiKey, chainId, ens: config.ens });
789
+ } catch (err) {
790
+ for (const target of targets) failed.push({ id: target.id, reason: describe(err) });
791
+ return;
792
+ }
793
+ try {
794
+ for (const target of targets) {
795
+ try {
796
+ await bridge.request("wallet_revokePermissions", [{ id: target.id }]);
797
+ revokedIds.push(target.id);
798
+ if (target === own) ownRevoked = true;
799
+ else remaining = remaining.filter((orphan) => orphan.id !== target.id);
800
+ saveRevokeProgress(sessionConfig, { orphans: remaining, ownPermissionRevoked: ownRevoked });
801
+ } catch (err) {
802
+ failed.push({ id: target.id, reason: describe(err) });
803
+ }
804
+ }
805
+ } finally {
806
+ bridge.close();
807
+ }
808
+ };
809
+ for (const chainId of [...new Set(orphans.map((orphan) => orphan.chainId))]) {
810
+ await revokeOn(
811
+ chainId,
812
+ orphans.filter((orphan) => orphan.chainId === chainId)
813
+ );
814
+ }
815
+ if (own) await revokeOn(own.chainId, [own]);
816
+ const cleanedUp = failed.length === 0 || flags.force;
817
+ if (cleanedUp) {
818
+ deleteKeystore();
819
+ deleteSessionConfig();
714
820
  }
715
- deleteKeystore();
716
- deleteSessionConfig();
717
821
  if (format === "json") {
718
- this.outputResult({ revoked: true, skippedOnChain: false }, format);
719
- } else {
822
+ this.outputResult(
823
+ { revoked: failed.length === 0, skippedOnChain: false, revokedIds, failed, localSessionDeleted: cleanedUp },
824
+ format
825
+ );
826
+ } else if (failed.length === 0) {
720
827
  this.log("Session revoked. On-chain permission removed and local keys deleted.");
828
+ if (revokedIds.length > 1) {
829
+ this.log(`Revoked ${revokedIds.length} permissions this key was holding.`);
830
+ }
831
+ } else {
832
+ if (revokedIds.length > 0) this.log(`Revoked ${revokedIds.length} of ${total} permissions.`);
833
+ for (const failure of failed) {
834
+ this.log(` could not revoke ${failure.id}: ${failure.reason}`);
835
+ }
836
+ this.log(
837
+ flags.force ? "\nLocal session deleted anyway, as asked. Those permissions stay live until they expire, and the ids above are the only record left of them." : "\nLocal session files were kept so the rest can be retried. Run this again, or pass --force to delete the local session anyway if one of these was already revoked elsewhere."
838
+ );
839
+ }
840
+ if (failed.length > 0 && !flags.force) {
841
+ this.error(
842
+ `${failed.length} of ${total} permissions could not be revoked. One already revoked elsewhere will keep failing, since the record it is read from is gone.`,
843
+ { exit: 1 }
844
+ );
721
845
  }
722
846
  }
723
847
  };
848
+ function describe(err) {
849
+ return sanitizeLine(err instanceof Error ? err.message : String(err), 200);
850
+ }
724
851
 
725
852
  export { SessionRevoke as default };
726
853
  //# sourceMappingURL=revoke.js.map