freshjots 0.2.0 → 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
@@ -30,12 +30,57 @@ console.log(note.plain_body);
30
30
  const notes = await client.notes();
31
31
  for (const n of notes) console.log(`${n.filename}\t${n.title}`);
32
32
 
33
- // Create a new note explicitly (errors if the filename is taken).
34
- await client.create({ filename: "research-2026-q2", body: "Initial outline." });
33
+ // Create a note. The API derives the filename from the title — for a
34
+ // note addressable by an exact filename, use append() instead.
35
+ const created = await client.create({ title: "Research 2026 Q2", body: "Initial outline." });
36
+ console.log(created.filename); // server-derived stream name
35
37
  ```
36
38
 
37
39
  The whole API is four methods: `notes()`, `note(filename)`,
38
- `create({ filename, body, title })`, `append(filename, text)`.
40
+ `create({ title, body })`, `append(filename, text)`. `note()` and
41
+ `create()` return the note object directly (no `{ note: … }` wrapper);
42
+ `notes()` returns the array.
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.
39
84
 
40
85
  ## TypeScript
41
86
 
@@ -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.d.ts CHANGED
@@ -61,10 +61,14 @@ declare module "freshjots" {
61
61
  baseUrl?: string;
62
62
  }
63
63
 
64
+ /**
65
+ * Input for `create()`. The API derives the note's filename from the
66
+ * title (it does not accept a client-supplied filename). For a note
67
+ * addressable by an exact, caller-chosen filename, use `append()`.
68
+ */
64
69
  export interface CreateInput {
65
- filename: string;
70
+ title: string;
66
71
  body?: string;
67
- title?: string;
68
72
  }
69
73
 
70
74
  export class Client {
package/index.js CHANGED
@@ -7,12 +7,18 @@
7
7
  // await client.append("cron-jobs-prod", "backup ok");
8
8
  // const note = await client.note("cron-jobs-prod");
9
9
  // console.log(note.plain_body);
10
+ // const created = await client.create({ title: "Deploy log" });
11
+ // console.log(created.filename); // server-derived from the title
10
12
  //
11
13
  // Requires Node 18+ (uses global fetch). All methods throw ApiError on
12
14
  // non-2xx responses, with the code/status/details from the API's stable
13
15
  // error envelope.
16
+ //
17
+ // Note on response shapes: GET /notes is the only endpoint that wraps its
18
+ // payload ({ "notes": [...] }). show / show-by-filename / create return
19
+ // the note object at the TOP LEVEL — there is no { "note": ... } wrapper.
14
20
 
15
- export const VERSION = "0.1.0";
21
+ export const VERSION = "0.3.0";
16
22
  const DEFAULT_BASE_URL = "https://freshjots.com/api/v1";
17
23
 
18
24
  export class ApiError extends Error {
@@ -39,14 +45,27 @@ export class Client {
39
45
  }
40
46
 
41
47
  async note(filename) {
48
+ // show-by-filename renders the serializer at the top level (no
49
+ // { note: ... } wrapper), so return the response as-is.
42
50
  const path = `/notes/by-filename/${encodeURIComponent(filename)}`;
43
- return (await this._request("GET", path)).note;
51
+ return await this._request("GET", path);
44
52
  }
45
53
 
46
- async create({ filename, body = "", title }) {
47
- const note = { filename, plain_body: body, format: "plain" };
48
- if (title) note.title = title;
49
- return (await this._request("POST", "/notes", { note })).note;
54
+ // Create a note. The API permits note[title, plain_body, format, ...]
55
+ // NOT filename: the server DERIVES the filename from the title. For
56
+ // a note addressable by an exact, caller-chosen filename, use append()
57
+ // (the by-filename endpoint creates it with that exact name on first
58
+ // call). Returns the created note (top level); read `.filename` for
59
+ // the server-derived stream name.
60
+ async create({ title, body = "" }) {
61
+ if (!title) {
62
+ throw new Error(
63
+ "create requires a title — the API derives the filename from it. " +
64
+ "For a note addressable by an exact filename, use append().",
65
+ );
66
+ }
67
+ const note = { title, plain_body: body, format: "plain" };
68
+ return await this._request("POST", "/notes", { note });
50
69
  }
51
70
 
52
71
  async append(filename, text) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freshjots",
3
- "version": "0.2.0",
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
  ],