flomo-post 0.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -0
  3. package/dist/cli.js +189 -0
  4. package/package.json +50 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tmasjc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # Flomo Post
2
+
3
+ Post notes to [Flomo](https://flomoapp.com) from the command line.
4
+
5
+ Zero runtime dependencies — a single small binary built on Node's native `fetch`.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js ≥ 18
10
+ - A Flomo account with API access (Pro account) — you need your personal
11
+ incoming-webhook URL, found in Flomo under **Settings → API**. It looks like
12
+ `https://flomoapp.com/iwh/<id>/<token>/`.
13
+
14
+ ## Quick start
15
+
16
+ ```bash
17
+ # one-time setup: paste your webhook URL at the prompt
18
+ npx flomo-post init
19
+
20
+ # post a note
21
+ npx flomo-post new "Reading notes on CLI design #dev"
22
+ ```
23
+
24
+ Tags are just inline `#hashtags` — flomo parses them natively.
25
+
26
+ ## Commands
27
+
28
+ | Command | Behavior |
29
+ |---|---|
30
+ | `flomo-post init` | Prompt for your webhook URL, validate it, save to the config file |
31
+ | `flomo-post test` | Post `Hello World!` with a timestamp (verifies your setup) |
32
+ | `flomo-post new <content...>` | Post the given content |
33
+ | `flomo-post --help` | Show usage |
34
+ | `flomo-post --version` | Show version |
35
+
36
+ `new` also reads piped stdin when no arguments are given:
37
+
38
+ ```bash
39
+ echo "captured from a pipeline #inbox" | flomo-post new
40
+ ```
41
+
42
+ ## Configuration
43
+
44
+ The webhook URL is resolved in precedence order:
45
+
46
+ 1. `FLOMO_API_URL` environment variable
47
+ 2. `~/.config/flomo-post/config.json` — written by `flomo-post init` with
48
+ file mode `600`
49
+
50
+ The env var is handy for scripts and CI; `init` is better for interactive
51
+ use since the URL never touches your shell history.
52
+
53
+ **The webhook URL is a secret** — it is the credential for your flomo
54
+ account. flomo-post never prints it: error messages redact it to
55
+ `https://flomoapp.com/iwh/****`.
56
+
57
+ ## Exit codes
58
+
59
+ | Code | Meaning |
60
+ |---|---|
61
+ | `0` | Posted successfully |
62
+ | `1` | Network or API failure |
63
+ | `2` | Usage or configuration error |
64
+
65
+ Errors go to stderr; normal output to stdout.
66
+
67
+ ## Development
68
+
69
+ ```bash
70
+ npm install
71
+ npm test # vitest, fetch always mocked — never hits the network
72
+ npm run typecheck # tsc --noEmit, strict
73
+ npm run build # tsup → dist/cli.js
74
+ npm run dev -- new "from source" # run the CLI via tsx
75
+ ```
76
+
77
+ ## License
78
+
79
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { realpathSync } from "fs";
5
+ import { createRequire } from "module";
6
+ import { createInterface } from "readline/promises";
7
+ import { pathToFileURL } from "url";
8
+
9
+ // src/post.ts
10
+ function redactUrl(_url) {
11
+ return "https://flomoapp.com/iwh/****";
12
+ }
13
+ async function postNote(apiUrl, content) {
14
+ const redact = (s) => s.split(apiUrl).join(redactUrl(apiUrl));
15
+ let res;
16
+ try {
17
+ res = await fetch(apiUrl, {
18
+ method: "POST",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify({ content, content_type: "markdown" })
21
+ });
22
+ } catch (err) {
23
+ const msg = err instanceof Error ? err.message : String(err);
24
+ const cause = err instanceof Error && err.cause instanceof Error ? `: ${err.cause.message}` : "";
25
+ return { ok: false, message: `Network error: ${redact(msg + cause)}` };
26
+ }
27
+ let body;
28
+ try {
29
+ body = await res.text();
30
+ } catch (err) {
31
+ const msg = err instanceof Error ? err.message : String(err);
32
+ return { ok: false, message: `Network error: ${redact(msg)}` };
33
+ }
34
+ let message = body;
35
+ try {
36
+ const parsed = JSON.parse(body);
37
+ if (parsed.message) message = parsed.message;
38
+ } catch {
39
+ }
40
+ if (!res.ok) {
41
+ return { ok: false, message: `API error (HTTP ${res.status}): ${message}` };
42
+ }
43
+ return { ok: true, message };
44
+ }
45
+
46
+ // src/config.ts
47
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
48
+ import { homedir } from "os";
49
+ import { dirname, join } from "path";
50
+ function getConfigPath() {
51
+ return join(homedir(), ".config", "flomo-post", "config.json");
52
+ }
53
+ function isValidApiUrl(url) {
54
+ return /^https:\/\/flomoapp\.com\/iwh\/.+/.test(url);
55
+ }
56
+ function resolveApiUrl() {
57
+ const envUrl = process.env.FLOMO_API_URL;
58
+ if (envUrl) return envUrl;
59
+ try {
60
+ const raw = readFileSync(getConfigPath(), "utf8");
61
+ const parsed = JSON.parse(raw);
62
+ return parsed.apiUrl ?? null;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ function saveApiUrl(url) {
68
+ const path = getConfigPath();
69
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
70
+ writeFileSync(path, JSON.stringify({ apiUrl: url }, null, 2) + "\n", { mode: 384 });
71
+ chmodSync(path, 384);
72
+ return path;
73
+ }
74
+
75
+ // src/cli.ts
76
+ var VERSION = createRequire(import.meta.url)("../package.json").version;
77
+ async function readAll(stream) {
78
+ const chunks = [];
79
+ for await (const chunk of stream) chunks.push(Buffer.from(chunk));
80
+ return Buffer.concat(chunks).toString("utf8");
81
+ }
82
+ var HELP = `flomo-post \u2014 post notes to flomo
83
+
84
+ Usage:
85
+ flomo-post init Save your webhook URL to the config file
86
+ flomo-post test Post "Hello World!" with a timestamp
87
+ flomo-post new <content...> Post content (or pipe it via stdin)
88
+ flomo-post --help Show this help
89
+ flomo-post --version Show the version
90
+
91
+ Configuration (in precedence order):
92
+ FLOMO_API_URL Webhook URL (overrides the config file)
93
+ ~/.config/flomo-post/config.json Written by \`flomo-post init\``;
94
+ async function main(argv, deps) {
95
+ const [command, ...rest] = argv;
96
+ if (command === "--help" || command === "-h") {
97
+ deps.stdout(HELP);
98
+ return 0;
99
+ }
100
+ if (command === "--version" || command === "-v") {
101
+ deps.stdout(VERSION);
102
+ return 0;
103
+ }
104
+ if (command === "init") return runInit(deps);
105
+ if (command === "test") {
106
+ return doPost(deps, `Hello World! ${(/* @__PURE__ */ new Date()).toISOString()}`);
107
+ }
108
+ if (command === "new") return runNew(rest, deps);
109
+ deps.stderr(HELP);
110
+ return 2;
111
+ }
112
+ async function runNew(args, deps) {
113
+ let content = args.join(" ").trim();
114
+ if (!content && !deps.isStdinTTY) {
115
+ content = (await deps.readStdin()).trim();
116
+ }
117
+ if (!content) {
118
+ deps.stderr("Usage: flomo-post new <content...> (or pipe content via stdin)");
119
+ return 2;
120
+ }
121
+ return doPost(deps, content);
122
+ }
123
+ async function doPost(deps, content) {
124
+ const apiUrl = deps.resolveApiUrl();
125
+ if (!apiUrl) {
126
+ deps.stderr("No API URL configured. Run `flomo-post init` or set FLOMO_API_URL.");
127
+ return 2;
128
+ }
129
+ const result = await deps.post(apiUrl, content);
130
+ if (!result.ok) {
131
+ deps.stderr(result.message);
132
+ return 1;
133
+ }
134
+ deps.stdout("Posted.");
135
+ return 0;
136
+ }
137
+ async function runInit(deps) {
138
+ const url = (await deps.promptForUrl()).trim();
139
+ if (!deps.isValidApiUrl(url)) {
140
+ deps.stderr("Invalid URL \u2014 expected the shape https://flomoapp.com/iwh/<id>/<token>/");
141
+ return 2;
142
+ }
143
+ const path = deps.saveApiUrl(url);
144
+ deps.stdout(`Saved to ${path}`);
145
+ return 0;
146
+ }
147
+ function defaultDeps() {
148
+ return {
149
+ stdout: (line) => process.stdout.write(line + "\n"),
150
+ stderr: (line) => process.stderr.write(line + "\n"),
151
+ isStdinTTY: process.stdin.isTTY === true,
152
+ readStdin: () => readAll(process.stdin),
153
+ promptForUrl: () => new Promise((resolve, reject) => {
154
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
155
+ rl.once("close", () => reject(new Error("stdin closed before answer")));
156
+ rl.question("Paste your flomo webhook URL: ").then((answer) => {
157
+ resolve(answer);
158
+ rl.close();
159
+ }, reject);
160
+ }),
161
+ post: postNote,
162
+ resolveApiUrl,
163
+ saveApiUrl,
164
+ isValidApiUrl
165
+ };
166
+ }
167
+ function isDirectRun() {
168
+ if (!process.argv[1]) return false;
169
+ try {
170
+ return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
171
+ } catch {
172
+ return false;
173
+ }
174
+ }
175
+ if (isDirectRun()) {
176
+ main(process.argv.slice(2), defaultDeps()).then(
177
+ (code) => process.exit(code),
178
+ (err) => {
179
+ const msg = err instanceof Error ? err.message : String(err);
180
+ process.stderr.write(msg + "\n");
181
+ process.exit(1);
182
+ }
183
+ );
184
+ }
185
+ export {
186
+ defaultDeps,
187
+ main,
188
+ readAll
189
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "flomo-post",
3
+ "version": "0.1.0",
4
+ "description": "Post notes to flomo from the command line",
5
+ "keywords": [
6
+ "flomo",
7
+ "cli",
8
+ "notes",
9
+ "note-taking",
10
+ "webhook",
11
+ "memo"
12
+ ],
13
+ "homepage": "https://github.com/tmasjc/flomo-post-cli#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/tmasjc/flomo-post-cli/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/tmasjc/flomo-post-cli.git"
20
+ },
21
+ "author": "tmasjc",
22
+ "type": "module",
23
+ "bin": {
24
+ "flomo-post": "dist/cli.js"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "scripts": {
36
+ "build": "tsup src/cli.ts --format esm --clean",
37
+ "test": "vitest run",
38
+ "typecheck": "tsc --noEmit",
39
+ "dev": "tsx src/cli.ts",
40
+ "prepublishOnly": "npm test && npm run build"
41
+ },
42
+ "license": "MIT",
43
+ "devDependencies": {
44
+ "@types/node": "^26.1.1",
45
+ "tsup": "^8.5.1",
46
+ "tsx": "^4.23.1",
47
+ "typescript": "^7.0.2",
48
+ "vitest": "^4.1.10"
49
+ }
50
+ }