@clawcard/cli 2.0.7 → 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.
- package/extension/background.js +99 -0
- package/extension/content.js +125 -0
- package/extension/icons/.keg/main-context.txt +14 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +22 -0
- package/package.json +4 -2
- package/skill/SKILL.md +136 -0
- package/src/commands/agent.js +72 -0
- package/src/commands/help.js +1 -0
- package/src/commands/setup.js +90 -2
- package/src/index.js +11 -0
- package/src/native-host.js +99 -0
- package/src/splash.js +6 -3
|
@@ -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
|
|
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.
|
package/src/commands/agent.js
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/help.js
CHANGED
|
@@ -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"],
|
package/src/commands/setup.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
|
-
import {
|
|
4
|
-
|
|
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();
|
package/src/splash.js
CHANGED
|
@@ -42,6 +42,8 @@ export async function checkForUpdate() {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export async function performAutoUpdate() {
|
|
45
|
+
if (process.env.CLAWCARD_SKIP_UPDATE) return;
|
|
46
|
+
|
|
45
47
|
const latest = await checkForUpdate();
|
|
46
48
|
if (!latest) return;
|
|
47
49
|
|
|
@@ -54,11 +56,12 @@ export async function performAutoUpdate() {
|
|
|
54
56
|
});
|
|
55
57
|
console.log(` Updated to v${latest}. Restarting...\n`);
|
|
56
58
|
|
|
57
|
-
// Re-run the
|
|
59
|
+
// Re-run using the clawcard binary directly (not the cached process)
|
|
60
|
+
const args = process.argv.slice(2); // skip node + script path
|
|
58
61
|
const { spawnSync } = await import("child_process");
|
|
59
|
-
const result = spawnSync(
|
|
62
|
+
const result = spawnSync("clawcard", args, {
|
|
60
63
|
stdio: "inherit",
|
|
61
|
-
env: process.env,
|
|
64
|
+
env: { ...process.env, CLAWCARD_SKIP_UPDATE: "1" },
|
|
62
65
|
});
|
|
63
66
|
process.exit(result.status ?? 0);
|
|
64
67
|
} catch {
|