clawmoney 0.15.38 → 0.15.40

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.
@@ -1,8 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { awalExec } from '../utils/awal.js';
3
+ import { awalExec, awalExecSafe } from '../utils/awal.js';
4
4
  import { apiGet } from '../utils/api.js';
5
- import { loadConfig } from '../utils/config.js';
5
+ import { loadConfig, saveConfig } from '../utils/config.js';
6
6
  // Base mainnet USDC contract + balanceOf(address) ABI selector.
7
7
  // Keeping on-chain reads as a first-class path lets `wallet balance`
8
8
  // skip the awal Electron bridge entirely, which is notorious for
@@ -43,7 +43,8 @@ async function readBaseUsdcBalance(walletAddress, timeoutMs = 8000) {
43
43
  export async function walletStatusCommand() {
44
44
  const spinner = ora('Getting wallet status...').start();
45
45
  try {
46
- const result = await awalExec(['status']);
46
+ // Read-only, safe to auto-retry after killing a wedged awal.
47
+ const result = await awalExecSafe(['status'], { timeoutMs: 8_000 });
47
48
  spinner.succeed('Wallet Status');
48
49
  console.log('');
49
50
  const data = result.data;
@@ -88,21 +89,48 @@ export async function walletBalanceCommand() {
88
89
  // Either half is allowed to fail — we print "(unavailable)" for the
89
90
  // broken section and keep going.
90
91
  const config = loadConfig();
92
+ // Wallet address lookup order:
93
+ // 1. ~/.clawmoney/config.yaml cache (instant)
94
+ // 2. Backend /api/v1/claw-agents/me (authoritative, ~200ms)
95
+ // 3. awal address (Electron cold-start, last resort, 5s cap)
96
+ // After a successful #2 we write the result back to the config so
97
+ // every future `wallet balance` hits path #1.
91
98
  let walletAddress = config?.wallet_address ?? null;
92
- // Fall back to awal only if we don't have a wallet address cached
93
- // in the config — e.g. for users who ran setup before we started
94
- // saving wallet_address. Capped at 5s so it can't block the command.
95
- let awalFallbackError = null;
99
+ let addressSource = 'config';
100
+ let addressError = null;
101
+ if (!walletAddress && config?.api_key) {
102
+ try {
103
+ const resp = await apiGet('/api/v1/claw-agents/me', config.api_key);
104
+ if (resp.ok && typeof resp.data?.wallet_address === 'string' && resp.data.wallet_address) {
105
+ walletAddress = resp.data.wallet_address;
106
+ addressSource = 'api';
107
+ // Cache it so the next run is instant.
108
+ try {
109
+ saveConfig({ wallet_address: walletAddress });
110
+ }
111
+ catch {
112
+ // Non-fatal — we still have the address for THIS run.
113
+ }
114
+ }
115
+ }
116
+ catch (err) {
117
+ addressError = err.message;
118
+ }
119
+ }
96
120
  if (!walletAddress) {
121
+ // Last resort: cold-start awal. Uses awalExecSafe so a wedged
122
+ // Electron is automatically killed + retried before surfacing
123
+ // the error. Capped at 6s per attempt.
97
124
  try {
98
- const awalResult = await withTimeout(awalExec(['address']), 5_000, 'awal address');
125
+ const awalResult = await awalExecSafe(['address'], { timeoutMs: 6_000 });
99
126
  const data = awalResult.data;
100
127
  if (typeof data?.address === 'string' && data.address) {
101
128
  walletAddress = data.address;
129
+ addressSource = 'awal';
102
130
  }
103
131
  }
104
132
  catch (err) {
105
- awalFallbackError = err.message;
133
+ addressError = err.message;
106
134
  }
107
135
  }
108
136
  const relayPromise = config?.api_key
@@ -121,8 +149,9 @@ export async function walletBalanceCommand() {
121
149
  }
122
150
  }
123
151
  else {
124
- onchainError = awalFallbackError ?? 'no wallet address in config';
152
+ onchainError = addressError ?? 'no wallet address in config';
125
153
  }
154
+ void addressSource; // reserved for future debug display
126
155
  const relayRows = await relayPromise;
127
156
  spinner.stop();
128
157
  console.log('');
@@ -160,7 +189,8 @@ export async function walletBalanceCommand() {
160
189
  export async function walletAddressCommand() {
161
190
  const spinner = ora('Getting wallet address...').start();
162
191
  try {
163
- const result = await awalExec(['address']);
192
+ // Read-only, safe to auto-retry on awal wedge.
193
+ const result = await awalExecSafe(['address'], { timeoutMs: 8_000 });
164
194
  spinner.succeed('Wallet Address');
165
195
  console.log('');
166
196
  const data = result.data;
@@ -1,5 +1,5 @@
1
1
  import { apiGet, apiPost } from "../utils/api.js";
2
- import { awalExec } from "../utils/awal.js";
2
+ import { awalExec, awalExecSafe } from "../utils/awal.js";
3
3
  import { requireConfig } from "../utils/config.js";
4
4
  const POLL_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
5
5
  const MAX_PER_CYCLE = 3;
@@ -11,7 +11,9 @@ function log(msg) {
11
11
  }
12
12
  async function getUsdcBalance() {
13
13
  try {
14
- const result = await awalExec(["balance"]);
14
+ // Read-only balance check inside a long-running daemon loop —
15
+ // must auto-recover if awal wedges during the day.
16
+ const result = await awalExecSafe(["balance"], { timeoutMs: 10_000 });
15
17
  const base = result.data.base;
16
18
  const balances = base?.balances;
17
19
  const usdc = balances?.USDC;
@@ -17,3 +17,29 @@ export declare function awalExecInteractive(args: string[]): Promise<string>;
17
17
  * Check if awal is installed and available.
18
18
  */
19
19
  export declare function isAwalAvailable(): Promise<boolean>;
20
+ /**
21
+ * Kill a wedged awal server process using the pattern documented in
22
+ * clawmoney-skill's SKILL.md. Reads the pid from `awal status --json`
23
+ * and SIGKILLs it. Safe to call even when awal isn't running (the
24
+ * inner command fails, kill -9 gets nothing, we swallow both).
25
+ *
26
+ * awal's Electron "Payments MCP" wrapper occasionally wedges on
27
+ * macOS (GPU render hang, stdin/stdout pipe full, or upstream
28
+ * Coinbase MCP endpoint unreachable). A hard kill lets the next
29
+ * `awal status` cold-start a fresh process.
30
+ */
31
+ export declare function killAwal(): Promise<void>;
32
+ /**
33
+ * Execute an awal command with timeout + one automatic retry on
34
+ * failure. On the first failure we kill any wedged awal process
35
+ * (using the SKILL-documented kill pattern) and re-run.
36
+ *
37
+ * Safe ONLY for READ operations (status, address, balance, etc).
38
+ * Do NOT use for writes (send, x402 pay, auth verify) — those
39
+ * either cost money twice on retry, or consume a single-use OTP
40
+ * and fail the second time. For writes, use awalExec directly
41
+ * and let the failure surface to the user.
42
+ */
43
+ export declare function awalExecSafe(args: string[], opts?: {
44
+ timeoutMs?: number;
45
+ }): Promise<AwalResult>;
@@ -98,3 +98,67 @@ export async function isAwalAvailable() {
98
98
  return false;
99
99
  }
100
100
  }
101
+ /**
102
+ * Kill a wedged awal server process using the pattern documented in
103
+ * clawmoney-skill's SKILL.md. Reads the pid from `awal status --json`
104
+ * and SIGKILLs it. Safe to call even when awal isn't running (the
105
+ * inner command fails, kill -9 gets nothing, we swallow both).
106
+ *
107
+ * awal's Electron "Payments MCP" wrapper occasionally wedges on
108
+ * macOS (GPU render hang, stdin/stdout pipe full, or upstream
109
+ * Coinbase MCP endpoint unreachable). A hard kill lets the next
110
+ * `awal status` cold-start a fresh process.
111
+ */
112
+ export async function killAwal() {
113
+ await new Promise((resolve) => {
114
+ const child = spawn('sh', [
115
+ '-c',
116
+ 'kill -9 $(npx awal status --json 2>/dev/null | grep -o \'"pid":[0-9]*\' | grep -o \'[0-9]*\') 2>/dev/null',
117
+ ], { stdio: 'ignore', shell: false });
118
+ child.on('exit', () => resolve());
119
+ child.on('error', () => resolve());
120
+ });
121
+ // Give the OS a moment to reclaim the process + named pipes.
122
+ await new Promise((r) => setTimeout(r, 800));
123
+ }
124
+ /**
125
+ * Execute an awal command with timeout + one automatic retry on
126
+ * failure. On the first failure we kill any wedged awal process
127
+ * (using the SKILL-documented kill pattern) and re-run.
128
+ *
129
+ * Safe ONLY for READ operations (status, address, balance, etc).
130
+ * Do NOT use for writes (send, x402 pay, auth verify) — those
131
+ * either cost money twice on retry, or consume a single-use OTP
132
+ * and fail the second time. For writes, use awalExec directly
133
+ * and let the failure surface to the user.
134
+ */
135
+ export async function awalExecSafe(args, opts = {}) {
136
+ const timeoutMs = opts.timeoutMs ?? 10_000;
137
+ const runWithTimeout = () => new Promise((resolve, reject) => {
138
+ const timer = setTimeout(() => {
139
+ reject(new Error(`awal ${args.join(' ')} timed out after ${timeoutMs}ms`));
140
+ }, timeoutMs);
141
+ awalExec(args).then((v) => {
142
+ clearTimeout(timer);
143
+ resolve(v);
144
+ }, (e) => {
145
+ clearTimeout(timer);
146
+ reject(e);
147
+ });
148
+ });
149
+ try {
150
+ return await runWithTimeout();
151
+ }
152
+ catch (firstErr) {
153
+ // First attempt failed — kill any wedged server and retry once.
154
+ await killAwal();
155
+ try {
156
+ return await runWithTimeout();
157
+ }
158
+ catch (secondErr) {
159
+ // Preserve the first error too — it's usually the more
160
+ // informative one (the retry typically just times out again).
161
+ throw new Error(`awal ${args.join(' ')} failed after retry: ${secondErr.message} (initial: ${firstErr.message})`);
162
+ }
163
+ }
164
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.15.38",
3
+ "version": "0.15.40",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {