freshjots 0.2.1 → 0.3.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 CHANGED
@@ -41,6 +41,47 @@ The whole API is four methods: `notes()`, `note(filename)`,
41
41
  `create()` return the note object directly (no `{ note: … }` wrapper);
42
42
  `notes()` returns the array.
43
43
 
44
+ ## CLI
45
+
46
+ Installing the package globally puts a `freshjots` command on your
47
+ PATH, so you can read and write notes straight from a terminal without
48
+ writing any JavaScript. This works in bash, zsh, fish, Windows
49
+ PowerShell, and CMD — npm generates a `.cmd` shim on Windows
50
+ automatically.
51
+
52
+ ```sh
53
+ npm install -g freshjots
54
+ export FRESHJOTS_TOKEN=mn_… # PowerShell: $env:FRESHJOTS_TOKEN = "mn_…"
55
+ ```
56
+
57
+ The CLI mirrors the four API methods one-for-one:
58
+
59
+ ```sh
60
+ freshjots list # prints "<filename>\t<title>" per row
61
+ freshjots show cron-jobs-prod # prints the note's plain_body
62
+ freshjots create "Research 2026 Q2" # body comes from stdin or --body
63
+ freshjots append cron-jobs-prod "ok" # text may also be piped on stdin
64
+ ```
65
+
66
+ Both `create` and `append` read from stdin when the body or text isn't
67
+ passed as an argument, so the usual pipe patterns work:
68
+
69
+ ```sh
70
+ backup.sh && echo "backup ok $(date -Iseconds)" | freshjots append cron-jobs-prod
71
+ git log -1 --pretty=format:"%h %s" | freshjots append deploys
72
+ ```
73
+
74
+ The same patterns work in PowerShell:
75
+
76
+ ```powershell
77
+ "backup ok $(Get-Date -Format o)" | freshjots append cron-jobs-prod
78
+ freshjots create "Deploy log" --body "Initial entry."
79
+ ```
80
+
81
+ Exit codes: `0` on success, `1` on runtime errors (missing token,
82
+ network failure, non-2xx API response — printed as `Error: HTTP <status>
83
+ <code>: <message>`), `2` on usage errors.
84
+
44
85
  ## TypeScript
45
86
 
46
87
  Types ship with the package — no `@types/freshjots` needed, no `.d.ts`
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "../cli.js";
3
+
4
+ run(process.argv.slice(2)).then((code) => process.exit(code));
package/cli.js ADDED
@@ -0,0 +1,149 @@
1
+ // CLI dispatcher for the `freshjots` command. The shebang entry
2
+ // (bin/freshjots.js) is a one-line wrapper around run() so this module
3
+ // stays import-safe and testable: parseArgs is pure, run takes its I/O
4
+ // surface as deps so tests can stub stdin/stdout/stderr and the Client.
5
+
6
+ import { Client, ApiError, VERSION } from "./index.js";
7
+
8
+ const USAGE = `freshjots — Fresh Jots CLI
9
+
10
+ Usage:
11
+ freshjots list
12
+ freshjots show <filename>
13
+ freshjots create <title> [--body <text>]
14
+ freshjots append <filename> [<text>]
15
+ freshjots --help | --version
16
+
17
+ Notes:
18
+ - <text> for append and --body for create may also be piped on stdin.
19
+ - Auth: set FRESHJOTS_TOKEN. Mint one at
20
+ https://freshjots.com/settings/api_tokens.
21
+ `;
22
+
23
+ export function parseArgs(argv) {
24
+ if (argv.length === 0) return { command: "help", exitCode: 2 };
25
+ const [first, ...rest] = argv;
26
+
27
+ if (first === "-h" || first === "--help" || first === "help") {
28
+ return { command: "help", exitCode: 0 };
29
+ }
30
+ if (first === "-v" || first === "--version" || first === "version") {
31
+ return { command: "version" };
32
+ }
33
+ if (first === "list") {
34
+ if (rest.length) return { command: "error", message: "list takes no arguments" };
35
+ return { command: "list" };
36
+ }
37
+ if (first === "show") {
38
+ if (rest.length !== 1) return { command: "error", message: "show requires exactly one <filename>" };
39
+ return { command: "show", filename: rest[0] };
40
+ }
41
+ if (first === "create") {
42
+ let body;
43
+ const positional = [];
44
+ for (let i = 0; i < rest.length; i++) {
45
+ const a = rest[i];
46
+ if (a === "--body" || a === "-b") {
47
+ if (i + 1 >= rest.length) return { command: "error", message: "--body requires a value" };
48
+ body = rest[++i];
49
+ } else if (a.startsWith("--body=")) {
50
+ body = a.slice("--body=".length);
51
+ } else {
52
+ positional.push(a);
53
+ }
54
+ }
55
+ if (positional.length !== 1) return { command: "error", message: "create requires exactly one <title>" };
56
+ return { command: "create", title: positional[0], body };
57
+ }
58
+ if (first === "append") {
59
+ if (rest.length < 1 || rest.length > 2) {
60
+ return { command: "error", message: "append requires <filename> and optional <text>" };
61
+ }
62
+ return { command: "append", filename: rest[0], text: rest[1] };
63
+ }
64
+ return { command: "error", message: `unknown command: ${first}` };
65
+ }
66
+
67
+ async function readStdin(stdin) {
68
+ if (!stdin || stdin.isTTY) return "";
69
+ let buf = "";
70
+ for await (const chunk of stdin) {
71
+ buf += typeof chunk === "string" ? chunk : chunk.toString("utf8");
72
+ }
73
+ return buf;
74
+ }
75
+
76
+ export async function run(argv, deps = {}) {
77
+ const env = deps.env ?? process.env;
78
+ const stdout = deps.stdout ?? ((s) => process.stdout.write(s));
79
+ const stderr = deps.stderr ?? ((s) => process.stderr.write(s));
80
+ const stdin = deps.stdin ?? process.stdin;
81
+ const clientFactory = deps.clientFactory ?? ((token) => new Client({ token }));
82
+
83
+ const parsed = parseArgs(argv);
84
+
85
+ if (parsed.command === "help") {
86
+ (parsed.exitCode ? stderr : stdout)(USAGE);
87
+ return parsed.exitCode ?? 0;
88
+ }
89
+ if (parsed.command === "version") {
90
+ stdout(`freshjots ${VERSION}\n`);
91
+ return 0;
92
+ }
93
+ if (parsed.command === "error") {
94
+ stderr(`Error: ${parsed.message}\n\n${USAGE}`);
95
+ return 2;
96
+ }
97
+
98
+ const token = env.FRESHJOTS_TOKEN;
99
+ if (!token) {
100
+ stderr("Error: FRESHJOTS_TOKEN is not set. Mint one at https://freshjots.com/settings/api_tokens\n");
101
+ return 1;
102
+ }
103
+
104
+ let client;
105
+ try {
106
+ client = clientFactory(token);
107
+ } catch (e) {
108
+ stderr(`Error: ${e.message}\n`);
109
+ return 1;
110
+ }
111
+
112
+ try {
113
+ if (parsed.command === "list") {
114
+ const notes = await client.notes();
115
+ for (const n of notes) stdout(`${n.filename}\t${n.title}\n`);
116
+ return 0;
117
+ }
118
+ if (parsed.command === "show") {
119
+ const note = await client.note(parsed.filename);
120
+ stdout(note.plain_body ?? "");
121
+ return 0;
122
+ }
123
+ if (parsed.command === "create") {
124
+ let body = parsed.body;
125
+ if (body === undefined) body = await readStdin(stdin);
126
+ const created = await client.create({ title: parsed.title, body });
127
+ stdout(`${created.filename}\n`);
128
+ return 0;
129
+ }
130
+ if (parsed.command === "append") {
131
+ let text = parsed.text;
132
+ if (text === undefined) text = await readStdin(stdin);
133
+ if (!text) {
134
+ stderr("Error: append requires text (as an argument or on stdin)\n");
135
+ return 2;
136
+ }
137
+ await client.append(parsed.filename, text);
138
+ return 0;
139
+ }
140
+ } catch (e) {
141
+ if (e instanceof ApiError) {
142
+ stderr(`Error: HTTP ${e.status} ${e.code}: ${e.message}\n`);
143
+ } else {
144
+ stderr(`Error: ${e.message}\n`);
145
+ }
146
+ return 1;
147
+ }
148
+ return 0;
149
+ }
package/index.js CHANGED
@@ -18,7 +18,7 @@
18
18
  // payload ({ "notes": [...] }). show / show-by-filename / create return
19
19
  // the note object at the TOP LEVEL — there is no { "note": ... } wrapper.
20
20
 
21
- export const VERSION = "0.2.1";
21
+ export const VERSION = "0.3.0";
22
22
  const DEFAULT_BASE_URL = "https://freshjots.com/api/v1";
23
23
 
24
24
  export class ApiError extends Error {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freshjots",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Tiny JavaScript client for the Fresh Jots API. Append-only notebooks for cron jobs, deploy scripts, and bots.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -11,9 +11,14 @@
11
11
  "default": "./index.js"
12
12
  }
13
13
  },
14
+ "bin": {
15
+ "freshjots": "./bin/freshjots.js"
16
+ },
14
17
  "files": [
15
18
  "index.js",
16
19
  "index.d.ts",
20
+ "cli.js",
21
+ "bin/",
17
22
  "README.md",
18
23
  "LICENSE"
19
24
  ],