memopaper 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 ADDED
@@ -0,0 +1,88 @@
1
+ # memopaper
2
+
3
+ CLI client for [memopaper](https://memopaper.dev) — store and retrieve memos from your terminal.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g memopaper
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ ```bash
14
+ memopaper init
15
+ ```
16
+
17
+ Enter your API key when prompted. Get one at [memopaper.dev](https://memopaper.dev) under **Dashboard → API Keys**.
18
+
19
+ Config is saved to `~/.memopaper/config.json`:
20
+
21
+ ```json
22
+ {
23
+ "api_key": "mp_..."
24
+ }
25
+ ```
26
+
27
+ ## Commands
28
+
29
+ | Command | Description |
30
+ |---------|-------------|
31
+ | `init` | Configure API key |
32
+ | `add` | Save a memo |
33
+ | `list` | List memos |
34
+ | `get <uuid\|index>` | Get a memo by UUID or index (1=latest) |
35
+ | `latest` | Get the latest memo |
36
+ | `oldest` | Get the oldest memo |
37
+ | `pop` | Get and delete the latest memo |
38
+ | `delete <uuid\|index>` | Delete a memo |
39
+ | `edit <uuid\|index> <text>` | Edit a memo's text |
40
+ | `copy` | Copy the latest memo to clipboard |
41
+ | `stats` | Show memo count by group |
42
+
43
+ ### `add`
44
+
45
+ ```bash
46
+ memopaper add "text" # save to inbox
47
+ memopaper add -g work "text" # save to a group
48
+ memopaper add --public "text" # save as public
49
+ memopaper add --file path/to/file.txt # save file contents
50
+ echo "text" | memopaper add # pipe from stdin
51
+ ```
52
+
53
+ ### `list`
54
+
55
+ ```bash
56
+ memopaper list # list inbox
57
+ memopaper list -g work # list a group
58
+ memopaper list -n 50 # show up to 50 memos
59
+ ```
60
+
61
+ ### `get`
62
+
63
+ ```bash
64
+ memopaper get 1 # latest memo
65
+ memopaper get 2 -g work # 2nd latest in @work
66
+ memopaper get <uuid> # by UUID
67
+ ```
68
+
69
+ ### `pop`
70
+
71
+ ```bash
72
+ memopaper pop # read + delete latest from inbox
73
+ memopaper pop -g work # read + delete latest from @work
74
+ ```
75
+
76
+ ### `copy`
77
+
78
+ ```bash
79
+ memopaper copy # copy latest memo to clipboard
80
+ memopaper copy -g work # copy latest from @work
81
+ ```
82
+
83
+ Supports macOS (`pbcopy`), Linux (`xclip`), and Windows (PowerShell).
84
+
85
+ ## Related
86
+
87
+ - [memopaper.dev](https://memopaper.dev) — web dashboard
88
+ - [memopaper-mcp](https://www.npmjs.com/package/memopaper-mcp) — MCP server for AI agents
package/dist/client.js ADDED
@@ -0,0 +1,95 @@
1
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2
+ function assertMemo(value) {
3
+ if (typeof value !== "object" || value === null ||
4
+ typeof value.id !== "string" ||
5
+ !UUID_REGEX.test(value.id) ||
6
+ typeof value.text !== "string" ||
7
+ typeof value.group !== "string") {
8
+ throw new Error("Unexpected response format from API");
9
+ }
10
+ return value;
11
+ }
12
+ export class MemoApiClient {
13
+ baseUrl;
14
+ apiKey;
15
+ constructor(baseUrl, apiKey) {
16
+ this.baseUrl = baseUrl;
17
+ this.apiKey = apiKey;
18
+ let parsed;
19
+ try {
20
+ parsed = new URL(baseUrl);
21
+ }
22
+ catch {
23
+ throw new Error(`Invalid api_url: "${baseUrl}"`);
24
+ }
25
+ if (parsed.protocol !== "https:" && parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1") {
26
+ process.stderr.write("WARNING: api_url is not HTTPS. API key will be sent in plaintext.\n");
27
+ }
28
+ }
29
+ async request(path, options) {
30
+ const res = await fetch(`${this.baseUrl}/api${path}`, {
31
+ ...options,
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ Authorization: `Bearer ${this.apiKey}`,
35
+ ...options?.headers,
36
+ },
37
+ });
38
+ if (res.status === 204)
39
+ return {};
40
+ const body = await res.json();
41
+ if (!res.ok) {
42
+ const raw = body?.error?.message ?? `API error: ${res.status}`;
43
+ const message = raw.length > 200 ? raw.slice(0, 200) + "…" : raw;
44
+ throw Object.assign(new Error(message), { code: body?.error?.code });
45
+ }
46
+ return body;
47
+ }
48
+ async create(data) {
49
+ const res = await this.request("/memos", {
50
+ method: "POST",
51
+ body: JSON.stringify(data),
52
+ });
53
+ return assertMemo(res);
54
+ }
55
+ async list(params = {}) {
56
+ const qs = new URLSearchParams();
57
+ if (params.group)
58
+ qs.set("group", params.group);
59
+ if (params.visibility)
60
+ qs.set("visibility", params.visibility);
61
+ if (params.limit)
62
+ qs.set("limit", String(params.limit));
63
+ if (params.offset)
64
+ qs.set("offset", String(params.offset));
65
+ if (params.order)
66
+ qs.set("order", params.order);
67
+ return this.request(`/memos?${qs}`);
68
+ }
69
+ async first(group) {
70
+ const result = await this.list({ group, limit: 1, order: "asc" });
71
+ return result.data[0] ?? null;
72
+ }
73
+ async get(uuid) {
74
+ const res = await this.request(`/memos/${encodeURIComponent(uuid)}`);
75
+ return assertMemo(res);
76
+ }
77
+ async pop(group) {
78
+ const qs = group ? `?group=${encodeURIComponent(group)}` : "";
79
+ const res = await this.request(`/memos/pop${qs}`, { method: "DELETE" });
80
+ return assertMemo(res);
81
+ }
82
+ async delete(uuid) {
83
+ await this.request(`/memos/${uuid}`, { method: "DELETE" });
84
+ }
85
+ async edit(uuid, text) {
86
+ const res = await this.request(`/memos/${encodeURIComponent(uuid)}`, {
87
+ method: "PUT",
88
+ body: JSON.stringify({ text }),
89
+ });
90
+ return assertMemo(res);
91
+ }
92
+ async stats() {
93
+ return this.request("/memos/stats");
94
+ }
95
+ }
@@ -0,0 +1,110 @@
1
+ import { fstatSync } from "fs";
2
+ import { readConfig } from "../config.js";
3
+ import { readFromFile, readFromStdin, validateInput, safeTruncate, promptConfirm } from "../input.js";
4
+ import { MemoApiClient } from "../client.js";
5
+ function isStdinInteractive() {
6
+ try {
7
+ return fstatSync(0).isCharacterDevice();
8
+ }
9
+ catch {
10
+ return process.stdin.isTTY ?? false;
11
+ }
12
+ }
13
+ export async function buildAddText({ text, file, stdinText, maxBytes }) {
14
+ if (file && text)
15
+ throw Object.assign(new Error("--file and text argument cannot be used together"), { code: "CONFLICTING_OPTIONS" });
16
+ if (file && stdinText)
17
+ throw Object.assign(new Error("stdin and --file cannot be used together"), { code: "CONFLICTING_OPTIONS" });
18
+ let content;
19
+ if (file) {
20
+ content = await readFromFile(file);
21
+ }
22
+ else if (stdinText !== null) {
23
+ content = stdinText;
24
+ }
25
+ else {
26
+ content = text ?? "";
27
+ }
28
+ const validation = validateInput(content, maxBytes);
29
+ if (validation.ok)
30
+ return validation.text;
31
+ const { bytes } = validation;
32
+ const confirmed = await promptConfirm(`Input is ${bytes} bytes (max ${maxBytes}). Truncate and save? [y/N] `);
33
+ if (!confirmed)
34
+ return null;
35
+ return safeTruncate(content, maxBytes);
36
+ }
37
+ export function registerAddCommand(program) {
38
+ program
39
+ .command("add [text]")
40
+ .description("Add a memo")
41
+ .option("--file <path>", "read content from file")
42
+ .option("-g, --group <group>", "group (default: inbox)")
43
+ .option("--public", "save as public memo")
44
+ .action(async (text, options) => {
45
+ const config = readConfig();
46
+ // stdin detection: skip stdin if --file or text arg is given, or if TTY
47
+ const stdinText = (options.file || text || isStdinInteractive()) ? null : await readFromStdin();
48
+ let content;
49
+ try {
50
+ content = await buildAddText({
51
+ text,
52
+ file: options.file,
53
+ stdinText,
54
+ maxBytes: config.max_bytes,
55
+ });
56
+ }
57
+ catch (err) {
58
+ const message = err instanceof Error ? err.message : String(err);
59
+ const code = err.code;
60
+ console.error(message);
61
+ if (code === "NOT_FOUND") {
62
+ process.exitCode = 3;
63
+ return;
64
+ }
65
+ if (code === "CONFLICTING_OPTIONS") {
66
+ process.exitCode = 4;
67
+ return;
68
+ }
69
+ process.exitCode = 1;
70
+ return;
71
+ }
72
+ if (content === null) {
73
+ process.exitCode = 0;
74
+ return;
75
+ }
76
+ if (!content.trim()) {
77
+ console.error("Memo content is empty");
78
+ process.exitCode = 4;
79
+ return;
80
+ }
81
+ if (!config.api_key) {
82
+ console.error("API key not configured. Run 'memopaper init'.");
83
+ process.exitCode = 1;
84
+ return;
85
+ }
86
+ const client = new MemoApiClient(config.api_url, config.api_key);
87
+ try {
88
+ const memo = await client.create({
89
+ text: content,
90
+ group: options.group,
91
+ visibility: options.public ? "public" : "private",
92
+ });
93
+ console.log(`✓ saved [@${memo.group}] ${memo.id}`);
94
+ }
95
+ catch (err) {
96
+ const message = err instanceof Error ? err.message : String(err);
97
+ const code = err.code;
98
+ console.error(`Failed to save: ${message}`);
99
+ if (code === "UNAUTHORIZED") {
100
+ process.exitCode = 2;
101
+ return;
102
+ }
103
+ if (code === "MEMO_TOO_LONG" || code === "INVALID_GROUP" || code === "INVALID_VISIBILITY") {
104
+ process.exitCode = 4;
105
+ return;
106
+ }
107
+ process.exitCode = 1;
108
+ }
109
+ });
110
+ }
@@ -0,0 +1,64 @@
1
+ import { spawn } from "child_process";
2
+ import { readConfig } from "../config.js";
3
+ import { MemoApiClient } from "../client.js";
4
+ function copyToClipboard(text) {
5
+ return new Promise((resolve, reject) => {
6
+ let cmd;
7
+ let args;
8
+ if (process.platform === "win32") {
9
+ cmd = "powershell";
10
+ args = ["-noprofile", "-command", "[Console]::InputEncoding = [Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())"];
11
+ }
12
+ else if (process.platform === "darwin") {
13
+ cmd = "pbcopy";
14
+ args = [];
15
+ }
16
+ else {
17
+ cmd = "xclip";
18
+ args = ["-selection", "clipboard"];
19
+ }
20
+ const proc = spawn(cmd, args, { stdio: ["pipe", "inherit", "inherit"] });
21
+ proc.on("error", (err) => {
22
+ reject(new Error(`Clipboard command failed: ${err.message}`));
23
+ });
24
+ proc.on("close", (code) => {
25
+ if (code === 0)
26
+ resolve();
27
+ else
28
+ reject(new Error(`Clipboard copy failed (exit ${code})`));
29
+ });
30
+ proc.stdin.write(text, "utf8");
31
+ proc.stdin.end();
32
+ });
33
+ }
34
+ export function registerCopyCommand(program) {
35
+ program
36
+ .command("copy")
37
+ .description("Copy the latest memo to clipboard")
38
+ .option("-g, --group <group>", "group filter")
39
+ .action(async (options) => {
40
+ const config = readConfig();
41
+ if (!config.api_key) {
42
+ console.error("API key not configured. Run 'memopaper init'.");
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ const client = new MemoApiClient(config.api_url, config.api_key);
47
+ try {
48
+ const result = await client.list({ limit: 1, group: options.group });
49
+ if (result.data.length === 0) {
50
+ console.error(options.group ? `No memos in @${options.group}` : "No memos");
51
+ process.exitCode = 1;
52
+ return;
53
+ }
54
+ const memo = result.data[0];
55
+ await copyToClipboard(memo.text);
56
+ console.log(`✓ copied to clipboard [@${memo.group}] ${memo.id}`);
57
+ }
58
+ catch (err) {
59
+ const message = err instanceof Error ? err.message : String(err);
60
+ console.error(`Failed to copy: ${message}`);
61
+ process.exitCode = 1;
62
+ }
63
+ });
64
+ }
@@ -0,0 +1,33 @@
1
+ import { readConfig } from "../config.js";
2
+ import { MemoApiClient } from "../client.js";
3
+ import { resolveUuid } from "../resolve.js";
4
+ export function registerDeleteCommand(program) {
5
+ program
6
+ .command("delete <uuid-or-index>")
7
+ .description("Delete a memo by UUID or 1-based index (1=latest)")
8
+ .option("-g, --group <group>", "group filter (used with index)")
9
+ .action(async (ref, options) => {
10
+ const config = readConfig();
11
+ if (!config.api_key) {
12
+ console.error("API key not configured. Run 'memopaper init'.");
13
+ process.exitCode = 1;
14
+ return;
15
+ }
16
+ const client = new MemoApiClient(config.api_url, config.api_key);
17
+ try {
18
+ const uuid = await resolveUuid(client, ref, options.group);
19
+ if (!uuid) {
20
+ console.error(`No memo at index ${ref}`);
21
+ process.exitCode = 1;
22
+ return;
23
+ }
24
+ await client.delete(uuid);
25
+ console.log(`✓ deleted (${uuid})`);
26
+ }
27
+ catch (err) {
28
+ const message = err instanceof Error ? err.message : String(err);
29
+ console.error(`Failed to delete: ${message}`);
30
+ process.exitCode = 1;
31
+ }
32
+ });
33
+ }
@@ -0,0 +1,33 @@
1
+ import { readConfig } from "../config.js";
2
+ import { MemoApiClient } from "../client.js";
3
+ import { resolveUuid } from "../resolve.js";
4
+ export function registerEditCommand(program) {
5
+ program
6
+ .command("edit <uuid-or-index> <text>")
7
+ .description("Edit a memo's text by UUID or 1-based index (1=latest)")
8
+ .option("-g, --group <group>", "group filter (used with index)")
9
+ .action(async (ref, text, options) => {
10
+ const config = readConfig();
11
+ if (!config.api_key) {
12
+ console.error("API key not configured. Run 'memopaper init'.");
13
+ process.exitCode = 1;
14
+ return;
15
+ }
16
+ const client = new MemoApiClient(config.api_url, config.api_key);
17
+ try {
18
+ const uuid = await resolveUuid(client, ref, options.group);
19
+ if (!uuid) {
20
+ console.error(`No memo at index ${ref}`);
21
+ process.exitCode = 1;
22
+ return;
23
+ }
24
+ const memo = await client.edit(uuid, text);
25
+ console.log(`✓ updated [@${memo.group}] ${memo.id}`);
26
+ }
27
+ catch (err) {
28
+ const message = err instanceof Error ? err.message : String(err);
29
+ console.error(`Failed to edit: ${message}`);
30
+ process.exitCode = 1;
31
+ }
32
+ });
33
+ }
@@ -0,0 +1,31 @@
1
+ import { readConfig } from "../config.js";
2
+ import { MemoApiClient } from "../client.js";
3
+ export function registerFirstCommand(program) {
4
+ program
5
+ .command("first")
6
+ .description("가장 오래된 메모 조회")
7
+ .option("-g, --group <group>", "그룹 필터")
8
+ .action(async (options) => {
9
+ const config = readConfig();
10
+ if (!config.api_key) {
11
+ console.error("API 키가 설정되지 않았습니다. 'memopaper init'을 실행하세요.");
12
+ process.exitCode = 1;
13
+ return;
14
+ }
15
+ const client = new MemoApiClient(config.api_url, config.api_key);
16
+ try {
17
+ const memo = await client.first(options.group);
18
+ if (!memo) {
19
+ console.error(options.group ? `@${options.group} 그룹에 메모가 없습니다` : "메모가 없습니다");
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+ process.stdout.write(memo.text + "\n");
24
+ }
25
+ catch (err) {
26
+ const message = err instanceof Error ? err.message : String(err);
27
+ console.error(`조회 실패: ${message}`);
28
+ process.exitCode = 1;
29
+ }
30
+ });
31
+ }
@@ -0,0 +1,33 @@
1
+ import { readConfig } from "../config.js";
2
+ import { MemoApiClient } from "../client.js";
3
+ import { resolveUuid } from "../resolve.js";
4
+ export function registerGetCommand(program) {
5
+ program
6
+ .command("get <uuid-or-index>")
7
+ .description("Get a memo by UUID or 1-based index (1=latest)")
8
+ .option("-g, --group <group>", "group filter (used with index)")
9
+ .action(async (ref, options) => {
10
+ const config = readConfig();
11
+ if (!config.api_key) {
12
+ console.error("API key not configured. Run 'memopaper init'.");
13
+ process.exitCode = 1;
14
+ return;
15
+ }
16
+ const client = new MemoApiClient(config.api_url, config.api_key);
17
+ try {
18
+ const uuid = await resolveUuid(client, ref, options.group);
19
+ if (!uuid) {
20
+ console.error(`No memo at index ${ref}`);
21
+ process.exitCode = 1;
22
+ return;
23
+ }
24
+ const memo = await client.get(uuid);
25
+ process.stdout.write(memo.text + "\n");
26
+ }
27
+ catch (err) {
28
+ const message = err instanceof Error ? err.message : String(err);
29
+ console.error(`Failed to fetch: ${message}`);
30
+ process.exitCode = 1;
31
+ }
32
+ });
33
+ }
@@ -0,0 +1,38 @@
1
+ import { existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { readConfig, writeConfig, defaultConfig } from "../config.js";
5
+ import { promptInput, promptConfirm } from "../input.js";
6
+ const CONFIG_PATH = join(homedir(), ".memopaper", "config.json");
7
+ export function registerInitCommand(program) {
8
+ program
9
+ .command("init")
10
+ .description("Configure API key")
11
+ .action(async () => {
12
+ if (existsSync(CONFIG_PATH)) {
13
+ const current = readConfig();
14
+ const masked = current.api_key
15
+ ? `mp_${"*".repeat(8)}...${current.api_key.slice(-4)}`
16
+ : "(not set)";
17
+ console.log(`Current API key: ${masked}`);
18
+ const overwrite = await promptConfirm("Overwrite? [y/N] ");
19
+ if (!overwrite) {
20
+ console.log("Aborted.");
21
+ return;
22
+ }
23
+ }
24
+ const apiKey = await promptInput("API key: ");
25
+ if (!apiKey) {
26
+ console.error("API key cannot be empty.");
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+ if (!apiKey.startsWith("mp_")) {
31
+ console.error("Invalid API key format. Expected: mp_...");
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+ writeConfig({ ...defaultConfig, api_key: apiKey });
36
+ console.log(`✓ Config saved (${CONFIG_PATH})`);
37
+ });
38
+ }
@@ -0,0 +1,31 @@
1
+ import { readConfig } from "../config.js";
2
+ import { MemoApiClient } from "../client.js";
3
+ export function registerLatestCommand(program) {
4
+ program
5
+ .command("latest")
6
+ .description("Get the latest memo")
7
+ .option("-g, --group <group>", "group filter")
8
+ .action(async (options) => {
9
+ const config = readConfig();
10
+ if (!config.api_key) {
11
+ console.error("API key not configured. Run 'memopaper init'.");
12
+ process.exitCode = 1;
13
+ return;
14
+ }
15
+ const client = new MemoApiClient(config.api_url, config.api_key);
16
+ try {
17
+ const result = await client.list({ group: options.group, limit: 1 });
18
+ if (!result.data[0]) {
19
+ console.error(options.group ? `No memos in @${options.group}` : "No memos");
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+ process.stdout.write(result.data[0].text + "\n");
24
+ }
25
+ catch (err) {
26
+ const message = err instanceof Error ? err.message : String(err);
27
+ console.error(`Failed to fetch: ${message}`);
28
+ process.exitCode = 1;
29
+ }
30
+ });
31
+ }
@@ -0,0 +1,40 @@
1
+ import { readConfig } from "../config.js";
2
+ import { MemoApiClient } from "../client.js";
3
+ export function registerListCommand(program) {
4
+ program
5
+ .command("list")
6
+ .description("List memos")
7
+ .option("-g, --group <group>", "group filter")
8
+ .option("-n, --limit <number>", "max count (default: 20)", "20")
9
+ .action(async (options) => {
10
+ const config = readConfig();
11
+ if (!config.api_key) {
12
+ console.error("API key not configured. Run 'memopaper init'.");
13
+ process.exitCode = 1;
14
+ return;
15
+ }
16
+ const client = new MemoApiClient(config.api_url, config.api_key);
17
+ try {
18
+ const result = await client.list({
19
+ group: options.group,
20
+ limit: parseInt(options.limit, 10),
21
+ });
22
+ if (result.data.length === 0) {
23
+ console.error(options.group ? `No memos in @${options.group}` : "No memos");
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ for (let i = 0; i < result.data.length; i++) {
28
+ const m = result.data[i];
29
+ const preview = m.text.length > 80 ? m.text.slice(0, 80) + "…" : m.text;
30
+ console.log(`${i + 1}. @${m.group} ${preview}`);
31
+ }
32
+ console.log(`\n${result.data.length} / ${result.total} memos`);
33
+ }
34
+ catch (err) {
35
+ const message = err instanceof Error ? err.message : String(err);
36
+ console.error(`Failed to fetch: ${message}`);
37
+ process.exitCode = 1;
38
+ }
39
+ });
40
+ }
@@ -0,0 +1,31 @@
1
+ import { readConfig } from "../config.js";
2
+ import { MemoApiClient } from "../client.js";
3
+ export function registerOldestCommand(program) {
4
+ program
5
+ .command("oldest")
6
+ .description("Get the oldest memo")
7
+ .option("-g, --group <group>", "group filter")
8
+ .action(async (options) => {
9
+ const config = readConfig();
10
+ if (!config.api_key) {
11
+ console.error("API key not configured. Run 'memopaper init'.");
12
+ process.exitCode = 1;
13
+ return;
14
+ }
15
+ const client = new MemoApiClient(config.api_url, config.api_key);
16
+ try {
17
+ const memo = await client.first(options.group);
18
+ if (!memo) {
19
+ console.error(options.group ? `No memos in @${options.group}` : "No memos");
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+ process.stdout.write(memo.text + "\n");
24
+ }
25
+ catch (err) {
26
+ const message = err instanceof Error ? err.message : String(err);
27
+ console.error(`Failed to fetch: ${message}`);
28
+ process.exitCode = 1;
29
+ }
30
+ });
31
+ }
@@ -0,0 +1,26 @@
1
+ import { readConfig } from "../config.js";
2
+ import { MemoApiClient } from "../client.js";
3
+ export function registerPopCommand(program) {
4
+ program
5
+ .command("pop")
6
+ .description("Get and delete the latest memo")
7
+ .option("-g, --group <group>", "group filter")
8
+ .action(async (options) => {
9
+ const config = readConfig();
10
+ if (!config.api_key) {
11
+ console.error("API key not configured. Run 'memopaper init'.");
12
+ process.exitCode = 1;
13
+ return;
14
+ }
15
+ const client = new MemoApiClient(config.api_url, config.api_key);
16
+ try {
17
+ const memo = await client.pop(options.group);
18
+ process.stdout.write(memo.text + "\n");
19
+ }
20
+ catch (err) {
21
+ const message = err instanceof Error ? err.message : String(err);
22
+ console.error(`Failed to pop: ${message}`);
23
+ process.exitCode = 1;
24
+ }
25
+ });
26
+ }
@@ -0,0 +1,28 @@
1
+ import { readConfig } from "../config.js";
2
+ import { MemoApiClient } from "../client.js";
3
+ export function registerStatsCommand(program) {
4
+ program
5
+ .command("stats")
6
+ .description("Show memo count by group")
7
+ .action(async () => {
8
+ const config = readConfig();
9
+ if (!config.api_key) {
10
+ console.error("API key not configured. Run 'memopaper init'.");
11
+ process.exitCode = 1;
12
+ return;
13
+ }
14
+ const client = new MemoApiClient(config.api_url, config.api_key);
15
+ try {
16
+ const stats = await client.stats();
17
+ console.log(`Total: ${stats.total}`);
18
+ for (const { group, count } of stats.groups) {
19
+ console.log(` @${group}: ${count}`);
20
+ }
21
+ }
22
+ catch (err) {
23
+ const message = err instanceof Error ? err.message : String(err);
24
+ console.error(`Failed to fetch stats: ${message}`);
25
+ process.exitCode = 1;
26
+ }
27
+ });
28
+ }
package/dist/config.js ADDED
@@ -0,0 +1,40 @@
1
+ import { homedir } from "os";
2
+ import { join } from "path";
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
+ const CONFIG_DIR = join(homedir(), ".memopaper");
5
+ const CONFIG_PATH = join(CONFIG_DIR, "config.json");
6
+ export const defaultConfig = {
7
+ api_url: "https://memopaper.dev",
8
+ api_key: "",
9
+ max_bytes: 2048,
10
+ };
11
+ export function readConfig() {
12
+ if (!existsSync(CONFIG_PATH))
13
+ return { ...defaultConfig };
14
+ try {
15
+ const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
16
+ const merged = { ...defaultConfig, ...raw };
17
+ if (typeof merged.api_url !== "string")
18
+ merged.api_url = defaultConfig.api_url;
19
+ if (typeof merged.api_key !== "string")
20
+ merged.api_key = defaultConfig.api_key;
21
+ if (typeof merged.max_bytes !== "number" ||
22
+ !Number.isInteger(merged.max_bytes) ||
23
+ merged.max_bytes <= 0) {
24
+ merged.max_bytes = defaultConfig.max_bytes;
25
+ }
26
+ return merged;
27
+ }
28
+ catch {
29
+ return { ...defaultConfig };
30
+ }
31
+ }
32
+ export function writeConfig(config) {
33
+ try {
34
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
35
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
36
+ }
37
+ catch (error) {
38
+ throw new Error(`Failed to save config: ${error instanceof Error ? error.message : String(error)}`);
39
+ }
40
+ }
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { program } from "commander";
3
+ import { registerAddCommand } from "./commands/add.js";
4
+ import { registerCopyCommand } from "./commands/copy.js";
5
+ import { registerDeleteCommand } from "./commands/delete.js";
6
+ import { registerEditCommand } from "./commands/edit.js";
7
+ import { registerGetCommand } from "./commands/get.js";
8
+ import { registerInitCommand } from "./commands/init.js";
9
+ import { registerLatestCommand } from "./commands/latest.js";
10
+ import { registerListCommand } from "./commands/list.js";
11
+ import { registerOldestCommand } from "./commands/oldest.js";
12
+ import { registerPopCommand } from "./commands/pop.js";
13
+ import { registerStatsCommand } from "./commands/stats.js";
14
+ program.name("memopaper").version("1.0.0");
15
+ registerInitCommand(program);
16
+ registerAddCommand(program);
17
+ registerListCommand(program);
18
+ registerGetCommand(program);
19
+ registerPopCommand(program);
20
+ registerDeleteCommand(program);
21
+ registerEditCommand(program);
22
+ registerLatestCommand(program);
23
+ registerCopyCommand(program);
24
+ registerOldestCommand(program);
25
+ registerStatsCommand(program);
26
+ program.parse();
package/dist/input.js ADDED
@@ -0,0 +1,70 @@
1
+ import { readFileSync, statSync } from "fs";
2
+ const MAX_FILE_SIZE = 1024 * 1024; // 1 MB
3
+ import { createInterface } from "readline";
4
+ export function countBytes(text) {
5
+ return Buffer.byteLength(text, "utf-8");
6
+ }
7
+ export function safeTruncate(text, maxBytes) {
8
+ const buf = Buffer.from(text, "utf-8");
9
+ if (buf.length <= maxBytes)
10
+ return text;
11
+ // Back up while buf[end] is a UTF-8 continuation byte (0x80–0xBF)
12
+ let end = maxBytes;
13
+ while (end > 0 && (buf[end] & 0xc0) === 0x80)
14
+ end--;
15
+ return buf.slice(0, end).toString("utf-8");
16
+ }
17
+ export function validateInput(text, maxBytes) {
18
+ const bytes = countBytes(text);
19
+ if (bytes <= maxBytes)
20
+ return { ok: true, text };
21
+ return { ok: false, bytes, maxBytes };
22
+ }
23
+ export async function readFromFile(filePath) {
24
+ try {
25
+ const stat = statSync(filePath);
26
+ if (stat.size > MAX_FILE_SIZE) {
27
+ throw Object.assign(new Error(`File too large: ${stat.size} bytes (max ${MAX_FILE_SIZE})`), { code: "FILE_TOO_LARGE" });
28
+ }
29
+ return readFileSync(filePath, "utf-8");
30
+ }
31
+ catch (err) {
32
+ const code = err.code;
33
+ if (code === "ENOENT") {
34
+ throw Object.assign(new Error(`File not found: ${filePath}`), { code: "NOT_FOUND" });
35
+ }
36
+ if (code === "FILE_TOO_LARGE")
37
+ throw err;
38
+ const message = err instanceof Error ? err.message : String(err);
39
+ throw Object.assign(new Error(`Failed to read file: ${message}`), { code: "READ_ERROR" });
40
+ }
41
+ }
42
+ export async function readFromStdin() {
43
+ return new Promise((resolve, reject) => {
44
+ const chunks = [];
45
+ const rl = createInterface({ input: process.stdin });
46
+ rl.on("line", (line) => chunks.push(line));
47
+ rl.on("close", () => resolve(chunks.join("\n")));
48
+ rl.on("error", (error) => reject(error));
49
+ });
50
+ }
51
+ export async function promptConfirm(question) {
52
+ return new Promise((resolve, reject) => {
53
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
54
+ rl.on("error", (error) => reject(error));
55
+ rl.question(question, (answer) => {
56
+ rl.close();
57
+ resolve(answer.trim().toLowerCase() === "y");
58
+ });
59
+ });
60
+ }
61
+ export async function promptInput(question) {
62
+ return new Promise((resolve, reject) => {
63
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
64
+ rl.on("error", (error) => reject(error));
65
+ rl.question(question, (answer) => {
66
+ rl.close();
67
+ resolve(answer.trim());
68
+ });
69
+ });
70
+ }
@@ -0,0 +1,19 @@
1
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2
+ /**
3
+ * Resolves a UUID or 1-based index to a UUID.
4
+ * Returns null if the index is out of range.
5
+ * Throws if ref is neither a valid index nor a valid UUID.
6
+ */
7
+ export async function resolveUuid(client, ref, group) {
8
+ if (/^\d+$/.test(ref)) {
9
+ const index = parseInt(ref, 10);
10
+ if (index < 1)
11
+ return null;
12
+ const result = await client.list({ group, limit: 1, offset: index - 1 });
13
+ return result.data[0]?.id ?? null;
14
+ }
15
+ if (!UUID_REGEX.test(ref)) {
16
+ throw new Error(`Invalid memo reference: expected a number or UUID, got "${ref}"`);
17
+ }
18
+ return ref;
19
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "memopaper",
3
+ "version": "1.0.0",
4
+ "description": "CLI-first cross-device memo buffer for developers and agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "memopaper": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/agcraft/memopaper.git",
16
+ "directory": "memopaper-cli"
17
+ },
18
+ "homepage": "https://memopaper.dev",
19
+ "keywords": [
20
+ "memo",
21
+ "cli",
22
+ "terminal",
23
+ "cross-device",
24
+ "mcp",
25
+ "developer-tools"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "dev": "tsx src/index.ts",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest"
35
+ },
36
+ "dependencies": {
37
+ "commander": "^12.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "typescript": "^5.4.0",
41
+ "tsx": "^4.7.0",
42
+ "vitest": "^1.4.0",
43
+ "@types/node": "^20.0.0"
44
+ }
45
+ }