@goplausible/openclaw-algorand-plugin 1.7.1 → 1.8.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.
package/README.md CHANGED
@@ -4,16 +4,21 @@
4
4
 
5
5
  ## Features
6
6
 
7
- - **Local MCP Server**: Bundled `algorand-mcp` (99 tools) — wallet, transactions, smart contracts, TEAL, indexer, DEX, NFD, knowledge base
7
+ - **Local MCP Server**: Bundled `algorand-mcp` (107 tools) — wallet, transactions, smart contracts, TEAL, indexer, DEX, NFD, Haystack Router, Alpha Arcade, knowledge base
8
8
  - **x402 Payment Protocol**: Built-in `x402_fetch` tool for HTTP-native payments on Algorand
9
+ - **Headless Linux Keyring Persistence**: Automatic setup for wallet key persistence on cloud VMs and Docker (survives reboots)
9
10
  - **Interactive Setup**: Guided wizard for configuration
10
- - **Skills Included** (6 skills):
11
+ - **Skills Included** (9 skills):
11
12
  - `algorand-development` — AlgoKit CLI, project creation, general workflows
12
13
  - `algorand-typescript` — TypeScript smart contracts (PuyaTs)
13
14
  - `algorand-python` — Python smart contracts (PuyaPy)
14
15
  - `algorand-interaction` — Blockchain interaction via MCP (wallet, transactions, swaps, NFD)
15
16
  - `algorand-x402-typescript` — x402 payments in TypeScript
16
17
  - `algorand-x402-python` — x402 payments in Python
18
+ - `algorand-x402-payment` — Runtime x402 payment (agent as client)
19
+ - `haystack-router-development` — DEX aggregator SDK integration
20
+ - `haystack-router-interaction` — Best-price swaps via MCP tools
21
+ - `alpha-arcade-interaction` — Prediction markets interaction
17
22
 
18
23
  ## Installation
19
24
 
@@ -35,7 +40,7 @@ After installing, run these commands:
35
40
  # 1. Initialize plugin (write memory file + configure mcporter)
36
41
  openclaw algorand-plugin init
37
42
 
38
- # 2. Run interactive setup (configure x402 toggle)
43
+ # 2. Run interactive setup (keyring persistence + x402 toggle)
39
44
  openclaw algorand-plugin setup
40
45
 
41
46
  # 3. Restart the gateway
@@ -55,7 +60,7 @@ openclaw algorand-plugin mcp-config # Show MCP config snippet for external codi
55
60
 
56
61
  The plugin bundles [`@goplausible/algorand-mcp`](https://www.npmjs.com/package/@goplausible/algorand-mcp) as an npm dependency. It runs locally via stdio through [mcporter](https://www.npmjs.com/package/mcporter).
57
62
 
58
- - **99 tools** across 11 categories (wallet, transactions, algod, indexer, NFD, Tinyman, TEAL, knowledge base, and more)
63
+ - **107 tools** across 13 categories (wallet, transactions, algod, indexer, NFD, Tinyman, Haystack Router, Pera verification, Alpha Arcade, TEAL, knowledge base, and more)
59
64
  - **Multi-network**: `mainnet`, `testnet`, `localnet`
60
65
  - **Secure wallet**: Per-transaction and daily spending limits, private keys never exposed to agents
61
66
 
@@ -67,6 +72,17 @@ When `enableX402` is enabled (default), the plugin registers the `x402_fetch` to
67
72
  - Agent builds payment using algorand-mcp wallet tools (atomic group with facilitator-sponsored fees)
68
73
  - Agent retries with signed `PAYMENT-SIGNATURE` header to complete the payment and access the resource
69
74
 
75
+ ## Headless Linux (Cloud VMs, Docker)
76
+
77
+ On headless Linux, the OS keyring defaults to in-memory storage — wallet keys created by `algorand-mcp` are **lost on reboot**. The setup wizard (`openclaw algorand-plugin setup`) automatically detects this and:
78
+
79
+ 1. Installs `gnome-keyring`, `libsecret-tools`, `dbus-user-session`
80
+ 2. Enables `loginctl linger` for D-Bus session persistence
81
+ 3. Creates a persistent login keyring (auto-unlocked, no password)
82
+ 4. Backs up and restores existing wallet mnemonics if upgrading
83
+
84
+ After setup, wallet keys persist across reboots with no user interaction needed. Agent wallets are hot wallets — keep funds minimal and use QR code top-ups.
85
+
70
86
  ## Configuration
71
87
 
72
88
  Config is stored in `~/.openclaw/openclaw.json` under `plugins.entries.openclaw-algorand-plugin.config`:
@@ -96,6 +112,9 @@ Config is stored in `~/.openclaw/openclaw.json` under `plugins.entries.openclaw-
96
112
  | `algorand-interaction` | MCP-based blockchain interaction (wallet, txns, DEX, NFD, x402) |
97
113
  | `algorand-x402-typescript` | x402 payments in TypeScript |
98
114
  | `algorand-x402-python` | x402 payments in Python |
115
+ | `haystack-router-development` | Haystack router development |
116
+ | `haystack-router-interaction` | Haystack router interaction |
117
+ | `alpha-arcade-interaction` | Alpha Arcade interaction |
99
118
 
100
119
  ## Links
101
120
 
package/index.ts CHANGED
@@ -301,6 +301,29 @@ export default function register(api: PluginApi) {
301
301
  console.log(" Config:");
302
302
  console.log(` x402: ${pluginConfig.enableX402 !== false ? "Enabled" : "Disabled"}`);
303
303
  console.log("");
304
+
305
+ // Keyring status
306
+ try {
307
+ const scriptPath = join(__dirname, "scripts", "setup-keyring.sh");
308
+ const keyringOutput = execSync(`bash "${scriptPath}" --detect`, { encoding: "utf-8", timeout: 5000 });
309
+ const vars = Object.fromEntries(
310
+ keyringOutput.trim().split("\n").map((line: string) => line.split("=", 2))
311
+ );
312
+ console.log(" Keyring:");
313
+ if (vars.PERSISTENT === "true") {
314
+ console.log(` Storage: ✅ ${vars.BACKEND}`);
315
+ console.log(` Wallets: ${vars.WALLET_DB_COUNT} account(s) in wallet.db`);
316
+ } else {
317
+ console.log(` Storage: ⚠️ ${vars.BACKEND} — run \`openclaw algorand-plugin setup\``);
318
+ if (parseInt(vars.WALLET_DB_COUNT) > 0) {
319
+ console.log(` Wallets: ⚠️ ${vars.WALLET_DB_COUNT} account(s) with mnemonics in volatile storage!`);
320
+ }
321
+ }
322
+ } catch {
323
+ console.log(" Keyring:");
324
+ console.log(" Storage: ❓ Could not detect");
325
+ }
326
+ console.log("");
304
327
  console.log(" Links:");
305
328
  console.log(` GoPlausible: ${GOPLAUSIBLE_SERVICES.website}`);
306
329
  console.log(` Algorand x402: ${GOPLAUSIBLE_SERVICES.x402}`);
@@ -346,6 +369,16 @@ export default function register(api: PluginApi) {
346
369
  console.log(" This plugin provides:");
347
370
  console.log(" • 9 Algorand skills (Algorand development in TS and Python, x402, MCP interaction, alpha arcade interaction, haystack router development and interaction)");
348
371
  console.log(" • algorand-mcp server (~100 blockchain tools via mcporter)\n");
372
+ // Keyring persistence warning for headless Linux
373
+ try {
374
+ const scriptPath = join(__dirname, "scripts", "setup-keyring.sh");
375
+ const keyringOutput = execSync(`bash "${scriptPath}" --detect`, { encoding: "utf-8", timeout: 5000 });
376
+ if (keyringOutput.includes("PERSISTENT=false")) {
377
+ console.log(" ⚠️ Headless Linux detected — wallet keys use in-memory storage.");
378
+ console.log(" Run `openclaw algorand-plugin setup` for persistent storage.\n");
379
+ }
380
+ } catch { /* ignore on install */ }
381
+
349
382
  console.log(" Next steps:");
350
383
  console.log(" 1. Run `openclaw algorand-plugin init` — configure mcporter + add plugin memory");
351
384
  console.log(" 2. Run `openclaw algorand-plugin setup` — configure options & add to allow list");
@@ -1,7 +1,7 @@
1
1
  export const ALGORAND_MCP = {
2
2
  id: "algorand-mcp",
3
3
  name: "Algorand MCP",
4
- description: "Local Algorand MCP server — 99 tools for blockchain interaction",
4
+ description: "Local Algorand MCP server — 107 tools for blockchain interaction",
5
5
  type: "stdio" as const,
6
6
  command: "algorand-mcp",
7
7
  } as const;
@@ -3,7 +3,7 @@
3
3
  This plugin enables three core capabilities:
4
4
 
5
5
  1. **Algorand Development** — Smart contracts, typed clients, React frontends via AlgoKit CLI and skills
6
- 2. **Blockchain Interaction** — Algorand MCP server (99 tools) via mcporter
6
+ 2. **Blockchain Interaction** — Algorand MCP server (107 tools) via mcporter
7
7
  3. **x402 Payment Protocol** — HTTP-native payments with Algorand as first-class chain
8
8
 
9
9
  ## Skill Routing
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-algorand-plugin",
3
3
  "name": "Algorand Integration",
4
4
  "description": "Algorand blockchain integration with MCP and skills — by GoPlausible",
5
- "version": "1.7.1",
5
+ "version": "1.8.0",
6
6
  "skills": [
7
7
  "skills/algorand-development",
8
8
  "skills/algorand-typescript",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goplausible/openclaw-algorand-plugin",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -27,6 +27,7 @@
27
27
  "index.ts",
28
28
  "setup.ts",
29
29
  "lib/",
30
+ "scripts/",
30
31
  "skills/",
31
32
  "memory/",
32
33
  "openclaw.plugin.json"
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ // Usage:
3
+ // node backup-keyring.js backup <output-file> — dump address\tmnemonic pairs
4
+ // node backup-keyring.js restore <input-file> — restore mnemonics from backup
5
+ // node backup-keyring.js count — print number of accounts in wallet.db
6
+ // node backup-keyring.js verify — check which accounts have mnemonics in keyring
7
+
8
+ const { Entry } = require('@napi-rs/keyring');
9
+ const initSqlJs = require('sql.js');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+
14
+ const KEYCHAIN_SERVICE = 'algorand-mcp';
15
+ const WALLET_DB = path.join(os.homedir(), '.algorand-mcp', 'wallet.db');
16
+
17
+ async function getAccounts() {
18
+ if (!fs.existsSync(WALLET_DB)) return [];
19
+ const SQL = await initSqlJs();
20
+ const buf = fs.readFileSync(WALLET_DB);
21
+ const db = new SQL.Database(buf);
22
+ const rows = db.exec('SELECT address FROM accounts');
23
+ db.close();
24
+ if (!rows.length || !rows[0].values.length) return [];
25
+ return rows[0].values.map(r => r[0]);
26
+ }
27
+
28
+ function getMnemonic(address) {
29
+ try {
30
+ const entry = new Entry(KEYCHAIN_SERVICE, address);
31
+ const pw = entry.getPassword();
32
+ return pw || null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function setMnemonic(address, mnemonic) {
39
+ const entry = new Entry(KEYCHAIN_SERVICE, address);
40
+ entry.setPassword(mnemonic);
41
+ }
42
+
43
+ async function main() {
44
+ const mode = process.argv[2];
45
+ const file = process.argv[3];
46
+
47
+ if (mode === 'count') {
48
+ const accounts = await getAccounts();
49
+ console.log(accounts.length);
50
+ return;
51
+ }
52
+
53
+ if (mode === 'verify') {
54
+ const accounts = await getAccounts();
55
+ let ok = 0, missing = 0;
56
+ for (const addr of accounts) {
57
+ if (getMnemonic(addr)) {
58
+ console.log('OK ' + addr);
59
+ ok++;
60
+ } else {
61
+ console.log('MISSING ' + addr);
62
+ missing++;
63
+ }
64
+ }
65
+ console.log(`TOTAL=${accounts.length} OK=${ok} MISSING=${missing}`);
66
+ return;
67
+ }
68
+
69
+ if (mode === 'backup') {
70
+ if (!file) { console.error('Usage: backup-keyring.js backup <output-file>'); process.exit(1); }
71
+ const accounts = await getAccounts();
72
+ if (accounts.length === 0) {
73
+ console.log('NO_ACCOUNTS');
74
+ return;
75
+ }
76
+ let backed = 0, failed = 0;
77
+ const lines = [];
78
+ for (const addr of accounts) {
79
+ const mnemonic = getMnemonic(addr);
80
+ if (mnemonic) {
81
+ lines.push(addr + '\t' + mnemonic);
82
+ backed++;
83
+ } else {
84
+ console.error('MISSING ' + addr);
85
+ failed++;
86
+ }
87
+ }
88
+ if (lines.length > 0) {
89
+ fs.writeFileSync(file, lines.join('\n') + '\n', { mode: 0o600 });
90
+ }
91
+ console.log(`BACKED_UP=${backed} FAILED=${failed}`);
92
+ return;
93
+ }
94
+
95
+ if (mode === 'restore') {
96
+ if (!file) { console.error('Usage: backup-keyring.js restore <input-file>'); process.exit(1); }
97
+ if (!fs.existsSync(file)) { console.error('File not found: ' + file); process.exit(1); }
98
+ const content = fs.readFileSync(file, 'utf-8').trim();
99
+ if (!content) { console.log('RESTORED=0'); return; }
100
+ let restored = 0;
101
+ for (const line of content.split('\n')) {
102
+ const [addr, ...rest] = line.split('\t');
103
+ const mnemonic = rest.join('\t');
104
+ if (addr && mnemonic) {
105
+ setMnemonic(addr, mnemonic);
106
+ restored++;
107
+ }
108
+ }
109
+ console.log(`RESTORED=${restored}`);
110
+ return;
111
+ }
112
+
113
+ console.error('Usage: backup-keyring.js <backup|restore|count|verify> [file]');
114
+ process.exit(1);
115
+ }
116
+
117
+ main().catch(err => { console.error(err.message); process.exit(1); });
@@ -0,0 +1,428 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ MODE="${1:---detect}"
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ PLUGIN_DIR="$(dirname "$SCRIPT_DIR")"
7
+ NODE_MODULES="$PLUGIN_DIR/node_modules"
8
+ BACKUP_SCRIPT="$SCRIPT_DIR/backup-keyring.js"
9
+ WALLET_DB="$HOME/.algorand-mcp/wallet.db"
10
+
11
+ # ═══════════════════════════════════════════════════════════
12
+ # 1. OS Detection
13
+ # ═══════════════════════════════════════════════════════════
14
+ if [[ "$(uname -s)" != "Linux" ]]; then
15
+ case "$(uname -s)" in
16
+ Darwin) BACKEND="macOS Keychain" ;;
17
+ *) BACKEND="OS Keychain" ;;
18
+ esac
19
+ if [ "$MODE" = "--detect" ]; then
20
+ echo "PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')"
21
+ echo "BACKEND=$BACKEND"
22
+ echo "PERSISTENT=true"
23
+ echo "HEADLESS=false"
24
+ else
25
+ echo ""
26
+ echo " ✅ $BACKEND — persistent by default. No setup needed."
27
+ echo ""
28
+ fi
29
+ exit 0
30
+ fi
31
+
32
+ # ═══════════════════════════════════════════════════════════
33
+ # 2. Linux Environment Detection
34
+ # ═══════════════════════════════════════════════════════════
35
+
36
+ PLATFORM="linux"
37
+ KEYRING_DIR="$HOME/.local/share/keyrings"
38
+
39
+ # Display (headless?)
40
+ HAS_DISPLAY=false
41
+ [[ -n "${DISPLAY:-}" || -n "${WAYLAND_DISPLAY:-}" ]] && HAS_DISPLAY=true
42
+
43
+ # D-Bus session
44
+ HAS_DBUS=false
45
+ [[ -n "${DBUS_SESSION_BUS_ADDRESS:-}" ]] && HAS_DBUS=true
46
+
47
+ # GNOME Keyring daemon running
48
+ KEYRING_RUNNING=false
49
+ pgrep -u "$USER" gnome-keyring-daemon >/dev/null 2>&1 && KEYRING_RUNNING=true
50
+
51
+ # Keyring files on disk
52
+ KEYRING_FILES=false
53
+ [[ -d "$KEYRING_DIR" && "$(ls -A "$KEYRING_DIR" 2>/dev/null)" ]] && KEYRING_FILES=true
54
+
55
+ # Wallet DB exists and account count (via Node.js)
56
+ WALLET_DB_EXISTS=false
57
+ WALLET_DB_COUNT=0
58
+ [[ -f "$WALLET_DB" ]] && WALLET_DB_EXISTS=true
59
+ if [[ "$WALLET_DB_EXISTS" == "true" && -f "$BACKUP_SCRIPT" ]]; then
60
+ WALLET_DB_COUNT=$(NODE_PATH="$NODE_MODULES" node "$BACKUP_SCRIPT" count 2>/dev/null || echo "0")
61
+ fi
62
+
63
+ # Package manager
64
+ PKG=""
65
+ if command -v apt >/dev/null 2>&1; then
66
+ PKG="apt"
67
+ elif command -v dnf >/dev/null 2>&1; then
68
+ PKG="dnf"
69
+ elif command -v yum >/dev/null 2>&1; then
70
+ PKG="yum"
71
+ elif command -v pacman >/dev/null 2>&1; then
72
+ PKG="pacman"
73
+ elif command -v apk >/dev/null 2>&1; then
74
+ PKG="apk"
75
+ fi
76
+
77
+ # Container detection
78
+ IN_CONTAINER=false
79
+ [[ -f /.dockerenv ]] && IN_CONTAINER=true
80
+ grep -q 'docker\|lxc\|containerd' /proc/1/cgroup 2>/dev/null && IN_CONTAINER=true
81
+
82
+ # Persistence verdict
83
+ HEADLESS=false
84
+ if [[ "$HAS_DBUS" == "true" && "$KEYRING_RUNNING" == "true" && "$KEYRING_FILES" == "true" ]]; then
85
+ PERSISTENT="true"
86
+ BACKEND="GNOME Keyring (persistent)"
87
+ elif [[ "$HAS_DISPLAY" == "true" ]]; then
88
+ PERSISTENT="true"
89
+ BACKEND="Desktop Keyring (persistent)"
90
+ else
91
+ PERSISTENT="false"
92
+ BACKEND="In-memory (volatile)"
93
+ HEADLESS=true
94
+ fi
95
+
96
+ # ═══════════════════════════════════════════════════════════
97
+ # 3. Detect-only mode
98
+ # ═══════════════════════════════════════════════════════════
99
+ if [ "$MODE" = "--detect" ]; then
100
+ echo "PLATFORM=$PLATFORM"
101
+ echo "BACKEND=$BACKEND"
102
+ echo "PERSISTENT=$PERSISTENT"
103
+ echo "HEADLESS=$HEADLESS"
104
+ echo "HAS_DISPLAY=$HAS_DISPLAY"
105
+ echo "HAS_DBUS=$HAS_DBUS"
106
+ echo "KEYRING_RUNNING=$KEYRING_RUNNING"
107
+ echo "KEYRING_FILES=$KEYRING_FILES"
108
+ echo "WALLET_DB_EXISTS=$WALLET_DB_EXISTS"
109
+ echo "WALLET_DB_COUNT=$WALLET_DB_COUNT"
110
+ echo "PKG_MANAGER=$PKG"
111
+ echo "IN_CONTAINER=$IN_CONTAINER"
112
+ exit 0
113
+ fi
114
+
115
+ # ═══════════════════════════════════════════════════════════
116
+ # 4. Setup mode
117
+ # ═══════════════════════════════════════════════════════════
118
+
119
+ echo ""
120
+ echo " ── Algorand MCP Keyring Persistence Setup ──"
121
+ echo ""
122
+
123
+ # ─── 4a. Status display ───
124
+ echo " Platform: Linux"
125
+ echo " Display: $( [[ "$HAS_DISPLAY" == "true" ]] && echo "✅ Yes" || echo "❌ No (headless)" )"
126
+ echo " D-Bus session: $( [[ "$HAS_DBUS" == "true" ]] && echo "✅ Active" || echo "❌ Not found" )"
127
+ echo " Keyring daemon: $( [[ "$KEYRING_RUNNING" == "true" ]] && echo "✅ Running" || echo "❌ Not running" )"
128
+ echo " Keyring files: $( [[ "$KEYRING_FILES" == "true" ]] && echo "✅ Found in $KEYRING_DIR" || echo "❌ None (in-memory only)" )"
129
+ echo " Wallet DB: $( [[ "$WALLET_DB_EXISTS" == "true" ]] && echo "✅ $WALLET_DB ($WALLET_DB_COUNT account(s))" || echo "— Not found (fresh install)" )"
130
+ echo " Package manager: ${PKG:-unknown}"
131
+ echo " Container: $( [[ "$IN_CONTAINER" == "true" ]] && echo "Yes" || echo "No" )"
132
+ echo ""
133
+
134
+ # Already persistent with keyring files on disk?
135
+ if [[ "$PERSISTENT" == "true" && "$KEYRING_FILES" == "true" ]]; then
136
+ echo " ✅ Keyring is persistent and will survive reboots."
137
+ echo ""
138
+ exit 0
139
+ fi
140
+
141
+ # ─── 4b. Backup existing wallet mnemonics (UPDATE scenario only) ───
142
+ BACKUP_FILE=""
143
+ if [[ "$WALLET_DB_COUNT" -gt 0 && "$KEYRING_RUNNING" == "true" && -f "$BACKUP_SCRIPT" ]]; then
144
+ echo " ── Step 1: Backup wallet mnemonics from current keyring ──"
145
+ echo ""
146
+ echo " Found $WALLET_DB_COUNT account(s) in wallet.db."
147
+ echo " Reading mnemonics from current keyring before setup..."
148
+ echo ""
149
+
150
+ BACKUP_FILE=$(mktemp /tmp/algorand-mcp-keyring-backup.XXXXXX)
151
+ chmod 600 "$BACKUP_FILE"
152
+
153
+ BACKUP_RESULT=$(NODE_PATH="$NODE_MODULES" node "$BACKUP_SCRIPT" backup "$BACKUP_FILE" 2>&1)
154
+ echo " $BACKUP_RESULT"
155
+
156
+ # Check if backup actually has content
157
+ if [[ ! -s "$BACKUP_FILE" ]]; then
158
+ rm -f "$BACKUP_FILE"
159
+ BACKUP_FILE=""
160
+ echo " ℹ️ No mnemonics to backup (keyring may be empty after a reboot)."
161
+ fi
162
+ echo ""
163
+ elif [[ "$WALLET_DB_COUNT" -gt 0 && "$KEYRING_RUNNING" != "true" ]]; then
164
+ echo " ── Step 1: Backup ──"
165
+ echo ""
166
+ echo " ⚠️ Keyring daemon not running — cannot backup existing mnemonics."
167
+ echo " If the system was rebooted, in-memory mnemonics are already lost."
168
+ echo ""
169
+ fi
170
+
171
+ # ─── 4c. Install keyring packages ───
172
+ echo " ── Step 2: Install keyring packages ──"
173
+ echo ""
174
+
175
+ if [[ -z "$PKG" ]]; then
176
+ echo " Could not detect package manager. Install manually:"
177
+ echo " gnome-keyring, libsecret-tools, dbus-user-session (or equivalent)"
178
+ echo ""
179
+ else
180
+ case "$PKG" in
181
+ apt)
182
+ INSTALL_CMD="sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt update && sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt install -y gnome-keyring libsecret-tools dbus-user-session"
183
+ ;;
184
+ dnf)
185
+ INSTALL_CMD="sudo dnf install -y gnome-keyring libsecret libsecret-tools"
186
+ ;;
187
+ yum)
188
+ INSTALL_CMD="sudo yum install -y gnome-keyring libsecret libsecret-tools"
189
+ ;;
190
+ pacman)
191
+ INSTALL_CMD="sudo pacman -Sy --noconfirm gnome-keyring libsecret"
192
+ ;;
193
+ apk)
194
+ INSTALL_CMD="sudo apk add gnome-keyring libsecret dbus secret-tool"
195
+ ;;
196
+ esac
197
+
198
+ echo " Run:"
199
+ echo " $INSTALL_CMD"
200
+ echo ""
201
+ read -r -p " Install now? [y/N] " REPLY
202
+ if [[ "$REPLY" =~ ^[Yy]$ ]]; then
203
+ echo ""
204
+ eval "$INSTALL_CMD"
205
+ echo ""
206
+ echo " ✅ Packages installed."
207
+ else
208
+ echo " Skipped. Install later with the command above."
209
+ fi
210
+ echo ""
211
+ fi
212
+
213
+ # ─── 4d. Enable lingering user session ───
214
+ echo " ── Step 3: Enable user session lingering ──"
215
+ echo ""
216
+ echo " This keeps D-Bus and keyring daemon alive after SSH logout."
217
+ echo ""
218
+
219
+ if loginctl show-user "$USER" 2>/dev/null | grep -q "Linger=yes"; then
220
+ echo " ✅ Lingering already enabled for $USER"
221
+ else
222
+ read -r -p " Enable lingering for $USER? [y/N] " REPLY
223
+ if [[ "$REPLY" =~ ^[Yy]$ ]]; then
224
+ loginctl enable-linger "$USER"
225
+ echo " ✅ Lingering enabled."
226
+ else
227
+ echo " Skipped."
228
+ fi
229
+ fi
230
+ echo ""
231
+
232
+ # ─── 4e. Start D-Bus and keyring daemon ───
233
+ echo " ── Step 4: Start D-Bus session and keyring daemon ──"
234
+ echo ""
235
+
236
+ # Ensure D-Bus is available
237
+ if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
238
+ echo " Starting D-Bus session..."
239
+ eval $(dbus-launch --sh-syntax)
240
+ echo " ✅ D-Bus session started."
241
+ else
242
+ echo " ✅ D-Bus session already active."
243
+ fi
244
+
245
+ # Kill stale keyring daemons (leftover --unlock processes, etc.)
246
+ STALE_COUNT=$(pgrep -u "$USER" gnome-keyring-daemon 2>/dev/null | wc -l)
247
+ if [[ "$STALE_COUNT" -gt 1 ]]; then
248
+ echo " Cleaning up $STALE_COUNT stale keyring daemon processes..."
249
+ pkill -9 -u "$USER" gnome-keyring-daemon 2>/dev/null || true
250
+ sleep 2
251
+ fi
252
+
253
+ # Start the daemon via --start (D-Bus activated, stays resident)
254
+ # Note: on headless, the daemon may be D-Bus activated on demand rather than
255
+ # staying resident. This is normal — secret-tool and @napi-rs/keyring will
256
+ # trigger D-Bus activation automatically when they query the Secret Service.
257
+ echo " Starting gnome-keyring-daemon..."
258
+ eval $(gnome-keyring-daemon --start --components=secrets 2>&1 | grep -v '^\*\*') || true
259
+ sleep 1
260
+
261
+ if pgrep -u "$USER" gnome-keyring-daemon >/dev/null 2>&1; then
262
+ echo " ✅ Keyring daemon running."
263
+ else
264
+ # On D-Bus activated systems, the daemon starts on demand — verify by querying
265
+ if command -v secret-tool >/dev/null 2>&1; then
266
+ secret-tool search --all service algorand-mcp 2>/dev/null && echo " ✅ Keyring daemon available (D-Bus activated)." || true
267
+ fi
268
+ echo " ℹ️ Daemon is D-Bus activated (starts on demand when queried)."
269
+ fi
270
+ echo ""
271
+
272
+ # ─── 4f. Create persistent login keyring collection ───
273
+ KEYRING_FILE="$KEYRING_DIR/login.keyring"
274
+ mkdir -p "$KEYRING_DIR"
275
+
276
+ echo " ── Step 5: Create persistent keyring ──"
277
+ echo ""
278
+
279
+ if [[ -f "$KEYRING_FILE" ]]; then
280
+ echo " ✅ Keyring already exists at $KEYRING_FILE"
281
+ else
282
+ # On headless Linux, gnome-keyring-daemon refuses to create a collection
283
+ # via normal prompts (no GUI). We use the D-Bus internal interface
284
+ # CreateWithMasterPassword to create the "login" collection with an
285
+ # empty master password — making it auto-unlocked and persistent on disk.
286
+ echo " Creating login keyring collection via D-Bus..."
287
+
288
+ python3 << 'PYEOF'
289
+ import sys
290
+ try:
291
+ from jeepney import new_method_call, DBusAddress
292
+ from jeepney.io.blocking import open_dbus_connection
293
+
294
+ conn = open_dbus_connection(bus='SESSION')
295
+
296
+ # Open a plain-text session with the Secret Service
297
+ msg = new_method_call(
298
+ DBusAddress('/org/freedesktop/secrets',
299
+ bus_name='org.freedesktop.secrets',
300
+ interface='org.freedesktop.Secret.Service'),
301
+ 'OpenSession',
302
+ 'sv',
303
+ ('plain', ('s', ''))
304
+ )
305
+ reply = conn.send_and_get_reply(msg)
306
+ session_path = reply.body[1]
307
+
308
+ # Create the "login" collection with empty master password
309
+ # Uses the internal gnome-keyring interface (bypasses GUI prompt)
310
+ props = {
311
+ 'org.freedesktop.Secret.Collection.Label': ('s', 'login')
312
+ }
313
+ master_password = (session_path, b'', b'', 'text/plain')
314
+
315
+ msg = new_method_call(
316
+ DBusAddress('/org/freedesktop/secrets',
317
+ bus_name='org.gnome.keyring',
318
+ interface='org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface'),
319
+ 'CreateWithMasterPassword',
320
+ 'a{sv}(oayays)',
321
+ (props, master_password)
322
+ )
323
+ reply = conn.send_and_get_reply(msg)
324
+ print(f" Collection created: {reply.body[0]}")
325
+ conn.close()
326
+ except Exception as e:
327
+ print(f" Error: {e}", file=sys.stderr)
328
+ sys.exit(1)
329
+ PYEOF
330
+
331
+ if [[ $? -eq 0 && -f "$KEYRING_FILE" ]]; then
332
+ echo " ✅ Persistent keyring created at $KEYRING_FILE"
333
+ else
334
+ FOUND_FILE=$(ls "$KEYRING_DIR"/*.keyring 2>/dev/null | head -1 || true)
335
+ if [[ -n "$FOUND_FILE" ]]; then
336
+ echo " ✅ Persistent keyring created at $FOUND_FILE"
337
+ else
338
+ echo " ⚠️ Keyring file not created. Ensure python3 and jeepney are installed."
339
+ echo " Install with: pip3 install jeepney"
340
+ fi
341
+ fi
342
+ fi
343
+ echo ""
344
+
345
+ # ─── 4g. Restore backed-up wallet mnemonics (UPDATE scenario only) ───
346
+ if [[ -n "$BACKUP_FILE" && -f "$BACKUP_FILE" && -s "$BACKUP_FILE" ]]; then
347
+ echo " ── Step 6: Restore wallet mnemonics ──"
348
+ echo ""
349
+
350
+ RESTORE_RESULT=$(NODE_PATH="$NODE_MODULES" node "$BACKUP_SCRIPT" restore "$BACKUP_FILE" 2>&1)
351
+ echo " $RESTORE_RESULT"
352
+
353
+ # Securely delete backup
354
+ shred -u "$BACKUP_FILE" 2>/dev/null || rm -f "$BACKUP_FILE"
355
+ echo " ✅ Temporary backup securely deleted."
356
+ echo ""
357
+ fi
358
+
359
+ # ─── 4h. Docker-specific guidance ───
360
+ if [[ "$IN_CONTAINER" == "true" ]]; then
361
+ echo " ── Docker / Container Notes ──"
362
+ echo ""
363
+ echo " Add to your Dockerfile:"
364
+ echo " RUN apt-get update && apt-get install -y \\"
365
+ echo " gnome-keyring libsecret-tools dbus-user-session \\"
366
+ echo " && rm -rf /var/lib/apt/lists/*"
367
+ echo ""
368
+ echo " Entrypoint wrapper (entrypoint.sh):"
369
+ echo ' #!/bin/bash'
370
+ echo ' export $(dbus-launch)'
371
+ echo ' eval $(gnome-keyring-daemon --start --components=secrets)'
372
+ echo ' # Create login collection if not exists (headless — no GUI prompt)'
373
+ echo ' if [ ! -f ~/.local/share/keyrings/login.keyring ]; then'
374
+ echo ' python3 -c "'
375
+ echo ' from jeepney import new_method_call, DBusAddress'
376
+ echo ' from jeepney.io.blocking import open_dbus_connection'
377
+ echo ' conn = open_dbus_connection(bus=\"SESSION\")'
378
+ echo ' r = conn.send_and_get_reply(new_method_call(DBusAddress(\"/org/freedesktop/secrets\",bus_name=\"org.freedesktop.secrets\",interface=\"org.freedesktop.Secret.Service\"),\"OpenSession\",\"sv\",(\"plain\",(\"s\",\"\"))))'
379
+ echo ' s = r.body[1]'
380
+ echo ' conn.send_and_get_reply(new_method_call(DBusAddress(\"/org/freedesktop/secrets\",bus_name=\"org.gnome.keyring\",interface=\"org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface\"),\"CreateWithMasterPassword\",\"a{sv}(oayays)\",({\"org.freedesktop.Secret.Collection.Label\":(\"s\",\"login\")},(s,b\"\",b\"\",\"text/plain\"))))'
381
+ echo ' conn.close()'
382
+ echo ' "'
383
+ echo ' fi'
384
+ echo ' exec "$@"'
385
+ echo ""
386
+ echo " Persist keyring + wallet data (docker-compose.yml):"
387
+ echo " volumes:"
388
+ echo " - keyring-data:/home/user/.local/share/keyrings"
389
+ echo " - wallet-data:/home/user/.algorand-mcp"
390
+ echo ""
391
+ fi
392
+
393
+ # ─── 4i. Final verification ───
394
+ echo " ── Results ──"
395
+ echo ""
396
+
397
+ # Re-check
398
+ VERIFY_RUNNING=false
399
+ pgrep -u "$USER" gnome-keyring-daemon >/dev/null 2>&1 && VERIFY_RUNNING=true
400
+
401
+ VERIFY_FILES=false
402
+ [[ -d "$KEYRING_DIR" && "$(ls -A "$KEYRING_DIR" 2>/dev/null)" ]] && VERIFY_FILES=true
403
+
404
+ if [[ "$VERIFY_RUNNING" == "true" ]]; then
405
+ echo " ✅ Daemon: Running"
406
+ else
407
+ echo " ✅ Daemon: D-Bus activated (starts on demand)"
408
+ fi
409
+
410
+ if [[ "$VERIFY_FILES" == "true" ]]; then
411
+ echo " ✅ Persistent: Yes (files at $KEYRING_DIR)"
412
+ else
413
+ echo " ⚠️ Persistent: No keyring files yet"
414
+ fi
415
+
416
+ # Verify wallet mnemonics via Node.js
417
+ if [[ -f "$BACKUP_SCRIPT" && "$WALLET_DB_EXISTS" == "true" ]]; then
418
+ VERIFY_RESULT=$(NODE_PATH="$NODE_MODULES" node "$BACKUP_SCRIPT" verify 2>/dev/null | tail -1)
419
+ echo " ✅ Wallets: $VERIFY_RESULT"
420
+ fi
421
+
422
+ echo ""
423
+ echo " Behavior after setup:"
424
+ echo " • Keyring stored on disk, auto-unlocked on boot via D-Bus activation"
425
+ echo " • loginctl linger keeps D-Bus session alive between SSH sessions"
426
+ echo " • Wallet keys persist across reboots — no user interaction needed"
427
+ echo " • Keep agent wallet funds minimal — use QR code top-ups as needed"
428
+ echo ""
package/setup.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import * as p from "@clack/prompts";
2
- // import { execSync } from "node:child_process";
2
+ import { execSync } from "node:child_process";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
3
5
  import { ALGORAND_MCP, GOPLAUSIBLE_SERVICES } from "./lib/mcp-servers.js";
4
6
 
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+
5
9
  export interface AlgorandPluginConfig {
6
10
  enableX402: boolean;
7
11
  }
@@ -11,34 +15,14 @@ export async function runSetup(
11
15
  ): Promise<AlgorandPluginConfig | null> {
12
16
  p.intro("🔷 Algorand Plugin Setup — powered by GoPlausible");
13
17
 
14
- // // Step 1: Verify algorand-mcp binary is available
15
- // let mcpAvailable = false;
16
- // let mcpPath = "";
17
- // try {
18
- // mcpPath = execSync("npm list @goplausible/algorand-mcp", { encoding: "utf-8" }).trim();
19
- // mcpAvailable = true;
20
- // } catch {
21
- // // Binary not found in PATH
22
- // }
23
-
24
- // if (mcpAvailable) {
25
- // p.note(
26
- // `MCP Server: ${ALGORAND_MCP.name}\n` +
27
- // `Type: ${ALGORAND_MCP.type} (local)\n` +
28
- // `Command: ${ALGORAND_MCP.command}\n` +
29
- // `Path: ${mcpPath}\n` +
30
- // `Status: ✅ Available`,
31
- // "Algorand MCP"
32
- // );
33
- // } else {
34
- // p.log.warn(
35
- // `algorand-mcp binary not found in PATH.\n\n` +
36
- // `Options:\n` +
37
- // ` • Run with npx: npx algorand-mcp\n` +
38
- // ` • Install globally: npm install -g @goplausible/algorand-mcp\n` +
39
- // ` • Add node_modules/.bin to PATH`
40
- // );
41
- // }
18
+ // Step 1: Keyring persistence check & setup
19
+ p.log.step("Checking keyring persistence for wallet storage...");
20
+ try {
21
+ const scriptPath = join(__dirname, "scripts", "setup-keyring.sh");
22
+ execSync(`bash "${scriptPath}" --setup`, { stdio: "inherit" });
23
+ } catch {
24
+ p.log.warn("Keyring setup script failed — you may need to configure manually.");
25
+ }
42
26
 
43
27
  // Step 2: x402 integration
44
28
  const enableX402 = await p.confirm({
@@ -59,7 +43,7 @@ export async function runSetup(
59
43
  p.note(
60
44
  `x402 Micropayments: ${config.enableX402 ? "Enabled" : "Disabled"}\n\n` +
61
45
  `MCP Server Setup:\n` +
62
- ` The Algorand MCP server (${ALGORAND_MCP.command}) provides 99 blockchain tools.\n` +
46
+ ` The Algorand MCP server (${ALGORAND_MCP.command}) provides 107 blockchain tools.\n` +
63
47
  ` x402 micropayment and AP2 mandate verifiable credentials flows are fully supported in Algorand MCP.\n` ,
64
48
  );
65
49