smsforwarder 0.0.0-stage → 1.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 (3) hide show
  1. package/README.md +24 -2
  2. package/package.json +10 -4
  3. package/smsforwarder.mjs +161 -0
package/README.md CHANGED
@@ -1,3 +1,25 @@
1
- # Temporary Holding Version
1
+ # smsforwarder
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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 --timeout 90)
11
+ ```
12
+
13
+ `code` waits for the newest code to arrive and prints only the code. It exits with status 2 if nothing arrives in time. If other codes arrived in the same window it says so on stderr; `smsforwarder messages --codes` lists them so you can pick by the text.
14
+
15
+ ```
16
+ smsforwarder code [--timeout sec] [--since time] [--json]
17
+ smsforwarder messages [--minutes n] [--codes] [--limit n] [--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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.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
+ }
@@ -0,0 +1,161 @@
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 newest verification code and print it
15
+ --timeout <sec> How long to wait, max 120 (default 60)
16
+ --since <time> Only messages after this ISO time / unix ms (default: 2 minutes ago)
17
+ --json Print the whole result as JSON, including other codes that arrived in the same window
18
+ smsforwarder messages [--minutes n] [--codes] [--limit n] [--json]
19
+ Messages from the last n minutes (default 10), newest first
20
+ smsforwarder devices List connected phones
21
+ smsforwarder webhook set <url> Send every new message to your URL (prints the signing secret)
22
+ smsforwarder webhook rm Stop sending to the webhook
23
+ smsforwarder webhook test Send a test event
24
+ smsforwarder mcp Print MCP config for Claude, Cursor and other AI tools
25
+
26
+ The key can also come from SMSFORWARDER_API_KEY. Docs: https://smsforwarder.app/docs`;
27
+
28
+ function die(msg, code = 1) {
29
+ process.stderr.write(msg + "\n");
30
+ process.exit(code);
31
+ }
32
+
33
+ function parse(argv) {
34
+ const pos = [];
35
+ const opt = {};
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const a = argv[i];
38
+ if (a.startsWith("--")) {
39
+ const [k, v] = a.slice(2).split("=", 2);
40
+ if (v !== undefined) opt[k] = v;
41
+ else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith("--")) opt[k] = argv[++i];
42
+ else opt[k] = true;
43
+ } else pos.push(a);
44
+ }
45
+ return { pos, opt };
46
+ }
47
+
48
+ function apiKey() {
49
+ if (process.env.SMSFORWARDER_API_KEY) return process.env.SMSFORWARDER_API_KEY;
50
+ try {
51
+ return JSON.parse(readFileSync(CONFIG, "utf8")).apiKey;
52
+ } catch {
53
+ die("No API key. Run `smsforwarder login <api-key>` (the key is shown in the phone app) or set SMSFORWARDER_API_KEY.");
54
+ }
55
+ }
56
+
57
+ async function call(method, path, body) {
58
+ const res = await fetch(API + path, {
59
+ method,
60
+ headers: { authorization: `Bearer ${apiKey()}`, "content-type": "application/json", "user-agent": "smsforwarder-cli/1.1.0" },
61
+ body: body ? JSON.stringify(body) : undefined,
62
+ });
63
+ const data = await res.json().catch(() => ({}));
64
+ if (!res.ok && res.status !== 404) die(`Error ${res.status}: ${data?.error?.message ?? res.statusText}`);
65
+ return { status: res.status, data };
66
+ }
67
+
68
+ function qs(o) {
69
+ const p = new URLSearchParams();
70
+ for (const [k, v] of Object.entries(o)) if (v !== undefined && v !== null && v !== false) p.set(k, String(v));
71
+ const s = p.toString();
72
+ return s ? "?" + s : "";
73
+ }
74
+
75
+ function ago(iso) {
76
+ const s = Math.round((Date.now() - Date.parse(iso)) / 1000);
77
+ if (s < 60) return `${s}s ago`;
78
+ if (s < 3600) return `${Math.round(s / 60)}m ago`;
79
+ if (s < 86400) return `${Math.round(s / 3600)}h ago`;
80
+ return `${Math.round(s / 86400)}d ago`;
81
+ }
82
+
83
+ const { pos, opt } = parse(process.argv.slice(2));
84
+ const cmd = pos[0];
85
+ for (const gone of ["from", "contains"]) {
86
+ if (opt[gone] !== undefined) die(`--${gone} is gone: who sends a code can't be predicted. Take the newest code, or list recent messages and pick by text.`);
87
+ }
88
+
89
+ switch (cmd) {
90
+ case "login": {
91
+ const key = pos[1];
92
+ if (!key) die("Usage: smsforwarder login <api-key>");
93
+ process.env.SMSFORWARDER_API_KEY = key;
94
+ const { data } = await call("GET", "/v1/me");
95
+ mkdirSync(join(CONFIG, ".."), { recursive: true });
96
+ writeFileSync(CONFIG, JSON.stringify({ apiKey: key }, null, 2), { mode: 0o600 });
97
+ console.log(`Saved. Plan: ${data.plan}, ${data.usage.messagesThisMonth}/${data.limits.messagesPerMonth} messages this month.`);
98
+ break;
99
+ }
100
+ case "code":
101
+ case "wait": {
102
+ const timeout = Math.min(Number(opt.timeout ?? 60) || 60, 120);
103
+ const { status, data } = await call("GET", "/v1/codes/wait" + qs({ since: opt.since, timeout }));
104
+ if (status === 404 || !data.code) die(`No code arrived within ${timeout}s.`, 2);
105
+ if (opt.json) console.log(JSON.stringify(data, null, 2));
106
+ else {
107
+ console.log(data.code);
108
+ if (data.otherCodes?.length) {
109
+ process.stderr.write(`${data.otherCodes.length} other code(s) arrived in the same window; run \`smsforwarder messages --codes\` to pick by text.\n`);
110
+ }
111
+ }
112
+ break;
113
+ }
114
+ case "messages":
115
+ case "ls": {
116
+ const { data } = await call("GET", "/v1/messages" + qs({ minutes: opt.minutes ?? 10, limit: opt.limit ?? 20, codes_only: opt.codes ? "true" : undefined }));
117
+ if (opt.json) {
118
+ console.log(JSON.stringify(data.messages, null, 2));
119
+ break;
120
+ }
121
+ if (!data.messages.length) console.log("No messages yet.");
122
+ for (const m of data.messages) {
123
+ const who = m.service || m.from || "?";
124
+ console.log(`${ago(m.receivedAt).padEnd(8)} ${who}${m.code ? ` [${m.code}]` : ""}\n ${m.body.replace(/\s+/g, " ").slice(0, 160)}`);
125
+ }
126
+ break;
127
+ }
128
+ case "devices": {
129
+ const { data } = await call("GET", "/v1/devices");
130
+ for (const d of data.devices) console.log(`${d.name} (${d.platform}) last seen ${d.lastSeenAt ? ago(d.lastSeenAt) : "never"}`);
131
+ break;
132
+ }
133
+ case "webhook": {
134
+ if (pos[1] === "set" && pos[2]) {
135
+ const { data } = await call("PUT", "/v1/webhook", { url: pos[2] });
136
+ console.log(`Webhook set. Verify the x-signature header (HMAC-SHA256 of "<t>.<body>") with this secret:\n${data.secret}`);
137
+ } else if (pos[1] === "rm") {
138
+ await call("DELETE", "/v1/webhook");
139
+ console.log("Webhook removed.");
140
+ } else if (pos[1] === "test") {
141
+ await call("POST", "/v1/webhook/test");
142
+ console.log("Test event sent.");
143
+ } else die("Usage: smsforwarder webhook set <url> | rm | test");
144
+ break;
145
+ }
146
+ case "mcp": {
147
+ const key = apiKey();
148
+ console.log(`Claude Code:\n claude mcp add --transport http sms-forwarder ${API}/mcp --header "Authorization: Bearer ${key}"\n`);
149
+ console.log("Cursor / Claude Desktop / other MCP clients (mcp.json):");
150
+ console.log(JSON.stringify({ mcpServers: { "sms-forwarder": { type: "http", url: `${API}/mcp`, headers: { Authorization: `Bearer ${key}` } } } }, null, 2));
151
+ break;
152
+ }
153
+ case undefined:
154
+ case "help":
155
+ case "--help":
156
+ case "-h":
157
+ console.log(HELP);
158
+ break;
159
+ default:
160
+ die(`Unknown command "${cmd}".\n\n${HELP}`);
161
+ }