skillpp 0.1.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 (208) hide show
  1. package/COMPATIBILITY.md +58 -0
  2. package/LICENSE +21 -0
  3. package/README.md +307 -0
  4. package/README.zh-CN.md +307 -0
  5. package/SKILL.md +490 -0
  6. package/adapters/binance-ai.md +22 -0
  7. package/adapters/claude.md +21 -0
  8. package/adapters/gemini.md +26 -0
  9. package/adapters/gpt.md +28 -0
  10. package/adapters/kimi.md +26 -0
  11. package/adapters/mimo.md +22 -0
  12. package/adapters/openclaw.md +29 -0
  13. package/assets/skillpp-banner.png +0 -0
  14. package/package.json +59 -0
  15. package/pipelines.md +310 -0
  16. package/prompts/newbie-mode.md +48 -0
  17. package/prompts/router-prompt.md +32 -0
  18. package/prompts/universal-system-prompt.md +41 -0
  19. package/registry.md +209 -0
  20. package/rules.md +323 -0
  21. package/schemas/audit.schema.json +67 -0
  22. package/schemas/checkpoint.schema.json +86 -0
  23. package/schemas/handoff.schema.json +82 -0
  24. package/schemas/token.schema.json +36 -0
  25. package/scripts/compatibility-check.mjs +130 -0
  26. package/scripts/selftest.mjs +384 -0
  27. package/scripts/skillpp.mjs +448 -0
  28. package/scripts/validate-skillpp.mjs +140 -0
  29. package/skillpp.manifest.json +714 -0
  30. package/skills/audit-plus/SKILL.md +612 -0
  31. package/skills/binance/binance/CHANGELOG.md +112 -0
  32. package/skills/binance/binance/LICENSE.md +9 -0
  33. package/skills/binance/binance/SKILL.md +69 -0
  34. package/skills/binance/binance/references/algo.md +21 -0
  35. package/skills/binance/binance/references/alpha.md +9 -0
  36. package/skills/binance/binance/references/auth.md +32 -0
  37. package/skills/binance/binance/references/c2c.md +5 -0
  38. package/skills/binance/binance/references/convert.md +19 -0
  39. package/skills/binance/binance/references/copy-trading.md +6 -0
  40. package/skills/binance/binance/references/crypto-loan.md +27 -0
  41. package/skills/binance/binance/references/derivatives-options-streams.md +25 -0
  42. package/skills/binance/binance/references/derivatives-options.md +85 -0
  43. package/skills/binance/binance/references/derivatives-portfolio-margin-pro-streams.md +5 -0
  44. package/skills/binance/binance/references/derivatives-portfolio-margin-pro.md +34 -0
  45. package/skills/binance/binance/references/derivatives-portfolio-margin-streams.md +5 -0
  46. package/skills/binance/binance/references/derivatives-portfolio-margin.md +146 -0
  47. package/skills/binance/binance/references/dual-investment.md +15 -0
  48. package/skills/binance/binance/references/fiat.md +9 -0
  49. package/skills/binance/binance/references/futures-coin-streams.md +29 -0
  50. package/skills/binance/binance/references/futures-coin.md +109 -0
  51. package/skills/binance/binance/references/futures-usds-streams.md +35 -0
  52. package/skills/binance/binance/references/futures-usds.md +144 -0
  53. package/skills/binance/binance/references/gift-card.md +10 -0
  54. package/skills/binance/binance/references/margin-trading-streams.md +6 -0
  55. package/skills/binance/binance/references/margin-trading.md +101 -0
  56. package/skills/binance/binance/references/mining.md +17 -0
  57. package/skills/binance/binance/references/pay.md +5 -0
  58. package/skills/binance/binance/references/rebate.md +5 -0
  59. package/skills/binance/binance/references/simple-earn.md +56 -0
  60. package/skills/binance/binance/references/spot-streams.md +25 -0
  61. package/skills/binance/binance/references/spot.md +114 -0
  62. package/skills/binance/binance/references/staking.md +59 -0
  63. package/skills/binance/binance/references/sub-account.md +67 -0
  64. package/skills/binance/binance/references/vip-loan.md +27 -0
  65. package/skills/binance/binance/references/wallet.md +75 -0
  66. package/skills/binance/fiat/CHANGELOG.md +11 -0
  67. package/skills/binance/fiat/LICENSE.md +9 -0
  68. package/skills/binance/fiat/SKILL.md +169 -0
  69. package/skills/binance/fiat/references/authentication.md +126 -0
  70. package/skills/binance/fiat/references/sapi-endpoints.md +217 -0
  71. package/skills/binance/onchain-pay/.local.md.example +10 -0
  72. package/skills/binance/onchain-pay/CHANGELOG.md +20 -0
  73. package/skills/binance/onchain-pay/LICENSE.md +9 -0
  74. package/skills/binance/onchain-pay/SKILL.md +466 -0
  75. package/skills/binance/onchain-pay/references/authentication.md +92 -0
  76. package/skills/binance/onchain-pay/scripts/sign_and_call.sh +52 -0
  77. package/skills/binance/p2p/CHANGELOG.md +33 -0
  78. package/skills/binance/p2p/LICENSE.md +9 -0
  79. package/skills/binance/p2p/SKILL.md +1082 -0
  80. package/skills/binance/p2p/references/agent-sapi-api.md +795 -0
  81. package/skills/binance/p2p/references/authentication.md +100 -0
  82. package/skills/binance/payment/SKILL.md +824 -0
  83. package/skills/binance/payment/common.py +560 -0
  84. package/skills/binance/payment/payment_skill.py +86 -0
  85. package/skills/binance/payment/receive.py +109 -0
  86. package/skills/binance/payment/references/setup-guide.md +77 -0
  87. package/skills/binance/payment/requirements.txt +4 -0
  88. package/skills/binance/payment/send.py +952 -0
  89. package/skills/binance/payment/send_extension/__init__.py +43 -0
  90. package/skills/binance/payment/send_extension/base.py +48 -0
  91. package/skills/binance/payment/send_extension/c2c.py +193 -0
  92. package/skills/binance/payment/send_extension/pix.py +316 -0
  93. package/skills/binance/square-post/README.md +62 -0
  94. package/skills/binance/square-post/SKILL.md +171 -0
  95. package/skills/binance/square-post/scripts/lib.mjs +175 -0
  96. package/skills/binance/square-post/scripts/post-image.mjs +80 -0
  97. package/skills/binance/square-post/scripts/post-text.mjs +41 -0
  98. package/skills/binance/square-post/scripts/post-video.mjs +110 -0
  99. package/skills/binance/square-post/scripts/save-key.mjs +34 -0
  100. package/skills/binance-web3/binance-agentic-wallet/SKILL.md +150 -0
  101. package/skills/binance-web3/binance-agentic-wallet/references/authentication.md +136 -0
  102. package/skills/binance-web3/binance-agentic-wallet/references/limit-order.md +204 -0
  103. package/skills/binance-web3/binance-agentic-wallet/references/market-order.md +179 -0
  104. package/skills/binance-web3/binance-agentic-wallet/references/prediction.md +489 -0
  105. package/skills/binance-web3/binance-agentic-wallet/references/preflight.md +66 -0
  106. package/skills/binance-web3/binance-agentic-wallet/references/security.md +47 -0
  107. package/skills/binance-web3/binance-agentic-wallet/references/send.md +53 -0
  108. package/skills/binance-web3/binance-agentic-wallet/references/wallet-setting.md +86 -0
  109. package/skills/binance-web3/binance-agentic-wallet/references/wallet-view.md +312 -0
  110. package/skills/binance-web3/binance-agentic-wallet/references/x402-payment.md +259 -0
  111. package/skills/binance-web3/binance-tokenized-securities-info/SKILL.md +613 -0
  112. package/skills/binance-web3/crypto-market-rank/SKILL.md +91 -0
  113. package/skills/binance-web3/crypto-market-rank/references/cli.md +219 -0
  114. package/skills/binance-web3/crypto-market-rank/scripts/cli.mjs +149 -0
  115. package/skills/binance-web3/meme-rush/SKILL.md +72 -0
  116. package/skills/binance-web3/meme-rush/references/cli.md +158 -0
  117. package/skills/binance-web3/meme-rush/scripts/cli.mjs +101 -0
  118. package/skills/binance-web3/query-address-info/SKILL.md +61 -0
  119. package/skills/binance-web3/query-address-info/references/cli.md +56 -0
  120. package/skills/binance-web3/query-address-info/scripts/cli.mjs +132 -0
  121. package/skills/binance-web3/query-token-audit/SKILL.md +162 -0
  122. package/skills/binance-web3/query-token-info/SKILL.md +83 -0
  123. package/skills/binance-web3/query-token-info/references/cli.md +135 -0
  124. package/skills/binance-web3/query-token-info/scripts/cli.mjs +112 -0
  125. package/skills/binance-web3/trading-signal/SKILL.md +66 -0
  126. package/skills/binance-web3/trading-signal/references/cli.md +90 -0
  127. package/skills/binance-web3/trading-signal/scripts/cli.mjs +92 -0
  128. package/skills/four-meme/four-guard/API-Contract-TaxToken.md +277 -0
  129. package/skills/four-meme/four-guard/API-CreateToken.02-02-2026.md +285 -0
  130. package/skills/four-meme/four-guard/API-Documents.03-03-2026.md +789 -0
  131. package/skills/four-meme/four-guard/AgentIdentifier.abi +585 -0
  132. package/skills/four-meme/four-guard/README.md +21 -0
  133. package/skills/four-meme/four-guard/SKILL.md +31 -0
  134. package/skills/four-meme/four-guard/TaxToken.abi +969 -0
  135. package/skills/four-meme/four-guard/TokenIdentifierSample.js_ +81 -0
  136. package/skills/four-meme/four-guard/TokenIdentifierSample.sol +69 -0
  137. package/skills/four-meme/four-guard/TokenManager.lite.abi +836 -0
  138. package/skills/four-meme/four-guard/TokenManager2.lite.abi +2325 -0
  139. package/skills/four-meme/four-guard/TokenManagerHelper3.abi +999 -0
  140. package/skills/four-meme/four-guard/go.mod +36 -0
  141. package/skills/four-meme/four-guard/go.sum +127 -0
  142. package/skills/four-meme/four-guard/main.go +183 -0
  143. package/skills/four-meme/four-meme-ai/SKILL.md +31 -0
  144. package/skills/four-meme/four-meme-ai/references/agent-creator-and-wallets.md +87 -0
  145. package/skills/four-meme/four-meme-ai/references/api-create-token.md +55 -0
  146. package/skills/four-meme/four-meme-ai/references/contract-addresses.md +47 -0
  147. package/skills/four-meme/four-meme-ai/references/create-token-scripts.md +131 -0
  148. package/skills/four-meme/four-meme-ai/references/errors.md +29 -0
  149. package/skills/four-meme/four-meme-ai/references/event-listening.md +75 -0
  150. package/skills/four-meme/four-meme-ai/references/execute-trade.md +31 -0
  151. package/skills/four-meme/four-meme-ai/references/tax-token-query.md +38 -0
  152. package/skills/four-meme/four-meme-ai/references/token-query-api.md +44 -0
  153. package/skills/four-meme/four-meme-ai/references/token-tax-info.md +77 -0
  154. package/skills/four-meme/four-meme-ai/scripts/8004-balance.ts +52 -0
  155. package/skills/four-meme/four-meme-ai/scripts/8004-register.ts +108 -0
  156. package/skills/four-meme/four-meme-ai/scripts/create-token-api.ts +321 -0
  157. package/skills/four-meme/four-meme-ai/scripts/create-token-chain.ts +102 -0
  158. package/skills/four-meme/four-meme-ai/scripts/create-token-instant.ts +106 -0
  159. package/skills/four-meme/four-meme-ai/scripts/execute-buy.ts +198 -0
  160. package/skills/four-meme/four-meme-ai/scripts/execute-sell.ts +150 -0
  161. package/skills/four-meme/four-meme-ai/scripts/get-public-config.ts +25 -0
  162. package/skills/four-meme/four-meme-ai/scripts/get-recent-events.ts +76 -0
  163. package/skills/four-meme/four-meme-ai/scripts/get-tax-token-info.ts +69 -0
  164. package/skills/four-meme/four-meme-ai/scripts/get-token-info.ts +94 -0
  165. package/skills/four-meme/four-meme-ai/scripts/quote-buy.ts +85 -0
  166. package/skills/four-meme/four-meme-ai/scripts/quote-sell.ts +66 -0
  167. package/skills/four-meme/four-meme-ai/scripts/send-token.ts +98 -0
  168. package/skills/four-meme/four-meme-ai/scripts/token-get.ts +31 -0
  169. package/skills/four-meme/four-meme-ai/scripts/token-list.ts +134 -0
  170. package/skills/four-meme/four-meme-ai/scripts/token-rankings.ts +162 -0
  171. package/skills/four-meme/four-meme-ai/scripts/verify-events.ts +47 -0
  172. package/skills/four-meme/four-meme-integration/SKILL.md +374 -0
  173. package/skills/four-meme/four-meme-integration/references/agent-creator-and-wallets.md +87 -0
  174. package/skills/four-meme/four-meme-integration/references/api-create-token.md +55 -0
  175. package/skills/four-meme/four-meme-integration/references/contract-addresses.md +47 -0
  176. package/skills/four-meme/four-meme-integration/references/create-token-scripts.md +131 -0
  177. package/skills/four-meme/four-meme-integration/references/errors.md +29 -0
  178. package/skills/four-meme/four-meme-integration/references/event-listening.md +75 -0
  179. package/skills/four-meme/four-meme-integration/references/execute-trade.md +31 -0
  180. package/skills/four-meme/four-meme-integration/references/tax-token-query.md +38 -0
  181. package/skills/four-meme/four-meme-integration/references/token-query-api.md +44 -0
  182. package/skills/four-meme/four-meme-integration/references/token-tax-info.md +77 -0
  183. package/skills/four-meme/four-meme-integration/scripts/8004-balance.ts +52 -0
  184. package/skills/four-meme/four-meme-integration/scripts/8004-register.ts +108 -0
  185. package/skills/four-meme/four-meme-integration/scripts/create-token-api.ts +321 -0
  186. package/skills/four-meme/four-meme-integration/scripts/create-token-chain.ts +102 -0
  187. package/skills/four-meme/four-meme-integration/scripts/create-token-instant.ts +106 -0
  188. package/skills/four-meme/four-meme-integration/scripts/execute-buy.ts +198 -0
  189. package/skills/four-meme/four-meme-integration/scripts/execute-sell.ts +150 -0
  190. package/skills/four-meme/four-meme-integration/scripts/get-public-config.ts +25 -0
  191. package/skills/four-meme/four-meme-integration/scripts/get-recent-events.ts +76 -0
  192. package/skills/four-meme/four-meme-integration/scripts/get-tax-token-info.ts +69 -0
  193. package/skills/four-meme/four-meme-integration/scripts/get-token-info.ts +94 -0
  194. package/skills/four-meme/four-meme-integration/scripts/quote-buy.ts +85 -0
  195. package/skills/four-meme/four-meme-integration/scripts/quote-sell.ts +66 -0
  196. package/skills/four-meme/four-meme-integration/scripts/send-token.ts +98 -0
  197. package/skills/four-meme/four-meme-integration/scripts/token-get.ts +31 -0
  198. package/skills/four-meme/four-meme-integration/scripts/token-list.ts +134 -0
  199. package/skills/four-meme/four-meme-integration/scripts/token-rankings.ts +162 -0
  200. package/skills/four-meme/four-meme-integration/scripts/verify-events.ts +47 -0
  201. package/skills/skillpp/contract-profiler/SKILL.md +118 -0
  202. package/skills/skillpp/newbie-tutor/SKILL.md +85 -0
  203. package/skills/skillpp/opportunity-board/SKILL.md +87 -0
  204. package/skills/skillpp/risk-fusion/SKILL.md +146 -0
  205. package/skills/skillpp/scam-pattern-lab/SKILL.md +115 -0
  206. package/skills/skillpp/wallet-doctor/SKILL.md +119 -0
  207. package/skills/skillpp/watchtower/SKILL.md +72 -0
  208. package/tests/compatibility/v0.1.0.json +117 -0
@@ -0,0 +1,952 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Payment Assistant - Send Actions
4
+
5
+ All send/pay action functions + QRCodeHandler.
6
+ Extracted from payment_skill.py — logic unchanged.
7
+ """
8
+ import os
9
+ import json
10
+ import subprocess
11
+ import platform
12
+ import time
13
+ from typing import Dict, Any, Optional
14
+
15
+ try:
16
+ import qrcode
17
+ HAS_QRCODE = True
18
+ except ImportError:
19
+ HAS_QRCODE = False
20
+
21
+ try:
22
+ from PIL import Image
23
+ HAS_PIL = True
24
+ except ImportError:
25
+ HAS_PIL = False
26
+
27
+ try:
28
+ from pyzbar.pyzbar import decode as pyzbar_decode
29
+ HAS_PYZBAR = True
30
+ except ImportError:
31
+ HAS_PYZBAR = False
32
+
33
+ try:
34
+ import cv2
35
+ HAS_CV2 = True
36
+ except ImportError:
37
+ HAS_CV2 = False
38
+
39
+ from common import (
40
+ OrderStatus, SKILLS_ERROR_CODES,
41
+ SKILL_DIR, CONFIG_FILE_PATH, STATE_FILE_PATH, QR_CODE_OUTPUT_PATH, INBOX_DIR, CLIPBOARD_IMAGE_PATH,
42
+ API_KEY_GUIDE_MESSAGE,
43
+ load_config, is_config_ready, show_config_guide, validate_config,
44
+ load_state, update_state, set_order_status, get_order_status, clear_state, get_status_hint,
45
+ PaymentAPI,
46
+ )
47
+ from send_extension import detect_extension, get_extension_by_type, get_all_endpoints
48
+
49
+ # API Endpoints - aggregated from all extensions
50
+ ENDPOINTS = get_all_endpoints()
51
+
52
+
53
+ # ============================================================
54
+ # State helpers dict - passed to extension.purchase()
55
+ # ============================================================
56
+ def _get_state_helpers() -> Dict[str, Any]:
57
+ """Build the state_helpers dict that extensions use to manage state."""
58
+ return {
59
+ 'set_order_status': set_order_status,
60
+ 'update_state': update_state,
61
+ 'OrderStatus': OrderStatus,
62
+ }
63
+
64
+
65
+ # ============================================================
66
+ # QR Code Handler
67
+ # ============================================================
68
+ class QRCodeHandler:
69
+ """Handle QR code generation, decoding, and clipboard/inbox image operations."""
70
+
71
+ @staticmethod
72
+ def generate_qr_image(qr_string: str, output_path: str = QR_CODE_OUTPUT_PATH) -> Optional[str]:
73
+ """Generate QR code image from string"""
74
+ if not HAS_QRCODE:
75
+ return None
76
+ try:
77
+ qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=2)
78
+ qr.add_data(qr_string)
79
+ qr.make(fit=True)
80
+ img = qr.make_image(fill_color="black", back_color="white")
81
+ img.save(output_path)
82
+ return output_path
83
+ except:
84
+ return None
85
+
86
+ @staticmethod
87
+ def decode_qr_from_image(image_path: str) -> Optional[str]:
88
+ """Decode QR code from image file. Tries pyzbar first, then opencv."""
89
+ # Try pyzbar first
90
+ if HAS_PIL and HAS_PYZBAR:
91
+ try:
92
+ img = Image.open(image_path)
93
+ decoded = pyzbar_decode(img)
94
+ if decoded:
95
+ return decoded[0].data.decode('utf-8')
96
+ except Exception:
97
+ pass
98
+
99
+ # Fallback to OpenCV
100
+ if HAS_CV2:
101
+ try:
102
+ img = cv2.imread(image_path)
103
+ if img is not None:
104
+ detector = cv2.QRCodeDetector()
105
+ data, _, _ = detector.detectAndDecode(img)
106
+ if data:
107
+ return data
108
+ except Exception:
109
+ pass
110
+
111
+ return None
112
+
113
+ @staticmethod
114
+ def save_clipboard_image_macos(output_path: str) -> bool:
115
+ """Save clipboard image to file on macOS using osascript"""
116
+ try:
117
+ script = f'''
118
+ set theFile to POSIX file "{output_path}"
119
+ try
120
+ set imgData to the clipboard as «class PNGf»
121
+ set fileRef to open for access theFile with write permission
122
+ write imgData to fileRef
123
+ close access fileRef
124
+ return "success"
125
+ on error
126
+ return "no_image"
127
+ end try
128
+ '''
129
+ result = subprocess.run(['osascript', '-e', script], capture_output=True, text=True, timeout=5)
130
+ return 'success' in result.stdout
131
+ except:
132
+ return False
133
+
134
+ @staticmethod
135
+ def save_clipboard_image_linux(output_path: str) -> bool:
136
+ """Save clipboard image to file on Linux using xclip"""
137
+ try:
138
+ result = subprocess.run(
139
+ ['xclip', '-selection', 'clipboard', '-t', 'image/png', '-o'],
140
+ capture_output=True, timeout=5
141
+ )
142
+ if result.returncode == 0 and result.stdout:
143
+ with open(output_path, 'wb') as f:
144
+ f.write(result.stdout)
145
+ return True
146
+ except:
147
+ pass
148
+ return False
149
+
150
+ @staticmethod
151
+ def save_clipboard_image_windows(output_path: str) -> bool:
152
+ """Save clipboard image to file on Windows"""
153
+ try:
154
+ script = f'''
155
+ Add-Type -AssemblyName System.Windows.Forms
156
+ $img = [System.Windows.Forms.Clipboard]::GetImage()
157
+ if ($img) {{
158
+ $img.Save("{output_path}")
159
+ Write-Output "success"
160
+ }} else {{
161
+ Write-Output "no_image"
162
+ }}
163
+ '''
164
+ result = subprocess.run(['powershell', '-Command', script], capture_output=True, text=True, timeout=5)
165
+ return 'success' in result.stdout
166
+ except:
167
+ return False
168
+
169
+ @staticmethod
170
+ def save_clipboard_image(output_path: str) -> bool:
171
+ """Save clipboard image to file (cross-platform)"""
172
+ system = platform.system().lower()
173
+ if system == 'darwin':
174
+ return QRCodeHandler.save_clipboard_image_macos(output_path)
175
+ elif system == 'linux':
176
+ return QRCodeHandler.save_clipboard_image_linux(output_path)
177
+ elif system == 'windows':
178
+ return QRCodeHandler.save_clipboard_image_windows(output_path)
179
+ return False
180
+
181
+ @staticmethod
182
+ def decode_qr_from_clipboard() -> tuple:
183
+ """
184
+ Decode QR from clipboard image.
185
+ Returns: (success: bool, qr_data: str or None, message: str)
186
+ """
187
+ os.makedirs(INBOX_DIR, exist_ok=True)
188
+
189
+ if not QRCodeHandler.save_clipboard_image(CLIPBOARD_IMAGE_PATH):
190
+ return False, None, "clipboard_no_image"
191
+
192
+ qr_data = QRCodeHandler.decode_qr_from_image(CLIPBOARD_IMAGE_PATH)
193
+ if qr_data:
194
+ return True, qr_data, "success"
195
+ else:
196
+ return False, None, "decode_failed"
197
+
198
+ @staticmethod
199
+ def parse_emvco_qr(qr_string: str) -> Dict[str, str]:
200
+ """Parse EMVCo QR code format to extract merchant info"""
201
+ result = {}
202
+ try:
203
+ if '5918' in qr_string:
204
+ idx = qr_string.index('5918') + 4
205
+ result['merchant_name'] = qr_string[idx:idx+18].strip()
206
+ if '6012' in qr_string:
207
+ idx = qr_string.index('6012') + 4
208
+ result['merchant_city'] = qr_string[idx:idx+12].strip()
209
+ if '5802' in qr_string:
210
+ idx = qr_string.index('5802') + 4
211
+ result['country_code'] = qr_string[idx:idx+2]
212
+ except:
213
+ pass
214
+ return result
215
+
216
+
217
+ # ============================================================
218
+ # Actions
219
+ # ============================================================
220
+ def action_config():
221
+ """Show configuration status and guide user to complete setup"""
222
+ config = load_config()
223
+ is_valid, missing = validate_config(config)
224
+ file_exists = os.path.exists(CONFIG_FILE_PATH)
225
+
226
+ print()
227
+ print("════════════════════════════════════════════════════")
228
+ print("⚙️ Configuration Status")
229
+ print("════════════════════════════════════════════════════")
230
+ print()
231
+ print(f"📁 Config file: {CONFIG_FILE_PATH}")
232
+ print(f" Status: {'✅ Exists' if file_exists else '❌ Not found'}")
233
+ print()
234
+
235
+ base_url_ok = config.get('base_url') and len(config.get('base_url', '')) > 0
236
+ api_key_ok = config.get('api_key') and len(config.get('api_key', '')) > 0
237
+ api_secret_ok = config.get('api_secret') and len(config.get('api_secret', '')) > 0
238
+
239
+ print("📊 Current Settings:")
240
+ print(f" base_url: {'✅ ' + config.get('base_url', '') + ' (auto)' if base_url_ok else '❌ Not set'}")
241
+ print(f" api_key: {'✅ ****' + config.get('api_key', '')[-4:] if api_key_ok else '❌ Not set'}")
242
+ print(f" api_secret: {'✅ ****' + config.get('api_secret', '')[-4:] if api_secret_ok else '❌ Not set'}")
243
+ print()
244
+
245
+ if is_valid:
246
+ print("════════════════════════════════════════════════════")
247
+ print("✅ Ready")
248
+ print("════════════════════════════════════════════════════")
249
+ print(" All credentials are configured.")
250
+ else:
251
+ print("════════════════════════════════════════════════════")
252
+ print("⚠️ Setup Required")
253
+ print("════════════════════════════════════════════════════")
254
+ print(f" Missing: {', '.join(missing)}")
255
+ print()
256
+ print(f"📝 Please edit: {CONFIG_FILE_PATH}")
257
+ print()
258
+ print(" Required fields:")
259
+ if 'api_key' in missing:
260
+ print(" • api_key: Your API key")
261
+ if 'api_secret' in missing:
262
+ print(" • api_secret: Your API secret")
263
+ print()
264
+ print("📝 Configuration Template:")
265
+ print(' {')
266
+ print(' "configured": true,')
267
+ print(' "api_key": "YOUR_API_KEY",')
268
+ print(' "api_secret": "YOUR_API_SECRET"')
269
+ print(' }')
270
+ print()
271
+ print(f"🔑 {API_KEY_GUIDE_MESSAGE}")
272
+ print()
273
+ print(" Or use environment variables:")
274
+ print(" export PAYMENT_API_KEY='your_key'")
275
+ print(" export PAYMENT_API_SECRET='your_secret'")
276
+
277
+ print("════════════════════════════════════════════════════")
278
+ print()
279
+
280
+ print(json.dumps({
281
+ 'config_exists': file_exists,
282
+ 'is_valid': is_valid,
283
+ 'missing_fields': missing,
284
+ 'config_path': CONFIG_FILE_PATH
285
+ }))
286
+
287
+
288
+ def action_purchase(config: Dict[str, Any], raw_qr: str):
289
+ """
290
+ Unified Purchase Flow - Step 1: Parse QR
291
+
292
+ Auto-detects QR type and delegates to the matching extension.
293
+ """
294
+ if not raw_qr:
295
+ print()
296
+ print("❌ Missing QR code")
297
+ print("💡 Please provide QR code data with --raw_qr parameter")
298
+ print()
299
+ return
300
+
301
+ is_ready, reason, missing_fields = is_config_ready(config)
302
+ if not is_ready:
303
+ show_config_guide(config, reason, missing_fields)
304
+ return
305
+
306
+ # Detect QR type via extension registry
307
+ ext = detect_extension(raw_qr)
308
+ api = PaymentAPI(config)
309
+
310
+ print()
311
+ print("════════════════════════════════════════════════════")
312
+ print(f"📦 Starting {ext.payment_type} Purchase Flow")
313
+ print("════════════════════════════════════════════════════")
314
+ print()
315
+
316
+ # Initialize state with payment type
317
+ update_state({
318
+ 'raw_qr': raw_qr,
319
+ 'payment_type': ext.payment_type,
320
+ 'order_status': OrderStatus.INIT.value
321
+ })
322
+
323
+ # Delegate to extension
324
+ ext.purchase(api, raw_qr, _get_state_helpers())
325
+
326
+
327
+ def action_set_amount(amount: float, currency: str = None):
328
+ """Set payment amount (and optionally currency) for orders without preset amount"""
329
+ state = load_state()
330
+
331
+ if not state.get('checkout_id'):
332
+ print()
333
+ print("❌ No active order")
334
+ print("💡 Run '--action purchase --raw_qr <QR_DATA>' first")
335
+ print()
336
+ return
337
+
338
+ # Block amount change if PIX QR has a locked amount
339
+ if state.get('pix_amount_locked'):
340
+ preset = state.get('preset_amount') or state.get('suggested_amount')
341
+ cur = state.get('currency', 'BRL')
342
+ print()
343
+ print("════════════════════════════════════════════════════")
344
+ print("❌ Cannot change amount")
345
+ print("════════════════════════════════════════════════════")
346
+ print(f" This PIX QR code has a fixed amount: {preset} {cur}")
347
+ print(" The amount is embedded in the QR code and cannot be modified.")
348
+ print()
349
+ print("💡 Reply 'y' to confirm payment with the QR amount, 'n' to cancel")
350
+ print()
351
+ print(json.dumps({
352
+ 'status': 'AMOUNT_LOCKED',
353
+ 'message': f'PIX QR has fixed amount: {preset} {cur}. Cannot be modified.',
354
+ 'locked_amount': str(preset),
355
+ 'currency': cur
356
+ }))
357
+ return
358
+
359
+ final_currency = currency or state.get('currency', 'USDT')
360
+
361
+ set_order_status(OrderStatus.AMOUNT_SET,
362
+ suggested_amount=amount,
363
+ currency=final_currency,
364
+ needs_amount_input=False
365
+ )
366
+
367
+ print()
368
+ print(f"✅ Amount set: {amount} {final_currency}")
369
+ print()
370
+ print("💡 Reply 'y' to confirm, 'n' to cancel")
371
+ print(json.dumps({
372
+ 'status': 'AMOUNT_SET',
373
+ 'amount': amount,
374
+ 'currency': final_currency,
375
+ 'checkout_id': state.get('checkout_id'),
376
+ 'payee': state.get('nickname')
377
+ }))
378
+
379
+
380
+ def action_pay_confirm(config: Dict[str, Any], amount: float = None, currency: str = None):
381
+ """
382
+ Payment Flow - Step 2: Confirm Payment
383
+
384
+ Routes to the correct extension endpoint based on payment_type in state.
385
+ """
386
+ is_ready, reason, missing_fields = is_config_ready(config)
387
+ if not is_ready:
388
+ show_config_guide(config, reason, missing_fields)
389
+ return
390
+
391
+ state = load_state()
392
+
393
+ # Safety check: Prevent duplicate payment
394
+ current_status = state.get('order_status')
395
+ if current_status in [OrderStatus.SUCCESS.value, OrderStatus.PAYMENT_CONFIRMED.value, OrderStatus.POLLING.value]:
396
+ print()
397
+ print("════════════════════════════════════════════════════")
398
+ print("⚠️ Payment Already In Progress or Complete")
399
+ print("════════════════════════════════════════════════════")
400
+ print(f" Current status: {current_status}")
401
+ if current_status == OrderStatus.SUCCESS.value:
402
+ print(" This order has already been paid successfully.")
403
+ else:
404
+ print(" Payment is in progress. Run --action poll to check result.")
405
+ print()
406
+ print("💡 Run: --action status to check current state")
407
+ print(" Run: --action reset to start a new payment")
408
+ print()
409
+ return
410
+
411
+ if not state.get('checkout_id'):
412
+ print()
413
+ print("❌ No active order")
414
+ print("💡 Run '--action purchase --raw_qr <QR_DATA>' first")
415
+ print()
416
+ return
417
+
418
+ # If PIX amount is locked, force use the QR amount regardless of user input
419
+ if state.get('pix_amount_locked'):
420
+ locked_amount = state.get('preset_amount') or state.get('suggested_amount')
421
+ if locked_amount is not None:
422
+ if amount is not None and float(amount) != float(locked_amount):
423
+ print(f"⚠️ PIX QR has fixed amount {locked_amount} {state.get('currency', 'BRL')}. Ignoring user amount {amount}.")
424
+ amount = float(locked_amount)
425
+ currency = state.get('currency', 'BRL')
426
+
427
+ if amount is None:
428
+ amount = state.get('suggested_amount')
429
+
430
+ if amount is None:
431
+ print()
432
+ print("❌ No amount specified")
433
+ print("💡 Use: --action set_amount --amount <amount> [--currency <currency>]")
434
+ print()
435
+ return
436
+
437
+ final_currency = currency or state.get('currency', 'USDT')
438
+
439
+ # Get the right extension for this payment type
440
+ payment_type = state.get('payment_type', 'C2C')
441
+ ext = get_extension_by_type(payment_type)
442
+ api = PaymentAPI(config)
443
+
444
+ payee = state.get('nickname', 'Unknown')
445
+ amount_str = str(int(amount)) if amount == int(amount) else str(amount)
446
+
447
+ print()
448
+ print("════════════════════════════════════════════════════")
449
+ print(f"💳 [Step 2] Confirming {payment_type} Payment")
450
+ print("════════════════════════════════════════════════════")
451
+ print(f" Checkout: {state.get('checkout_id')}")
452
+ print(f" Amount: {amount_str} {final_currency}")
453
+ print(f" Payee: {payee}")
454
+ print()
455
+
456
+ # Build params via extension and call API
457
+ print("🔄 Processing payment...")
458
+ confirm_params = ext.build_confirm_params(state, amount_str, final_currency)
459
+ confirm_result = api.confirm_payment(ext.get_confirm_endpoint(), confirm_params)
460
+
461
+ if not confirm_result['success']:
462
+ error_status = confirm_result.get('status', 'ERROR')
463
+ error_msg = confirm_result.get('message', 'Payment failed')
464
+ error_hint = confirm_result.get('hint', '')
465
+ error_code = confirm_result.get('code')
466
+
467
+ set_order_status(OrderStatus.FAILED, error_message=error_msg, error_code=error_code)
468
+
469
+ print()
470
+ print("════════════════════════════════════════════════════")
471
+ print(f"❌ Payment Failed")
472
+ print("════════════════════════════════════════════════════")
473
+ print(f" {error_msg}")
474
+ if error_hint:
475
+ print(f" 💡 {error_hint}")
476
+ print("════════════════════════════════════════════════════")
477
+
478
+ print(json.dumps({
479
+ 'status': error_status,
480
+ 'code': error_code,
481
+ 'message': error_msg,
482
+ 'hint': error_hint
483
+ }))
484
+ return
485
+
486
+ payment_info = confirm_result['payment_info']
487
+
488
+ set_order_status(OrderStatus.PAYMENT_CONFIRMED,
489
+ pay_order_id=payment_info.pay_order_id,
490
+ amount=amount,
491
+ currency=final_currency,
492
+ usd_amount=str(payment_info.usd_amount) if payment_info.usd_amount else None,
493
+ daily_used_before=str(payment_info.daily_used_before) if payment_info.daily_used_before is not None else None,
494
+ daily_used_after=str(payment_info.daily_used_after) if payment_info.daily_used_after is not None else None
495
+ )
496
+
497
+ print("✅ Payment confirmed, processing...")
498
+ print(f" Pay Order ID: {payment_info.pay_order_id}")
499
+ if payment_info.usd_amount:
500
+ print(f" USD Amount: {payment_info.usd_amount}")
501
+ daily_limit = state.get('daily_limit')
502
+ if payment_info.daily_used_before is not None and payment_info.daily_used_after is not None and daily_limit:
503
+ print(f" Daily Usage: {payment_info.daily_used_before} → {payment_info.daily_used_after} / {daily_limit} USD")
504
+ elif payment_info.daily_used_after is not None and daily_limit:
505
+ print(f" Daily Usage: {payment_info.daily_used_after} / {daily_limit} USD")
506
+
507
+ print(json.dumps({
508
+ 'status': 'PROCESSING',
509
+ 'pay_order_id': payment_info.pay_order_id,
510
+ 'amount': amount,
511
+ 'currency': final_currency,
512
+ 'payee': payee,
513
+ 'usd_amount': str(payment_info.usd_amount) if payment_info.usd_amount else None,
514
+ 'daily_used_before': str(payment_info.daily_used_before) if payment_info.daily_used_before is not None else None,
515
+ 'daily_used_after': str(payment_info.daily_used_after) if payment_info.daily_used_after is not None else None,
516
+ 'daily_limit': daily_limit
517
+ }))
518
+
519
+
520
+ def action_poll(config: Dict[str, Any]):
521
+ """Payment Flow - Step 3: Poll payment status until final result"""
522
+ state = load_state()
523
+ pay_order_id = state.get('pay_order_id')
524
+
525
+ if not pay_order_id:
526
+ print()
527
+ print("❌ No active payment")
528
+ print()
529
+ return
530
+
531
+ # Get the right extension for this payment type
532
+ payment_type = state.get('payment_type', 'C2C')
533
+ ext = get_extension_by_type(payment_type)
534
+ api = PaymentAPI(config)
535
+
536
+ print()
537
+ print("🔍 Querying order status...")
538
+
539
+ poll_params = ext.build_poll_params(state)
540
+ status_result = api.query_payment_status(ext.get_poll_endpoint(), poll_params)
541
+
542
+ if not status_result['success']:
543
+ print(f"❌ Query failed: {status_result.get('message', '')}")
544
+ return
545
+
546
+ status_info = status_result['status_info']
547
+ status_icon = '✅' if status_info.status == 'SUCCESS' else ('❌' if status_info.status in ['FAILED', 'FAIL'] else '⏳')
548
+ status_text = 'Success' if status_info.status == 'SUCCESS' else ('Failed' if status_info.status in ['FAILED', 'FAIL'] else 'Processing')
549
+
550
+ print()
551
+ print("════════════════════════════════════════════════════")
552
+ print(f"{status_icon} Status: {status_text}")
553
+ print("════════════════════════════════════════════════════")
554
+ print(f" 📝 Pay Order: {pay_order_id}")
555
+ if state.get('amount'):
556
+ print(f" 💵 Amount Sent: {state['amount']} {state.get('currency', 'USDT')}")
557
+ if status_info.asset_cost_vos:
558
+ costs = [f"{vo['amount']} {vo['asset']}" for vo in status_info.asset_cost_vos]
559
+ print(f" 💳 Paid With: {' + '.join(costs)}")
560
+ # Show daily usage change on success
561
+ daily_used_before = state.get('daily_used_before')
562
+ daily_used_after = state.get('daily_used_after')
563
+ daily_limit = state.get('daily_limit')
564
+ if status_info.status == 'SUCCESS' and daily_used_before is not None and daily_used_after is not None and daily_limit:
565
+ print(f" 📊 Daily Usage: {daily_used_before} → {daily_used_after} / {daily_limit} USD")
566
+ print("════════════════════════════════════════════════════")
567
+ print(json.dumps({
568
+ 'status': status_info.status,
569
+ 'pay_order_id': pay_order_id,
570
+ 'amount_sent': state.get('amount'),
571
+ 'currency': state.get('currency', 'USDT'),
572
+ 'paid_with': status_info.asset_cost_vos if status_info.asset_cost_vos else None,
573
+ 'daily_used_before': daily_used_before,
574
+ 'daily_used_after': daily_used_after,
575
+ 'daily_limit': daily_limit
576
+ }))
577
+
578
+
579
+ def action_status():
580
+ """Show current order status and next steps"""
581
+ state = load_state()
582
+ status = get_order_status()
583
+
584
+ print()
585
+ print("════════════════════════════════════════════════════")
586
+ print("📊 Current Order Status")
587
+ print("════════════════════════════════════════════════════")
588
+
589
+ if not state or not status:
590
+ print(" No active order")
591
+ print()
592
+ print("💡 Start with: --action purchase --raw_qr <QR_DATA>")
593
+ print("════════════════════════════════════════════════════")
594
+ return
595
+
596
+ checkout_id = state.get('checkout_id')
597
+ pay_order_id = state.get('pay_order_id')
598
+ payment_type = state.get('payment_type', 'C2C')
599
+
600
+ print(f" Type: {payment_type}")
601
+ print(f" Status: {status.value}")
602
+ print(f" Checkout ID: {checkout_id or 'Not yet created'}")
603
+ if pay_order_id:
604
+ print(f" Pay Order: {pay_order_id}")
605
+
606
+ if state.get('nickname'):
607
+ print(f" Payee: {state.get('nickname')}")
608
+ if state.get('receiver_psp'):
609
+ print(f" Bank: {state.get('receiver_psp')}")
610
+ if state.get('receiver_document'):
611
+ print(f" Document: {state.get('receiver_document')}")
612
+ if state.get('currency'):
613
+ print(f" Currency: {state.get('currency')}")
614
+ if state.get('suggested_amount') or state.get('amount'):
615
+ amt = state.get('amount') or state.get('suggested_amount')
616
+ print(f" Amount: {amt} {state.get('currency', '')}")
617
+ if state.get('error_message'):
618
+ print(f" Error: {state.get('error_message')}")
619
+ if state.get('last_updated'):
620
+ print(f" Updated: {state.get('last_updated')}")
621
+
622
+ print()
623
+ print(f"💡 {get_status_hint(status, state)}")
624
+ print("════════════════════════════════════════════════════")
625
+
626
+ print(json.dumps({
627
+ 'status': status.value,
628
+ 'payment_type': payment_type,
629
+ 'checkout_id': checkout_id,
630
+ 'pay_order_id': pay_order_id,
631
+ 'amount': state.get('amount') or state.get('suggested_amount'),
632
+ 'currency': state.get('currency'),
633
+ 'payee': state.get('nickname')
634
+ }))
635
+
636
+
637
+ def action_reset():
638
+ """Clear state and start fresh"""
639
+ clear_state()
640
+ print()
641
+ print("🗑️ State cleared")
642
+ print()
643
+ print("💡 Ready for new payment: --action purchase --raw_qr <QR_DATA>")
644
+ print()
645
+
646
+
647
+ def action_resume(config: Dict[str, Any]):
648
+ """Resume from current state - automatically continue the payment flow."""
649
+ is_ready, reason, missing_fields = is_config_ready(config)
650
+ if not is_ready:
651
+ show_config_guide(config, reason, missing_fields)
652
+ return
653
+
654
+ state = load_state()
655
+ status = get_order_status()
656
+
657
+ if not state or not status:
658
+ print()
659
+ print("📭 No active order to resume")
660
+ print("💡 Start with: --action purchase --raw_qr <QR_DATA>")
661
+ print()
662
+ return
663
+
664
+ print()
665
+ print(f"🔄 Resuming from status: {status.value}")
666
+ print()
667
+
668
+ if status == OrderStatus.INIT:
669
+ raw_qr = state.get('raw_qr')
670
+ if raw_qr:
671
+ action_purchase(config, raw_qr)
672
+ else:
673
+ print("❌ No QR code in state")
674
+ print("💡 Run: --action purchase --raw_qr <QR_DATA>")
675
+
676
+ elif status == OrderStatus.QR_PARSED:
677
+ if state.get('has_preset_amount') and state.get('preset_amount'):
678
+ amount = float(state.get('preset_amount'))
679
+ action_pay_confirm(config, amount)
680
+ else:
681
+ print("💡 Please set amount: --action set_amount --amount <AMOUNT>")
682
+ print(f" Currency: {state.get('currency', 'USDT')}")
683
+
684
+ elif status == OrderStatus.AWAITING_AMOUNT:
685
+ print("💡 Please set amount: --action set_amount --amount <AMOUNT>")
686
+ print(f" Currency: {state.get('currency', 'USDT')}")
687
+
688
+ elif status == OrderStatus.AMOUNT_SET:
689
+ amount = state.get('suggested_amount') or state.get('amount')
690
+ if amount:
691
+ action_pay_confirm(config, float(amount))
692
+ else:
693
+ print("❌ No amount set")
694
+ print("💡 Run: --action set_amount --amount <AMOUNT>")
695
+
696
+ elif status in [OrderStatus.PAYMENT_CONFIRMED, OrderStatus.POLLING]:
697
+ action_poll(config)
698
+
699
+ elif status == OrderStatus.SUCCESS:
700
+ print("✅ Payment already completed!")
701
+ if state.get('asset_costs'):
702
+ costs = [f"{c.get('amount')} {c.get('asset')}" for c in state['asset_costs']]
703
+ print(f" 💳 Paid With: {' + '.join(costs)}")
704
+ print()
705
+ print("💡 Run: --action reset for a new payment")
706
+
707
+ elif status == OrderStatus.FAILED:
708
+ print(f"❌ Order failed: {state.get('error_message', 'Unknown error')}")
709
+ print()
710
+ print("💡 Run: --action reset to start over")
711
+
712
+ else:
713
+ print(f"⚠️ Unknown status: {status.value}")
714
+ print("💡 Run: --action status to check details")
715
+
716
+
717
+ def action_help():
718
+ """Show help information"""
719
+ print()
720
+ print("════════════════════════════════════════════════════")
721
+ print("👋 Payment Assistant Skill (C2C + PIX)")
722
+ print("════════════════════════════════════════════════════")
723
+ print()
724
+ print("📋 Core Actions (3-step flow):")
725
+ print(" purchase - Step 1: Parse QR (requires --raw_qr)")
726
+ print(" Auto-detects C2C URL or PIX EMV QR")
727
+ print(" set_amount - Set amount (e.g., --amount 100 --currency BRL)")
728
+ print(" pay_confirm - Step 2: Confirm payment")
729
+ print(" poll - Step 3: Poll until final status")
730
+ print(" query - Check order status (API call)")
731
+ print()
732
+ print("📷 QR Decode Actions:")
733
+ print(" decode_qr - Decode QR from clipboard or image file")
734
+ print()
735
+ print("💰 Receive Actions:")
736
+ print(" receive - Generate receive QR code / payment link")
737
+ print()
738
+ print("🔄 Recovery Actions:")
739
+ print(" status - Show current state and next steps")
740
+ print(" resume - Auto-continue from any state")
741
+ print(" reset - Clear state for fresh start")
742
+ print()
743
+ print("⚙️ Config Actions:")
744
+ print(" config - Show configuration guide")
745
+
746
+ print()
747
+ print("💡 C2C Example Flow:")
748
+ print(" 1. --action decode_qr # Decode from clipboard/inbox")
749
+ print(" 2. --action purchase --raw_qr '<QR_DATA>'")
750
+ print(" 3. --action set_amount --amount 50 # If no preset amount")
751
+ print(" 4. --action pay_confirm")
752
+ print(" 5. --action poll")
753
+ print()
754
+ print("💡 PIX Example Flow:")
755
+ print(" 1. --action purchase --raw_qr '00020126...br.gov.bcb.pix...'")
756
+ print(" 2. --action set_amount --amount 100 --currency BRL # If no preset")
757
+ print(" 3. --action pay_confirm")
758
+ print(" 4. --action poll")
759
+ print()
760
+ print("💡 Receive Example:")
761
+ print(" --action receive --currency USDT --amount 50 --note 'For lunch'")
762
+ print()
763
+ print("🔄 Recovery (if interrupted at any point):")
764
+ print(" --action status # Check where you are")
765
+ print(" --action resume # Auto-continue")
766
+ print("════════════════════════════════════════════════════")
767
+ print()
768
+
769
+
770
+ def _get_file_info(file_path: str) -> Dict[str, Any]:
771
+ """Get file metadata for debugging/transparency."""
772
+ try:
773
+ stat = os.stat(file_path)
774
+ return {
775
+ 'path': file_path,
776
+ 'filename': os.path.basename(file_path),
777
+ 'size_bytes': stat.st_size,
778
+ 'modified_time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stat.st_mtime)),
779
+ }
780
+ except Exception:
781
+ return {'path': file_path, 'filename': os.path.basename(file_path)}
782
+
783
+
784
+ def action_decode_qr(image_path: str = None, base64_data: str = None, use_clipboard: bool = False):
785
+ """
786
+ Decode QR code from image.
787
+
788
+ Three MUTUALLY EXCLUSIVE input modes (no fallback between them):
789
+ - --image <path> : Decode from file path
790
+ - --base64 <data> : Decode from base64 encoded image
791
+ - --clipboard : Explicitly read from system clipboard
792
+
793
+ If no input specified, returns an error asking for explicit input.
794
+ This ensures 100% clarity on which image is being decoded.
795
+
796
+ Returns JSON with qr_data and source info for transparency.
797
+ """
798
+ qr_handler = QRCodeHandler()
799
+
800
+ has_decoder = (HAS_PIL and HAS_PYZBAR) or HAS_CV2
801
+ if not has_decoder:
802
+ print(json.dumps({
803
+ 'success': False,
804
+ 'error': 'missing_dependencies',
805
+ 'message': "No QR decoder available. Install: pip install opencv-python pyzbar"
806
+ }))
807
+ return
808
+
809
+ # Count how many input modes are specified
810
+ input_modes = sum([bool(image_path), bool(base64_data), use_clipboard])
811
+
812
+ if input_modes > 1:
813
+ print(json.dumps({
814
+ 'success': False,
815
+ 'error': 'multiple_inputs',
816
+ 'message': "Only one input mode allowed. Use --image OR --base64 OR --clipboard, not multiple.",
817
+ 'hint': 'Choose one input source to avoid ambiguity.'
818
+ }))
819
+ return
820
+
821
+ if input_modes == 0:
822
+ print(json.dumps({
823
+ 'success': False,
824
+ 'error': 'no_input',
825
+ 'message': "No image input specified. You must provide one of: --image, --base64, or --clipboard",
826
+ 'usage': {
827
+ '--image <path>': 'Path to image file (from message attachment)',
828
+ '--base64 <data>': 'Base64 encoded image data',
829
+ '--clipboard': 'Read from system clipboard (user must have just copied an image)'
830
+ },
831
+ 'hint': 'AI should use --image with the attachment path from the user message, or use Vision to read QR directly and pass --raw_qr to purchase action.'
832
+ }))
833
+ return
834
+
835
+ # ============================================================
836
+ # Mode 1: Image file path
837
+ # ============================================================
838
+ if image_path:
839
+ if not os.path.exists(image_path):
840
+ print(json.dumps({
841
+ 'success': False,
842
+ 'error': 'file_not_found',
843
+ 'message': f"File not found: {image_path}",
844
+ 'source_type': 'image_path',
845
+ 'provided_path': image_path
846
+ }))
847
+ return
848
+
849
+ file_info = _get_file_info(image_path)
850
+ qr_data = qr_handler.decode_qr_from_image(image_path)
851
+
852
+ if qr_data:
853
+ print(json.dumps({
854
+ 'success': True,
855
+ 'qr_data': qr_data,
856
+ 'source_type': 'image_path',
857
+ 'source_info': file_info,
858
+ 'message': f"QR decoded from: {file_info['filename']}"
859
+ }))
860
+ else:
861
+ print(json.dumps({
862
+ 'success': False,
863
+ 'error': 'decode_failed',
864
+ 'message': f"No QR code found in image: {file_info['filename']}",
865
+ 'source_type': 'image_path',
866
+ 'source_info': file_info,
867
+ 'hint': 'Image exists but no QR code detected. Verify this is the correct image.'
868
+ }))
869
+ return
870
+
871
+ # ============================================================
872
+ # Mode 2: Base64 encoded image
873
+ # ============================================================
874
+ if base64_data:
875
+ import base64
876
+ import tempfile
877
+
878
+ try:
879
+ # Remove data URI prefix if present (e.g., "data:image/png;base64,")
880
+ if ',' in base64_data:
881
+ base64_data = base64_data.split(',', 1)[1]
882
+
883
+ image_bytes = base64.b64decode(base64_data)
884
+
885
+ # Save to temp file for decoding
886
+ with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
887
+ tmp.write(image_bytes)
888
+ tmp_path = tmp.name
889
+
890
+ qr_data = qr_handler.decode_qr_from_image(tmp_path)
891
+
892
+ # Clean up temp file
893
+ try:
894
+ os.unlink(tmp_path)
895
+ except:
896
+ pass
897
+
898
+ if qr_data:
899
+ print(json.dumps({
900
+ 'success': True,
901
+ 'qr_data': qr_data,
902
+ 'source_type': 'base64',
903
+ 'source_info': {
904
+ 'data_length': len(base64_data),
905
+ 'decoded_size': len(image_bytes)
906
+ },
907
+ 'message': 'QR decoded from base64 image data'
908
+ }))
909
+ else:
910
+ print(json.dumps({
911
+ 'success': False,
912
+ 'error': 'decode_failed',
913
+ 'message': 'No QR code found in base64 image',
914
+ 'source_type': 'base64',
915
+ 'hint': 'Image decoded successfully but no QR code detected.'
916
+ }))
917
+ except Exception as e:
918
+ print(json.dumps({
919
+ 'success': False,
920
+ 'error': 'base64_decode_failed',
921
+ 'message': f'Failed to decode base64 image: {str(e)}',
922
+ 'source_type': 'base64',
923
+ 'hint': 'Ensure the base64 data is valid image data.'
924
+ }))
925
+ return
926
+
927
+ # ============================================================
928
+ # Mode 3: System clipboard (explicit)
929
+ # ============================================================
930
+ if use_clipboard:
931
+ success, data, msg = qr_handler.decode_qr_from_clipboard()
932
+
933
+ if success:
934
+ print(json.dumps({
935
+ 'success': True,
936
+ 'qr_data': data,
937
+ 'source_type': 'clipboard',
938
+ 'source_info': {
939
+ 'method': 'system_clipboard',
940
+ 'note': 'Image was read from current system clipboard'
941
+ },
942
+ 'message': 'QR decoded from clipboard'
943
+ }))
944
+ else:
945
+ print(json.dumps({
946
+ 'success': False,
947
+ 'error': 'clipboard_failed',
948
+ 'message': msg or 'Failed to read QR from clipboard',
949
+ 'source_type': 'clipboard',
950
+ 'hint': 'Ensure an image is copied to clipboard. On macOS: Cmd+Ctrl+Shift+4 to screenshot to clipboard.'
951
+ }))
952
+ return