@clawcard/cli 2.0.8 → 2.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.
@@ -0,0 +1,99 @@
1
+ // ClawCard Pay — Background Service Worker
2
+ // Bridges native messaging host (CLI) ↔ content scripts (page frames)
3
+
4
+ const NATIVE_HOST = "com.clawcard.pay";
5
+ let nativePort = null;
6
+
7
+ function connectNative() {
8
+ try {
9
+ nativePort = chrome.runtime.connectNative(NATIVE_HOST);
10
+
11
+ nativePort.onMessage.addListener((message) => {
12
+ if (message.action === "fill") {
13
+ fillActiveTab(message);
14
+ }
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);
25
+ }
26
+ }
27
+
28
+ async function fillActiveTab(message) {
29
+ try {
30
+ const [tab] = await chrome.tabs.query({
31
+ active: true,
32
+ currentWindow: true,
33
+ });
34
+
35
+ if (!tab) {
36
+ sendNativeResponse({ success: false, error: "No active tab" });
37
+ return;
38
+ }
39
+
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
52
+ const frames = await chrome.webNavigation.getAllFrames({ tabId: tab.id });
53
+ if (!frames || frames.length === 0) {
54
+ sendNativeResponse({
55
+ success: false,
56
+ error: "No frames found on the current page",
57
+ });
58
+ return;
59
+ }
60
+
61
+ for (const frame of frames) {
62
+ if (frame.frameId === 0) continue; // already tried main frame
63
+ try {
64
+ const response = await chrome.tabs.sendMessage(tab.id, message, {
65
+ frameId: frame.frameId,
66
+ });
67
+ if (response && response.filled && response.filled.length > 0) {
68
+ sendNativeResponse(response);
69
+ return;
70
+ }
71
+ } catch {
72
+ // This frame doesn't have our content script or no fields — continue
73
+ }
74
+ }
75
+
76
+ sendNativeResponse({
77
+ success: false,
78
+ error: "No payment form fields found on the current page",
79
+ });
80
+ } catch (err) {
81
+ sendNativeResponse({
82
+ success: false,
83
+ error: err.message || "Unknown error",
84
+ });
85
+ }
86
+ }
87
+
88
+ function sendNativeResponse(response) {
89
+ if (nativePort) {
90
+ try {
91
+ nativePort.postMessage(response);
92
+ } catch {
93
+ // Port disconnected
94
+ }
95
+ }
96
+ }
97
+
98
+ // Start native messaging connection
99
+ connectNative();
@@ -0,0 +1,125 @@
1
+ // ClawCard Pay — Content Script
2
+ // Injected into all frames on all pages. Fills Stripe payment form fields.
3
+
4
+ const NUMBER_SELECTORS = [
5
+ '#cardNumber',
6
+ '#payment-numberInput',
7
+ 'input[name="cardnumber"]',
8
+ 'input[name="number"]',
9
+ 'input[name="cardNumber"]',
10
+ 'input[autocomplete="cc-number"]',
11
+ ];
12
+
13
+ const EXPIRY_SELECTORS = [
14
+ '#cardExpiry',
15
+ '#payment-expiryInput',
16
+ 'input[name="cardExpiry"]',
17
+ 'input[name="expiry"]',
18
+ 'input[name="exp-date"]',
19
+ 'input[autocomplete="cc-exp"]',
20
+ ];
21
+
22
+ const CVC_SELECTORS = [
23
+ '#cardCvc',
24
+ '#payment-cvcInput',
25
+ 'input[name="cardCvc"]',
26
+ 'input[name="cvc"]',
27
+ 'input[autocomplete="cc-csc"]',
28
+ ];
29
+
30
+ function findField(selectors) {
31
+ for (const sel of selectors) {
32
+ const el = document.querySelector(sel);
33
+ if (el) return el;
34
+ }
35
+ return null;
36
+ }
37
+
38
+ function fillField(el, value) {
39
+ el.focus();
40
+ el.click();
41
+
42
+ // Use native setter to bypass React/Stripe controlled input protection
43
+ const nativeSetter = Object.getOwnPropertyDescriptor(
44
+ window.HTMLInputElement.prototype,
45
+ "value"
46
+ ).set;
47
+
48
+ // Clear first
49
+ nativeSetter.call(el, "");
50
+ el.dispatchEvent(new Event("input", { bubbles: true }));
51
+
52
+ // Type each character to simulate real keystrokes
53
+ // Stripe validates on each input event
54
+ for (let i = 0; i < value.length; i++) {
55
+ const char = value[i];
56
+ const currentVal = el.value;
57
+
58
+ el.dispatchEvent(
59
+ new KeyboardEvent("keydown", {
60
+ key: char,
61
+ code: `Digit${char}`,
62
+ bubbles: true,
63
+ })
64
+ );
65
+ el.dispatchEvent(
66
+ new KeyboardEvent("keypress", {
67
+ key: char,
68
+ code: `Digit${char}`,
69
+ bubbles: true,
70
+ })
71
+ );
72
+
73
+ nativeSetter.call(el, currentVal + char);
74
+
75
+ el.dispatchEvent(
76
+ new InputEvent("input", {
77
+ data: char,
78
+ inputType: "insertText",
79
+ bubbles: true,
80
+ })
81
+ );
82
+ el.dispatchEvent(
83
+ new KeyboardEvent("keyup", {
84
+ key: char,
85
+ code: `Digit${char}`,
86
+ bubbles: true,
87
+ })
88
+ );
89
+ }
90
+
91
+ el.dispatchEvent(new Event("change", { bubbles: true }));
92
+ el.dispatchEvent(new Event("blur", { bubbles: true }));
93
+ }
94
+
95
+ // Listen for fill commands from background script
96
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
97
+ if (message.action !== "fill") return;
98
+
99
+ const filled = [];
100
+
101
+ const numberField = findField(NUMBER_SELECTORS);
102
+ if (numberField) {
103
+ fillField(numberField, message.pan.replace(/\s/g, ""));
104
+ filled.push("number");
105
+ }
106
+
107
+ const expiryField = findField(EXPIRY_SELECTORS);
108
+ if (expiryField) {
109
+ // Format: MM / YY (Stripe expects spaces around slash)
110
+ const exp = message.exp.includes("/")
111
+ ? message.exp.replace("/", " / ").replace(" ", " ")
112
+ : `${message.exp.slice(0, 2)} / ${message.exp.slice(2)}`;
113
+ fillField(expiryField, exp);
114
+ filled.push("expiry");
115
+ }
116
+
117
+ const cvcField = findField(CVC_SELECTORS);
118
+ if (cvcField) {
119
+ fillField(cvcField, message.cvc);
120
+ filled.push("cvc");
121
+ }
122
+
123
+ sendResponse({ success: filled.length > 0, filled });
124
+ return true;
125
+ });
@@ -0,0 +1,14 @@
1
+ Timestamp: 2026-03-16T07:12:44Z
2
+ Branch: "main"
3
+ Type: "notes"
4
+ Tool: Claude Code
5
+ Summary:
6
+ "**Task 2: Content script.**"
7
+ ========
8
+ Timestamp: 2026-03-16T07:13:03Z
9
+ Branch: "main"
10
+ Type: "notes"
11
+ Tool: Claude Code
12
+ Summary:
13
+ "**Task 3: Background service worker.**"
14
+ ========
Binary file
Binary file
Binary file
@@ -0,0 +1,22 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "ClawCard Pay",
4
+ "version": "1.0.0",
5
+ "description": "Fills payment form fields for ClawCard agents",
6
+ "permissions": ["nativeMessaging", "webNavigation"],
7
+ "host_permissions": ["<all_urls>"],
8
+ "background": {
9
+ "service_worker": "background.js"
10
+ },
11
+ "content_scripts": [{
12
+ "matches": ["<all_urls>"],
13
+ "all_frames": true,
14
+ "js": ["content.js"],
15
+ "run_at": "document_idle"
16
+ }],
17
+ "icons": {
18
+ "16": "icons/icon16.png",
19
+ "48": "icons/icon48.png",
20
+ "128": "icons/icon128.png"
21
+ }
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawcard/cli",
3
- "version": "2.0.8",
3
+ "version": "2.1.0",
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"
@@ -9,7 +9,9 @@
9
9
  "license": "MIT",
10
10
  "files": [
11
11
  "bin/",
12
- "src/"
12
+ "src/",
13
+ "skill/",
14
+ "extension/"
13
15
  ],
14
16
  "dependencies": {
15
17
  "@clack/prompts": "^0.10.0",
package/skill/SKILL.md ADDED
@@ -0,0 +1,136 @@
1
+ ---
2
+ name: clawcard
3
+ description: Email, SMS, virtual cards, and credential vault for autonomous agents. Run clawcard agent commands to interact with your resources.
4
+ ---
5
+
6
+ You have access to ClawCard via the `clawcard` CLI. All commands support `--json` for machine-readable output. Always pass `--json` when calling these commands.
7
+
8
+ ## Step 1: Check your identity
9
+
10
+ ```
11
+ clawcard agent info --json
12
+ ```
13
+
14
+ Returns your agent name, email address, phone number, and remaining budget.
15
+
16
+ ## Email
17
+
18
+ List inbox:
19
+ ```
20
+ clawcard agent emails --json [--limit 20] [--unread]
21
+ ```
22
+
23
+ Send email:
24
+ ```
25
+ clawcard agent emails send --to "recipient@example.com" --subject "Subject" --body "Body" --json
26
+ ```
27
+
28
+ Mark as read:
29
+ ```
30
+ clawcard agent emails read <email-id> --json
31
+ ```
32
+
33
+ ## SMS
34
+
35
+ List messages:
36
+ ```
37
+ clawcard agent sms --json [--limit 20]
38
+ ```
39
+
40
+ Send SMS:
41
+ ```
42
+ clawcard agent sms send --to "+15551234567" --body "Message" --json
43
+ ```
44
+
45
+ ## Virtual Cards
46
+
47
+ IMPORTANT: Always list cards first. Reuse open merchant_locked cards for the same merchant.
48
+
49
+ Card types (REQUIRED — you must specify one):
50
+ - single_use: auto-closes after one charge. Use for one-time purchases (domains, invoices).
51
+ - merchant_locked: locks to first merchant, allows repeat charges. Use for subscriptions (hosting, SaaS).
52
+
53
+ List cards:
54
+ ```
55
+ clawcard agent cards --json
56
+ ```
57
+
58
+ Create card:
59
+ ```
60
+ clawcard agent cards create --amount <cents> --type <single_use|merchant_locked> --memo "description" --json
61
+ ```
62
+
63
+ Get card details (PAN, CVV, expiry):
64
+ ```
65
+ clawcard agent cards details <card-id> --json
66
+ ```
67
+
68
+ Close card:
69
+ ```
70
+ clawcard agent cards close <card-id> --json
71
+ ```
72
+
73
+ ## Credentials
74
+
75
+ Store and retrieve secrets. Use consistent lowercase naming.
76
+
77
+ Naming convention:
78
+ - --service: lowercase service name (openai, aws, stripe, vercel, namecheap)
79
+ - --key: lowercase key type (api_key, secret_key, access_token, password)
80
+
81
+ List stored credentials:
82
+ ```
83
+ clawcard agent creds --json
84
+ ```
85
+
86
+ Store credential:
87
+ ```
88
+ clawcard agent creds set --service <name> --key <key> --value <secret> --json
89
+ ```
90
+
91
+ Retrieve credential:
92
+ ```
93
+ clawcard agent creds get --service <name> --key <key> --json
94
+ ```
95
+
96
+ ## Budget
97
+
98
+ Check remaining budget:
99
+ ```
100
+ clawcard agent budget --json
101
+ ```
102
+
103
+ ## Activity
104
+
105
+ View activity log:
106
+ ```
107
+ clawcard agent activity --json [--limit 50]
108
+ ```
109
+
110
+ ## Paying on Checkout Pages
111
+
112
+ When you reach a payment form:
113
+ 1. Fill all non-payment fields yourself (name, email, address)
114
+ 2. Create a card if needed:
115
+ ```
116
+ clawcard agent cards create --amount <cents> --type single_use --memo "desc" --json
117
+ ```
118
+ 3. Try filling card fields yourself first (card number, expiry, CVC)
119
+ 4. If you can't access the card fields (iframe/cross-origin), run:
120
+ ```
121
+ clawcard agent pay --card-id <card-id> --json
122
+ ```
123
+ This sends the card details to the ClawCard browser extension which
124
+ fills payment form fields that are inside iframes you can't reach.
125
+ 5. If the extension is not installed, this will return an error —
126
+ fall back to telling the user they need the extension installed.
127
+ 6. After running pay, wait a moment then verify the fields are filled
128
+ before clicking the submit/pay button.
129
+
130
+ ## Tips
131
+
132
+ - Always run `clawcard agent info --json` first to verify your identity.
133
+ - Check budget before creating cards: `clawcard agent budget --json`
134
+ - Reuse merchant_locked cards for repeat purchases at the same merchant.
135
+ - Use single_use cards for one-time purchases — they auto-close after one charge.
136
+ - Store credentials in the vault with consistent naming so you can find them later.
@@ -236,3 +236,75 @@ export async function agentActivityCmd(options) {
236
236
  }
237
237
  console.log();
238
238
  }
239
+
240
+ // ── Pay (fill payment form via browser extension) ──
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");
250
+
251
+ // Get card details
252
+ const agentId = await getAgentId();
253
+ let card;
254
+ try {
255
+ card = await getCardDetails(agentId, options.cardId);
256
+ } catch (err) {
257
+ if (options.json) return output({ success: false, error: err.message }, true);
258
+ console.log(` Error: ${err.message}`);
259
+ return;
260
+ }
261
+
262
+ if (!card.pan || !card.cvv) {
263
+ const err = "Card details not available (card may be closed)";
264
+ if (options.json) return output({ success: false, error: err }, true);
265
+ console.log(` Error: ${err}`);
266
+ return;
267
+ }
268
+
269
+ // Format expiry as MM/YY
270
+ const exp = `${String(card.exp_month).padStart(2, "0")}/${String(card.exp_year).slice(-2)}`;
271
+
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(", ")}`);
294
+ } else {
295
+ console.log(` Error: ${response.error}`);
296
+ }
297
+ return;
298
+ } catch {
299
+ // File being written — retry
300
+ }
301
+ }
302
+ await new Promise((r) => setTimeout(r, 200));
303
+ }
304
+
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}`);
310
+ }
@@ -33,6 +33,7 @@ const COMMANDS = [
33
33
  [" agent creds get", "Retrieve credential"],
34
34
  [" agent budget", "Check budget"],
35
35
  [" agent activity", "Activity log"],
36
+ [" agent pay", "Fill Stripe payment form"],
36
37
  ["", ""],
37
38
  [orange.bold("Keys"), ""],
38
39
  [" keys create", "Create a new agent key"],
@@ -1,7 +1,16 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { execSync } from "child_process";
3
- import { readFileSync, writeFileSync, mkdirSync } from "fs";
4
- import { join } from "path";
3
+ import {
4
+ readFileSync,
5
+ writeFileSync,
6
+ mkdirSync,
7
+ existsSync,
8
+ readdirSync,
9
+ statSync,
10
+ copyFileSync,
11
+ } from "fs";
12
+ import { join, dirname } from "path";
13
+ import { fileURLToPath } from "url";
5
14
  import { homedir } from "os";
6
15
  import { requireAuth } from "../auth-guard.js";
7
16
  import { listAgents } from "../api.js";
@@ -129,6 +138,85 @@ export async function setupCommand() {
129
138
  .join("\n")
130
139
  );
131
140
 
141
+ // Step: Install extension files
142
+ const __dirname = dirname(fileURLToPath(import.meta.url));
143
+ const extensionSource = join(__dirname, "..", "..", "extension");
144
+ const extensionDest = join(home, ".clawcard", "extension");
145
+
146
+ if (existsSync(extensionSource)) {
147
+ const s4 = p.spinner();
148
+ s4.start("Installing ClawCard Pay extension...");
149
+
150
+ mkdirSync(extensionDest, { recursive: true });
151
+
152
+ function copyDir(src, dest) {
153
+ mkdirSync(dest, { recursive: true });
154
+ for (const entry of readdirSync(src)) {
155
+ const srcPath = join(src, entry);
156
+ const destPath = join(dest, entry);
157
+ if (statSync(srcPath).isDirectory()) {
158
+ copyDir(srcPath, destPath);
159
+ } else {
160
+ copyFileSync(srcPath, destPath);
161
+ }
162
+ }
163
+ }
164
+
165
+ copyDir(extensionSource, extensionDest);
166
+
167
+ // Register native messaging host
168
+ const nativeHostScript = join(__dirname, "..", "native-host.js");
169
+ const platform = process.platform;
170
+ let nativeHostDir;
171
+
172
+ if (platform === "darwin") {
173
+ nativeHostDir = join(
174
+ home,
175
+ "Library",
176
+ "Application Support",
177
+ "Google",
178
+ "Chrome",
179
+ "NativeMessagingHosts"
180
+ );
181
+ } else {
182
+ nativeHostDir = join(
183
+ home,
184
+ ".config",
185
+ "google-chrome",
186
+ "NativeMessagingHosts"
187
+ );
188
+ }
189
+
190
+ mkdirSync(nativeHostDir, { recursive: true });
191
+
192
+ const nativeHostManifest = {
193
+ name: "com.clawcard.pay",
194
+ description: "ClawCard payment form filler",
195
+ path: nativeHostScript,
196
+ type: "stdio",
197
+ allowed_origins: ["chrome-extension://*/"],
198
+ };
199
+
200
+ writeFileSync(
201
+ join(nativeHostDir, "com.clawcard.pay.json"),
202
+ JSON.stringify(nativeHostManifest, null, 2)
203
+ );
204
+
205
+ s4.stop("Extension installed!");
206
+
207
+ p.note(
208
+ [
209
+ "To enable payment form filling:",
210
+ "",
211
+ `1. Open Chrome → ${orange("chrome://extensions")}`,
212
+ `2. Enable ${orange("Developer Mode")} (top right)`,
213
+ `3. Click ${orange("Load Unpacked")}`,
214
+ `4. Select: ${chalk.dim(extensionDest)}`,
215
+ ].join("\n"),
216
+ "ClawCard Pay Extension"
217
+ );
218
+ }
219
+
132
220
  p.outro(
133
221
  chalk.dim('Tell your agent to "use ClawCard to check your identity"')
134
222
  );
package/src/index.js CHANGED
@@ -257,6 +257,17 @@ agent
257
257
  await agentActivityCmd(options);
258
258
  });
259
259
 
260
+ // Agent pay (fill payment form via extension)
261
+ agent
262
+ .command("pay")
263
+ .description("Fill payment form via browser extension")
264
+ .requiredOption("--card-id <cardId>", "Card ID to use for payment")
265
+ .option("--json", "Output as JSON")
266
+ .action(async (options) => {
267
+ const { agentPayCmd } = await import("./commands/agent.js");
268
+ await agentPayCmd(options);
269
+ });
270
+
260
271
  // Setup
261
272
  program
262
273
  .command("setup")
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ClawCard Native Messaging Host
4
+ // Chrome spawns this when the extension calls connectNative().
5
+ // Watches for pending pay commands in ~/.clawcard/pending-pay.json
6
+ // and relays them to the extension. Writes responses to pay-response.json.
7
+
8
+ import { readFileSync, writeFileSync, unlinkSync, existsSync, watchFile } from "fs";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+
12
+ const HOME = homedir();
13
+ const PENDING_PATH = join(HOME, ".clawcard", "pending-pay.json");
14
+ const RESPONSE_PATH = join(HOME, ".clawcard", "pay-response.json");
15
+
16
+ // ── Native messaging protocol ──
17
+ // Messages are JSON prefixed with a 32-bit length in native byte order.
18
+
19
+ function sendMessage(msg) {
20
+ const json = JSON.stringify(msg);
21
+ const header = Buffer.alloc(4);
22
+ header.writeUInt32LE(json.length);
23
+ process.stdout.write(header);
24
+ process.stdout.write(json);
25
+ }
26
+
27
+ // Read a single native message from stdin
28
+ function readNextMessage() {
29
+ return new Promise((resolve) => {
30
+ let buffer = Buffer.alloc(0);
31
+ let expectedLen = null;
32
+
33
+ function onData(chunk) {
34
+ buffer = Buffer.concat([buffer, chunk]);
35
+
36
+ // Read header
37
+ if (expectedLen === null && buffer.length >= 4) {
38
+ expectedLen = buffer.readUInt32LE(0);
39
+ buffer = buffer.slice(4);
40
+ }
41
+
42
+ // Read body
43
+ if (expectedLen !== null && buffer.length >= expectedLen) {
44
+ process.stdin.removeListener("data", onData);
45
+ const json = buffer.slice(0, expectedLen).toString();
46
+ try {
47
+ resolve(JSON.parse(json));
48
+ } catch {
49
+ resolve(null);
50
+ }
51
+ }
52
+ }
53
+
54
+ process.stdin.on("data", onData);
55
+ });
56
+ }
57
+
58
+ // Check for pending pay commands
59
+ function checkPending() {
60
+ try {
61
+ if (existsSync(PENDING_PATH)) {
62
+ const content = readFileSync(PENDING_PATH, "utf-8");
63
+ const command = JSON.parse(content);
64
+ unlinkSync(PENDING_PATH);
65
+ sendMessage(command);
66
+ }
67
+ } catch {
68
+ // File doesn't exist or is being written — skip
69
+ }
70
+ }
71
+
72
+ // Poll for pending commands
73
+ setInterval(checkPending, 500);
74
+
75
+ // Also watch for file changes (faster response)
76
+ try {
77
+ watchFile(PENDING_PATH, { interval: 200 }, () => checkPending());
78
+ } catch {
79
+ // watchFile may fail if file doesn't exist yet
80
+ }
81
+
82
+ // Listen for responses from extension
83
+ async function listenForResponses() {
84
+ while (true) {
85
+ const response = await readNextMessage();
86
+ if (response) {
87
+ try {
88
+ writeFileSync(RESPONSE_PATH, JSON.stringify(response));
89
+ } catch {
90
+ // write failed
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ listenForResponses();
97
+
98
+ // Keep process alive
99
+ process.stdin.resume();