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 +88 -0
- package/dist/client.js +95 -0
- package/dist/commands/add.js +110 -0
- package/dist/commands/copy.js +64 -0
- package/dist/commands/delete.js +33 -0
- package/dist/commands/edit.js +33 -0
- package/dist/commands/first.js +31 -0
- package/dist/commands/get.js +33 -0
- package/dist/commands/init.js +38 -0
- package/dist/commands/latest.js +31 -0
- package/dist/commands/list.js +40 -0
- package/dist/commands/oldest.js +31 -0
- package/dist/commands/pop.js +26 -0
- package/dist/commands/stats.js +28 -0
- package/dist/config.js +40 -0
- package/dist/index.js +26 -0
- package/dist/input.js +70 -0
- package/dist/resolve.js +19 -0
- package/package.json +45 -0
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
|
+
}
|
package/dist/resolve.js
ADDED
|
@@ -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
|
+
}
|