smsforwarder 0.0.0-stage → 1.0.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/README.md +24 -2
- package/package.json +10 -4
- package/smsforwarder.mjs +153 -0
package/README.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# smsforwarder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Get SMS verification codes from your own Android phone in shell scripts and AI agents.
|
|
4
|
+
|
|
5
|
+
Install [SMS Forwarder](https://smsforwarder.app/download/) on the phone that receives the texts, copy the API key from the app, then:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install -g smsforwarder
|
|
9
|
+
smsforwarder login sk_your_key
|
|
10
|
+
CODE=$(smsforwarder code --from GitHub --timeout 90)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`code` waits until a matching code arrives and prints only the code. It exits with status 2 if nothing arrives in time.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
smsforwarder code [--from x] [--contains x] [--timeout sec] [--json]
|
|
17
|
+
smsforwarder messages [--from x] [--limit n] [--codes] [--json]
|
|
18
|
+
smsforwarder devices
|
|
19
|
+
smsforwarder webhook set <url> | rm | test
|
|
20
|
+
smsforwarder mcp # MCP config for Claude, Cursor and other AI tools
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The key can also come from `SMSFORWARDER_API_KEY`. Needs Node.js 18 or newer.
|
|
24
|
+
|
|
25
|
+
Docs: https://smsforwarder.app/docs/ · 中文: https://duanxinzhuanfa.com/
|
package/package.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smsforwarder",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Get SMS verification codes from your own phone in scripts and AI agents. CLI for SMS Forwarder (smsforwarder.app).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "smsforwarder": "smsforwarder.mjs" },
|
|
7
|
+
"files": ["smsforwarder.mjs", "README.md"],
|
|
8
|
+
"engines": { "node": ">=18" },
|
|
9
|
+
"keywords": ["sms", "otp", "2fa", "verification code", "sms forwarder", "mcp", "ai agent", "sms gateway"],
|
|
10
|
+
"homepage": "https://smsforwarder.app",
|
|
11
|
+
"license": "MIT"
|
|
12
|
+
}
|
package/smsforwarder.mjs
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SMS Forwarder CLI — read texts and verification codes forwarded from your phone.
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const API = process.env.SMSFORWARDER_API_URL || "https://api.smsforwarder.app";
|
|
8
|
+
const CONFIG = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "smsforwarder", "config.json");
|
|
9
|
+
|
|
10
|
+
const HELP = `SMS Forwarder — verification codes from your phone, in your terminal and AI agents.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
smsforwarder login <api-key> Save your API key (from the phone app)
|
|
14
|
+
smsforwarder code [options] Wait for the next verification code and print it
|
|
15
|
+
--from <text> Only codes from this service / sender (e.g. Google, 淘宝)
|
|
16
|
+
--contains <text> Only messages containing this text
|
|
17
|
+
--timeout <sec> How long to wait, max 120 (default 60)
|
|
18
|
+
--since <time> Only messages after this ISO time / unix ms (default: 2 minutes ago)
|
|
19
|
+
--json Print the whole message as JSON
|
|
20
|
+
smsforwarder messages [--from x] [--contains x] [--limit n] [--codes] [--json]
|
|
21
|
+
smsforwarder devices List connected phones
|
|
22
|
+
smsforwarder webhook set <url> Send every new message to your URL (prints the signing secret)
|
|
23
|
+
smsforwarder webhook rm Stop sending to the webhook
|
|
24
|
+
smsforwarder webhook test Send a test event
|
|
25
|
+
smsforwarder mcp Print MCP config for Claude, Cursor and other AI tools
|
|
26
|
+
|
|
27
|
+
The key can also come from SMSFORWARDER_API_KEY. Docs: https://smsforwarder.app/docs`;
|
|
28
|
+
|
|
29
|
+
function die(msg, code = 1) {
|
|
30
|
+
process.stderr.write(msg + "\n");
|
|
31
|
+
process.exit(code);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parse(argv) {
|
|
35
|
+
const pos = [];
|
|
36
|
+
const opt = {};
|
|
37
|
+
for (let i = 0; i < argv.length; i++) {
|
|
38
|
+
const a = argv[i];
|
|
39
|
+
if (a.startsWith("--")) {
|
|
40
|
+
const [k, v] = a.slice(2).split("=", 2);
|
|
41
|
+
if (v !== undefined) opt[k] = v;
|
|
42
|
+
else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith("--")) opt[k] = argv[++i];
|
|
43
|
+
else opt[k] = true;
|
|
44
|
+
} else pos.push(a);
|
|
45
|
+
}
|
|
46
|
+
return { pos, opt };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function apiKey() {
|
|
50
|
+
if (process.env.SMSFORWARDER_API_KEY) return process.env.SMSFORWARDER_API_KEY;
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(readFileSync(CONFIG, "utf8")).apiKey;
|
|
53
|
+
} catch {
|
|
54
|
+
die("No API key. Run `smsforwarder login <api-key>` (the key is shown in the phone app) or set SMSFORWARDER_API_KEY.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function call(method, path, body) {
|
|
59
|
+
const res = await fetch(API + path, {
|
|
60
|
+
method,
|
|
61
|
+
headers: { authorization: `Bearer ${apiKey()}`, "content-type": "application/json", "user-agent": "smsforwarder-cli/1.0.0" },
|
|
62
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
63
|
+
});
|
|
64
|
+
const data = await res.json().catch(() => ({}));
|
|
65
|
+
if (!res.ok && res.status !== 404) die(`Error ${res.status}: ${data?.error?.message ?? res.statusText}`);
|
|
66
|
+
return { status: res.status, data };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function qs(o) {
|
|
70
|
+
const p = new URLSearchParams();
|
|
71
|
+
for (const [k, v] of Object.entries(o)) if (v !== undefined && v !== null && v !== false) p.set(k, String(v));
|
|
72
|
+
const s = p.toString();
|
|
73
|
+
return s ? "?" + s : "";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ago(iso) {
|
|
77
|
+
const s = Math.round((Date.now() - Date.parse(iso)) / 1000);
|
|
78
|
+
if (s < 60) return `${s}s ago`;
|
|
79
|
+
if (s < 3600) return `${Math.round(s / 60)}m ago`;
|
|
80
|
+
if (s < 86400) return `${Math.round(s / 3600)}h ago`;
|
|
81
|
+
return `${Math.round(s / 86400)}d ago`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { pos, opt } = parse(process.argv.slice(2));
|
|
85
|
+
const cmd = pos[0];
|
|
86
|
+
|
|
87
|
+
switch (cmd) {
|
|
88
|
+
case "login": {
|
|
89
|
+
const key = pos[1];
|
|
90
|
+
if (!key) die("Usage: smsforwarder login <api-key>");
|
|
91
|
+
process.env.SMSFORWARDER_API_KEY = key;
|
|
92
|
+
const { data } = await call("GET", "/v1/me");
|
|
93
|
+
mkdirSync(join(CONFIG, ".."), { recursive: true });
|
|
94
|
+
writeFileSync(CONFIG, JSON.stringify({ apiKey: key }, null, 2), { mode: 0o600 });
|
|
95
|
+
console.log(`Saved. Plan: ${data.plan}, ${data.usage.messagesThisMonth}/${data.limits.messagesPerMonth} messages this month.`);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case "code":
|
|
99
|
+
case "wait": {
|
|
100
|
+
const timeout = Math.min(Number(opt.timeout ?? 60) || 60, 120);
|
|
101
|
+
const { status, data } = await call("GET", "/v1/codes/wait" + qs({ from: opt.from, contains: opt.contains, since: opt.since, timeout }));
|
|
102
|
+
if (status === 404 || !data.code) die(`No code arrived within ${timeout}s.`, 2);
|
|
103
|
+
console.log(opt.json ? JSON.stringify(data.message, null, 2) : data.code);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case "messages":
|
|
107
|
+
case "ls": {
|
|
108
|
+
const { data } = await call("GET", "/v1/messages" + qs({ from: opt.from, contains: opt.contains, limit: opt.limit ?? 20, codes_only: opt.codes ? "true" : undefined }));
|
|
109
|
+
if (opt.json) {
|
|
110
|
+
console.log(JSON.stringify(data.messages, null, 2));
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
if (!data.messages.length) console.log("No messages yet.");
|
|
114
|
+
for (const m of data.messages) {
|
|
115
|
+
const who = m.service || m.from || "?";
|
|
116
|
+
console.log(`${ago(m.receivedAt).padEnd(8)} ${who}${m.code ? ` [${m.code}]` : ""}\n ${m.body.replace(/\s+/g, " ").slice(0, 160)}`);
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case "devices": {
|
|
121
|
+
const { data } = await call("GET", "/v1/devices");
|
|
122
|
+
for (const d of data.devices) console.log(`${d.name} (${d.platform}) last seen ${d.lastSeenAt ? ago(d.lastSeenAt) : "never"}`);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "webhook": {
|
|
126
|
+
if (pos[1] === "set" && pos[2]) {
|
|
127
|
+
const { data } = await call("PUT", "/v1/webhook", { url: pos[2] });
|
|
128
|
+
console.log(`Webhook set. Verify the x-signature header (HMAC-SHA256 of "<t>.<body>") with this secret:\n${data.secret}`);
|
|
129
|
+
} else if (pos[1] === "rm") {
|
|
130
|
+
await call("DELETE", "/v1/webhook");
|
|
131
|
+
console.log("Webhook removed.");
|
|
132
|
+
} else if (pos[1] === "test") {
|
|
133
|
+
await call("POST", "/v1/webhook/test");
|
|
134
|
+
console.log("Test event sent.");
|
|
135
|
+
} else die("Usage: smsforwarder webhook set <url> | rm | test");
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case "mcp": {
|
|
139
|
+
const key = apiKey();
|
|
140
|
+
console.log(`Claude Code:\n claude mcp add --transport http sms-forwarder ${API}/mcp --header "Authorization: Bearer ${key}"\n`);
|
|
141
|
+
console.log("Cursor / Claude Desktop / other MCP clients (mcp.json):");
|
|
142
|
+
console.log(JSON.stringify({ mcpServers: { "sms-forwarder": { type: "http", url: `${API}/mcp`, headers: { Authorization: `Bearer ${key}` } } } }, null, 2));
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case undefined:
|
|
146
|
+
case "help":
|
|
147
|
+
case "--help":
|
|
148
|
+
case "-h":
|
|
149
|
+
console.log(HELP);
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
die(`Unknown command "${cmd}".\n\n${HELP}`);
|
|
153
|
+
}
|