diff --git a/README.md b/README.md
index 851a6b6..10738de 100644
--- a/README.md
+++ b/README.md
@@ -155,6 +155,19 @@ Use the signer command once at least one encrypted share is saved locally:
Use either `igloo status` or `igloo share status` to decrypt a saved share, connect a temporary bifrost node, and ping each peer. The command prints relay endpoints plus a color-coded list of online/offline peers. Provide `--password` or `--password-file` for automation, and customise relays with `--relays` when needed.
+### Echo debugging
+
+- `--debug-echo` — turn on verbose echo logs (both listener and sender). This sets `IGLOO_DEBUG_ECHO=1` for the current run.
+- `IGLOO_TEST_RELAY` — optionally pin a specific relay for both desktop and CLI, e.g.
+
+ ```bash
+ IGLOO_TEST_RELAY=wss://your-relay igloo share load --debug-echo
+ ```
+
+ Desktop and CLI must share at least one relay to exchange echo events.
+
+Behind the scenes, the CLI normalizes Nostr subscribe filters so relays that require a single filter object (instead of a one-element array) accept subscriptions reliably.
+
## Development scripts
- Node (default)
diff --git a/llm/context/igloo-core-readme.md b/llm/context/igloo-core-readme.md
index a1cfbcb..ed14d5e 100644
--- a/llm/context/igloo-core-readme.md
+++ b/llm/context/igloo-core-readme.md
@@ -321,6 +321,8 @@ with `data === 'echo'` and newer challenge-based requests where `data` is an
even-length hex string. No changes are required on the sender side when
upgrading.
+Tip (CLI): when testing end-to-end with igloo-cli and igloo-desktop, you can enable detailed echo logs with the CLI flag `--debug-echo` (sets `IGLOO_DEBUG_ECHO=1` for that run). To guarantee relay overlap, set `IGLOO_TEST_RELAY=wss://your-relay` in both apps.
+
```typescript
import { awaitShareEcho } from '@frostr/igloo-core';
@@ -366,6 +368,8 @@ try {
```
`challenge` must be an even-length hexadecimal string (32 bytes / 64 hex characters recommended).
+
+Troubleshooting: some Nostr relays reject a single-element filter array during subscription. igloo-cli patches `nostr-tools`’s `SimplePool.subscribeMany` at runtime to unwrap `[[filter]]` → `filter`. If you are wiring listeners manually outside the CLI, apply the same normalization.
#### `startListeningForAllEchoes(groupCredential, shareCredentials, callback, options?)`
Starts listening for echo events on all shares in a keyset.
diff --git a/src/cli.tsx b/src/cli.tsx
index 8d4b744..72b1012 100644
--- a/src/cli.tsx
+++ b/src/cli.tsx
@@ -1,4 +1,5 @@
import './polyfills/websocket.js';
+import './polyfills/nostr.js';
import React from 'react';
import {render} from 'ink';
import {PassThrough} from 'node:stream';
@@ -114,6 +115,12 @@ function parseArgv(argv: string[]): ParsedArgs {
delete flags.T;
}
+ // Short alias: -E → --debug-echo
+ if (flags.E !== undefined && flags['debug-echo'] === undefined) {
+ flags['debug-echo'] = flags.E;
+ delete flags.E;
+ }
+
return {
command: positionals[0] ?? 'intro',
args: positionals.slice(1),
@@ -123,6 +130,16 @@ function parseArgv(argv: string[]): ParsedArgs {
};
}
+function toBool(value: string | boolean | undefined): boolean {
+ if (typeof value === 'boolean') return value;
+ if (typeof value === 'string') {
+ const v = value.trim().toLowerCase();
+ if (['1', 'true', 'yes', 'on'].includes(v)) return true;
+ if (['0', 'false', 'no', 'off'].includes(v)) return false;
+ }
+ return false;
+}
+
function showHelpScreen(version: string, opts?: any) {
const instance = render(, opts);
instance.waitUntilExit().then(() => process.exit(0));
@@ -137,6 +154,15 @@ const {command, args, flags, showHelp, showVersion: shouldShowVersion} = parseAr
process.argv.slice(2)
);
+// Allow --debug-echo to enable/disable echo diagnostics without env vars.
+// This is read by echo send/listen utilities.
+(() => {
+ const raw = (flags['debug-echo'] ?? (flags as any).debugEcho) as string | boolean | undefined;
+ if (raw !== undefined) {
+ process.env.IGLOO_DEBUG_ECHO = toBool(raw) ? '1' : '0';
+ }
+})();
+
// In non-interactive environments (CI/tests), Ink raw mode can throw.
// Allow tests to opt-out via IGLOO_DISABLE_RAW_MODE=1
let inkOptions: any | undefined;
diff --git a/src/components/Help.tsx b/src/components/Help.tsx
index ae5ba4b..d913cff 100644
--- a/src/components/Help.tsx
+++ b/src/components/Help.tsx
@@ -44,6 +44,7 @@ export function Help({version}: HelpProps) {
--relays list Override relay list (comma-separated)
--verbose Stream signer diagnostics
--log-level level Signer log level (debug|info|warn|error)
+ --debug-echo Enable echo debug logs (relay + events)
);
diff --git a/src/components/keyset/KeysetSigner.tsx b/src/components/keyset/KeysetSigner.tsx
index d59e6d7..bb08ff8 100644
--- a/src/components/keyset/KeysetSigner.tsx
+++ b/src/components/keyset/KeysetSigner.tsx
@@ -17,7 +17,8 @@ import {
PeerManager
} from '@frostr/igloo-core';
import type {BifrostNode} from '@frostr/igloo-core';
-import {SimplePool} from 'nostr-tools';
+// Ensure nostr subscribe shim is loaded if this component is used in isolation.
+import '../../polyfills/nostr.js';
import {Prompt} from '../ui/Prompt.js';
import {
readShareFiles,
@@ -799,49 +800,3 @@ export function KeysetSigner({args, flags}: KeysetSignerProps) {
}
export default KeysetSigner;
-let simplePoolPatched = false;
-
-function ensureSimplePoolPatched() {
- if (simplePoolPatched) {
- return;
- }
-
- const prototype = (SimplePool as any)?.prototype;
- if (!prototype) {
- simplePoolPatched = true;
- return;
- }
-
- if (prototype.__iglooFilterNormalizePatched) {
- simplePoolPatched = true;
- return;
- }
-
- const originalSubscribeMany = prototype.subscribeMany;
- if (typeof originalSubscribeMany !== 'function') {
- simplePoolPatched = true;
- return;
- }
-
- prototype.subscribeMany = function patchedSubscribeMany(this: unknown, relays: unknown, filters: unknown, params: unknown) {
- const normalizedFilters =
- Array.isArray(filters) &&
- filters.length === 1 &&
- filters[0] !== null &&
- typeof filters[0] === 'object' &&
- !Array.isArray(filters[0])
- ? filters[0]
- : filters;
-
- return originalSubscribeMany.call(this, relays, normalizedFilters, params);
- };
-
- Object.defineProperty(prototype, '__iglooFilterNormalizePatched', {
- value: true,
- enumerable: false
- });
-
- simplePoolPatched = true;
-}
-
-ensureSimplePoolPatched();
diff --git a/src/components/keyset/useShareEchoListener.ts b/src/components/keyset/useShareEchoListener.ts
index b3aaefc..831c3f2 100644
--- a/src/components/keyset/useShareEchoListener.ts
+++ b/src/components/keyset/useShareEchoListener.ts
@@ -158,6 +158,12 @@ export function useShareEchoListener(
} catch {}
})
: undefined;
+ if (debugEnabled) {
+ try {
+ // eslint-disable-next-line no-console
+ console.log('[echo-listen] INFO using relays', relays ?? 'default');
+ } catch {}
+ }
const result = await awaitShareEcho(
groupCredential,
shareCredential,
diff --git a/src/polyfills/nostr.ts b/src/polyfills/nostr.ts
new file mode 100644
index 0000000..3ec61d9
--- /dev/null
+++ b/src/polyfills/nostr.ts
@@ -0,0 +1,25 @@
+// Global runtime patch for nostr-tools SimplePool.subscribeMany
+// Some relays reject a single-element array for filters (expecting an object).
+// Normalize `[[filter]]` to `filter` to avoid "provided filter is not an object" errors.
+// Idempotent and safe to import multiple times.
+import {SimplePool} from 'nostr-tools';
+
+try {
+ const proto = (SimplePool as any)?.prototype;
+ if (proto && !proto.__iglooFilterNormalizePatched) {
+ const original = proto.subscribeMany;
+ if (typeof original === 'function') {
+ proto.subscribeMany = function patchedSubscribeMany(this: unknown, relays: unknown, filters: unknown, params: unknown) {
+ const normalized = Array.isArray(filters) && filters.length === 1 && filters[0] &&
+ typeof filters[0] === 'object' && !Array.isArray(filters[0])
+ ? filters[0]
+ : filters;
+ return original.call(this, relays, normalized, params);
+ };
+ Object.defineProperty(proto, '__iglooFilterNormalizePatched', {value: true});
+ }
+ }
+} catch {
+ // Best-effort only; if nostr-tools changes, we fail silently.
+}
+