@clawcard/cli 2.1.1 → 2.1.3

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,28 +1,23 @@
1
1
  // ClawCard Pay — Background Service Worker
2
- // Bridges native messaging host (CLI) content scripts (page frames)
2
+ // Polls localhost:7891 for pending fill commands from the CLI
3
3
 
4
- const NATIVE_HOST = "com.clawcard.pay";
5
- let nativePort = null;
4
+ const POLL_URL = "http://localhost:7891/pending";
5
+ const RESULT_URL = "http://localhost:7891/result";
6
+ const POLL_INTERVAL = 500;
6
7
 
7
- function connectNative() {
8
+ async function poll() {
8
9
  try {
9
- nativePort = chrome.runtime.connectNative(NATIVE_HOST);
10
-
11
- nativePort.onMessage.addListener((message) => {
12
- if (message.action === "fill") {
13
- fillActiveTab(message);
10
+ const res = await fetch(POLL_URL);
11
+ if (res.ok) {
12
+ const message = await res.json();
13
+ if (message && message.action === "fill") {
14
+ await fillActiveTab(message);
14
15
  }
15
- });
16
-
17
- nativePort.onDisconnect.addListener(() => {
18
- nativePort = null;
19
- // Reconnect after delay
20
- setTimeout(connectNative, 3000);
21
- });
22
- } catch (e) {
23
- // Native host not available — retry later
24
- setTimeout(connectNative, 5000);
16
+ }
17
+ } catch {
18
+ // Server not running — CLI hasn't started a pay command
25
19
  }
20
+ setTimeout(poll, POLL_INTERVAL);
26
21
  }
27
22
 
28
23
  async function fillActiveTab(message) {
@@ -33,67 +28,67 @@ async function fillActiveTab(message) {
33
28
  });
34
29
 
35
30
  if (!tab) {
36
- sendNativeResponse({ success: false, error: "No active tab" });
31
+ await sendResult({ success: false, error: "No active tab" });
37
32
  return;
38
33
  }
39
34
 
40
- // Try main frame first
41
- try {
42
- const response = await chrome.tabs.sendMessage(tab.id, message);
43
- if (response && response.filled && response.filled.length > 0) {
44
- sendNativeResponse(response);
45
- return;
46
- }
47
- } catch {
48
- // Main frame didn't have content script or no fields — try subframes
49
- }
50
-
51
- // Try all subframes
35
+ // Get all frames in the tab
52
36
  const frames = await chrome.webNavigation.getAllFrames({ tabId: tab.id });
53
37
  if (!frames || frames.length === 0) {
54
- sendNativeResponse({
55
- success: false,
56
- error: "No frames found on the current page",
57
- });
38
+ await sendResult({ success: false, error: "No frames found" });
58
39
  return;
59
40
  }
60
41
 
42
+ // Inject content script into ALL frames first (handles dynamically added iframes)
43
+ for (const frame of frames) {
44
+ try {
45
+ await chrome.scripting.executeScript({
46
+ target: { tabId: tab.id, frameIds: [frame.frameId] },
47
+ files: ["content.js"],
48
+ });
49
+ } catch {
50
+ // Can't inject into this frame — that's fine
51
+ }
52
+ }
53
+
54
+ // Small delay to let injected scripts initialize
55
+ await new Promise((r) => setTimeout(r, 100));
56
+
57
+ // Now try sending the fill command to each frame
61
58
  for (const frame of frames) {
62
- if (frame.frameId === 0) continue; // already tried main frame
63
59
  try {
64
60
  const response = await chrome.tabs.sendMessage(tab.id, message, {
65
61
  frameId: frame.frameId,
66
62
  });
67
63
  if (response && response.filled && response.filled.length > 0) {
68
- sendNativeResponse(response);
64
+ await sendResult(response);
69
65
  return;
70
66
  }
71
67
  } catch {
72
- // This frame doesn't have our content script or no fields — continue
68
+ // No content script in this frame or no fields — continue
73
69
  }
74
70
  }
75
71
 
76
- sendNativeResponse({
72
+ await sendResult({
77
73
  success: false,
78
74
  error: "No payment form fields found on the current page",
79
75
  });
80
76
  } catch (err) {
81
- sendNativeResponse({
82
- success: false,
83
- error: err.message || "Unknown error",
84
- });
77
+ await sendResult({ success: false, error: err.message || "Unknown error" });
85
78
  }
86
79
  }
87
80
 
88
- function sendNativeResponse(response) {
89
- if (nativePort) {
90
- try {
91
- nativePort.postMessage(response);
92
- } catch {
93
- // Port disconnected
94
- }
81
+ async function sendResult(result) {
82
+ try {
83
+ await fetch(RESULT_URL, {
84
+ method: "POST",
85
+ headers: { "Content-Type": "application/json" },
86
+ body: JSON.stringify(result),
87
+ });
88
+ } catch {
89
+ // CLI server might have timed out
95
90
  }
96
91
  }
97
92
 
98
- // Start native messaging connection
99
- connectNative();
93
+ // Start polling
94
+ poll();
@@ -92,6 +92,13 @@ function fillField(el, value) {
92
92
  el.dispatchEvent(new Event("blur", { bubbles: true }));
93
93
  }
94
94
 
95
+ // Guard against duplicate injection
96
+ if (window.__clawcardPayInjected) {
97
+ // Already injected — just re-register listener
98
+ } else {
99
+ window.__clawcardPayInjected = true;
100
+ }
101
+
95
102
  // Listen for fill commands from background script
96
103
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
97
104
  if (message.action !== "fill") return;
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "ClawCard Pay",
4
- "version": "1.0.0",
4
+ "version": "1.0.1",
5
5
  "description": "Fills payment form fields for ClawCard agents",
6
- "permissions": ["nativeMessaging", "webNavigation"],
6
+ "permissions": ["webNavigation", "scripting", "activeTab"],
7
7
  "host_permissions": ["<all_urls>"],
8
8
  "background": {
9
9
  "service_worker": "background.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawcard/cli",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "The ClawCard CLI — manage your agent keys, billing, and setup from the terminal",
5
5
  "bin": {
6
6
  "clawcard": "./bin/clawcard.mjs"
@@ -239,14 +239,7 @@ export async function agentActivityCmd(options) {
239
239
 
240
240
  // ── Pay (fill payment form via browser extension) ──
241
241
  export async function agentPayCmd(options) {
242
- const { writeFileSync, existsSync, readFileSync, unlinkSync, mkdirSync } =
243
- await import("fs");
244
- const { join } = await import("path");
245
- const { homedir } = await import("os");
246
-
247
- const clawcardDir = join(homedir(), ".clawcard");
248
- const pendingPath = join(clawcardDir, "pending-pay.json");
249
- const responsePath = join(clawcardDir, "pay-response.json");
242
+ const http = await import("http");
250
243
 
251
244
  // Get card details
252
245
  const agentId = await getAgentId();
@@ -266,45 +259,95 @@ export async function agentPayCmd(options) {
266
259
  return;
267
260
  }
268
261
 
269
- // Format expiry as MM/YY
270
262
  const exp = `${String(card.exp_month).padStart(2, "0")}/${String(card.exp_year).slice(-2)}`;
263
+ const fillData = { action: "fill", pan: card.pan, exp, cvc: card.cvv };
271
264
 
272
- // Clean up old response
273
- try {
274
- unlinkSync(responsePath);
275
- } catch {}
276
-
277
- // Write fill command for native host
278
- mkdirSync(clawcardDir, { recursive: true });
279
- writeFileSync(
280
- pendingPath,
281
- JSON.stringify({ action: "fill", pan: card.pan, exp, cvc: card.cvv })
282
- );
283
-
284
- // Poll for response (5 second timeout)
285
- const start = Date.now();
286
- while (Date.now() - start < 5000) {
287
- if (existsSync(responsePath)) {
288
- try {
289
- const response = JSON.parse(readFileSync(responsePath, "utf-8"));
290
- unlinkSync(responsePath);
291
- if (options.json) return output(response, true);
292
- if (response.success) {
293
- console.log(` Payment fields filled: ${response.filled.join(", ")}`);
265
+ // Start localhost HTTP server — extension polls this
266
+ const PORT = 7891;
267
+
268
+ const result = await new Promise((resolve) => {
269
+ let settled = false;
270
+
271
+ const server = http.createServer((req, res) => {
272
+ // CORS headers for extension
273
+ res.setHeader("Access-Control-Allow-Origin", "*");
274
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
275
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
276
+
277
+ if (req.method === "OPTIONS") {
278
+ res.writeHead(204);
279
+ res.end();
280
+ return;
281
+ }
282
+
283
+ // Extension polls this for pending fill commands
284
+ if (req.method === "GET" && req.url === "/pending") {
285
+ if (!settled) {
286
+ res.writeHead(200, { "Content-Type": "application/json" });
287
+ res.end(JSON.stringify(fillData));
294
288
  } else {
295
- console.log(` Error: ${response.error}`);
289
+ res.writeHead(204);
290
+ res.end();
296
291
  }
297
292
  return;
298
- } catch {
299
- // File being written — retry
300
293
  }
301
- }
302
- await new Promise((r) => setTimeout(r, 200));
303
- }
304
294
 
305
- // Timeout
306
- const err =
307
- "ClawCard extension not responding. Make sure it's installed: load ~/.clawcard/extension/ in chrome://extensions";
308
- if (options.json) return output({ success: false, error: err }, true);
309
- console.log(` Error: ${err}`);
295
+ // Extension posts fill result here
296
+ if (req.method === "POST" && req.url === "/result") {
297
+ let body = "";
298
+ req.on("data", (chunk) => (body += chunk));
299
+ req.on("end", () => {
300
+ res.writeHead(200);
301
+ res.end("ok");
302
+ if (!settled) {
303
+ settled = true;
304
+ try {
305
+ resolve(JSON.parse(body));
306
+ } catch {
307
+ resolve({ success: false, error: "Invalid response from extension" });
308
+ }
309
+ server.close();
310
+ }
311
+ });
312
+ return;
313
+ }
314
+
315
+ res.writeHead(404);
316
+ res.end();
317
+ });
318
+
319
+ server.listen(PORT, "127.0.0.1", () => {
320
+ // Server started — extension will pick up the fill command
321
+ });
322
+
323
+ server.on("error", (err) => {
324
+ if (!settled) {
325
+ settled = true;
326
+ resolve({
327
+ success: false,
328
+ error: `Could not start pay server on port ${PORT}: ${err.message}`,
329
+ });
330
+ }
331
+ });
332
+
333
+ // Timeout after 10 seconds
334
+ setTimeout(() => {
335
+ if (!settled) {
336
+ settled = true;
337
+ server.close();
338
+ resolve({
339
+ success: false,
340
+ error:
341
+ "ClawCard extension not responding. Make sure it's loaded in Chrome: chrome://extensions → Load Unpacked → ~/.clawcard/extension/",
342
+ });
343
+ }
344
+ }, 10000);
345
+ });
346
+
347
+ if (options.json) return output(result, true);
348
+ if (result.success) {
349
+ console.log(` Payment fields filled: ${result.filled.join(", ")}`);
350
+ } else {
351
+ console.log(` Error: ${result.error}`);
352
+ }
310
353
  }
@@ -164,91 +164,6 @@ export async function setupCommand() {
164
164
 
165
165
  copyDir(extensionSource, extensionDest);
166
166
 
167
- // Create native host wrapper script (Chrome requires an executable, not a .js file)
168
- const nativeHostScript = join(home, ".clawcard", "native-host.js");
169
- const nativeHostWrapper = join(home, ".clawcard", "native-host.sh");
170
- const nativeHostSource = join(__dirname, "..", "native-host.js");
171
-
172
- // Copy native host JS
173
- copyFileSync(nativeHostSource, nativeHostScript);
174
-
175
- // Create shell wrapper
176
- const nodePath = process.execPath;
177
- writeFileSync(
178
- nativeHostWrapper,
179
- `#!/bin/bash\nexec "${nodePath}" "${nativeHostScript}"\n`
180
- );
181
- // Make wrapper executable
182
- const { chmodSync } = await import("fs");
183
- chmodSync(nativeHostWrapper, "755");
184
-
185
- // Detect extension ID from Chrome (or use broad matching with key)
186
- // Read the extension ID from the installed manifest
187
- const extensionManifestPath = join(extensionDest, "manifest.json");
188
- const extensionManifest = JSON.parse(readFileSync(extensionManifestPath, "utf-8"));
189
-
190
- // If no key in manifest, we need to accept any extension ID
191
- // Chrome requires exact IDs — we'll add a stable key to the manifest
192
- // For now, prompt user for their extension ID
193
- let extensionId = "PLACEHOLDER";
194
- try {
195
- // Try to read from a saved config
196
- const savedConfig = JSON.parse(readFileSync(join(home, ".clawcard", "extension-id.txt"), "utf-8"));
197
- extensionId = savedConfig;
198
- } catch {}
199
-
200
- if (extensionId === "PLACEHOLDER") {
201
- p.log.info("After loading the extension in Chrome, find its ID on chrome://extensions");
202
- const idInput = await p.text({
203
- message: "Enter the extension ID from chrome://extensions",
204
- placeholder: "abcdefghijklmnopqrstuvwxyz123456",
205
- validate: (v) => {
206
- if (!v || v.length < 10) return "Paste the full extension ID from Chrome";
207
- },
208
- });
209
- if (!p.isCancel(idInput)) {
210
- extensionId = idInput;
211
- writeFileSync(join(home, ".clawcard", "extension-id.txt"), JSON.stringify(extensionId));
212
- }
213
- }
214
-
215
- // Register native messaging host
216
- const platform = process.platform;
217
- let nativeHostDir;
218
-
219
- if (platform === "darwin") {
220
- nativeHostDir = join(
221
- home,
222
- "Library",
223
- "Application Support",
224
- "Google",
225
- "Chrome",
226
- "NativeMessagingHosts"
227
- );
228
- } else {
229
- nativeHostDir = join(
230
- home,
231
- ".config",
232
- "google-chrome",
233
- "NativeMessagingHosts"
234
- );
235
- }
236
-
237
- mkdirSync(nativeHostDir, { recursive: true });
238
-
239
- const nativeHostManifest = {
240
- name: "com.clawcard.pay",
241
- description: "ClawCard payment form filler",
242
- path: nativeHostWrapper,
243
- type: "stdio",
244
- allowed_origins: [`chrome-extension://${extensionId}/`],
245
- };
246
-
247
- writeFileSync(
248
- join(nativeHostDir, "com.clawcard.pay.json"),
249
- JSON.stringify(nativeHostManifest, null, 2)
250
- );
251
-
252
167
  s4.stop("Extension installed!");
253
168
 
254
169
  p.note(