saveinme 1.4.0 → 2.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 +64 -6
- package/bin/saveinme.js +99 -24
- package/package.json +2 -1
- package/src/ai.js +447 -0
- package/src/display.js +48 -10
- package/src/editor.js +150 -3
- package/src/store.js +16 -8
- package/src/sync.js +165 -0
- package/src/tui.js +336 -0
package/README.md
CHANGED
|
@@ -25,16 +25,26 @@ npm link
|
|
|
25
25
|
| Command | What it does |
|
|
26
26
|
|---|---|
|
|
27
27
|
| `saveinme` or `sim` | List all your saved notes |
|
|
28
|
+
| `saveinme <title>` | Directly view a note by its title |
|
|
29
|
+
| `saveinme -ui` | Launch the Split-Pane TUI Dashboard (keyboard-driven screen) |
|
|
28
30
|
| `saveinme -c` | Create a new note or edit an existing one |
|
|
29
31
|
| `saveinme -c <title>` | Jump straight to editing/creating a note by title |
|
|
30
32
|
| `saveinme -c <title> -n <notebook>` | Create/edit a note inside a specific notebook |
|
|
31
33
|
| `saveinme -cl` | Instantly save system clipboard content as a note |
|
|
32
34
|
| `saveinme -cl <title>` | Save clipboard content directly with a specific title |
|
|
33
|
-
| `saveinme -
|
|
34
|
-
| `saveinme -
|
|
35
|
-
| `saveinme -
|
|
35
|
+
| `saveinme -d` | Set/select the default target notebook for new notes (opens interactive list) |
|
|
36
|
+
| `saveinme -d <notebook>` | Set the default target notebook directly |
|
|
37
|
+
| `saveinme -v` | View a note (opens interactive select menu) |
|
|
38
|
+
| `saveinme -v <title>` | View a specific note in full |
|
|
39
|
+
| `saveinme -rm` | Remove notes (opens interactive multi-select checkbox menu) |
|
|
40
|
+
| `saveinme -rm <title>` | Delete a specific note |
|
|
36
41
|
| `saveinme -p <title>` | Toggle pin (pinned notes appear at the top) |
|
|
37
42
|
| `saveinme -f <term>` | Search by title, content, tags, or category |
|
|
43
|
+
| `saveinme -ai` | Start an interactive AI chat session with Gemini |
|
|
44
|
+
| `saveinme -ai <query>` | Query notes database via Gemini AI |
|
|
45
|
+
| `saveinme -ai --setup` | Configure Gemini API key |
|
|
46
|
+
| `saveinme --sync` | Sync notes database push/pull to remote repository |
|
|
47
|
+
| `saveinme --sync --setup` | Configure Git remote sync URL |
|
|
38
48
|
| `saveinme --path` | Show where your notes are stored |
|
|
39
49
|
| `saveinme -h` | Help |
|
|
40
50
|
|
|
@@ -43,11 +53,11 @@ npm link
|
|
|
43
53
|
### Default Notebook Target
|
|
44
54
|
You can lock the default notebook (category) where your notes go:
|
|
45
55
|
```bash
|
|
46
|
-
sim -
|
|
56
|
+
sim -d "Work"
|
|
47
57
|
```
|
|
48
58
|
Once set, any new note created via `sim -c` or `sim -cl` will default to the `"Work"` notebook automatically.
|
|
49
|
-
* View current target: `sim -
|
|
50
|
-
* Clear default target: `sim -
|
|
59
|
+
* View current target / Select from list: `sim -d`
|
|
60
|
+
* Clear default target: `sim -d --clear`
|
|
51
61
|
|
|
52
62
|
### Override Notebook
|
|
53
63
|
Send a note to a specific notebook at any time using the `-n` or `--notebook` flag at the end of your command:
|
|
@@ -94,6 +104,54 @@ Type `:keep` (when editing) to keep the existing content unchanged.
|
|
|
94
104
|
|
|
95
105
|
Works on **Windows** (PowerShell / CMD) and **Linux/macOS** — no external editor opens.
|
|
96
106
|
|
|
107
|
+
## Split-Pane TUI Dashboard (`sim -ui`)
|
|
108
|
+
|
|
109
|
+
Open a visual workspace right inside your terminal:
|
|
110
|
+
```bash
|
|
111
|
+
sim -ui
|
|
112
|
+
```
|
|
113
|
+
- **Navigation**: Use Up/Down arrows (or `j`/`k` Vim keys) to browse notes.
|
|
114
|
+
- **Preview**: Note content and details update in real-time on the right.
|
|
115
|
+
- **Controls**:
|
|
116
|
+
- `Enter` — Edit the selected note.
|
|
117
|
+
- `c` — Create a new note.
|
|
118
|
+
- `p` — Pin/unpin the selected note.
|
|
119
|
+
- `d` — Delete the selected note (with confirmation).
|
|
120
|
+
- `/` — Enter search mode (type search terms, `Enter` to filter, `Esc` to clear).
|
|
121
|
+
- `q` or `Esc` — Quit dashboard.
|
|
122
|
+
|
|
123
|
+
## AI Copilot (Gemini / Ollama) (`sim -ai`)
|
|
124
|
+
|
|
125
|
+
Query your notes using Google's Gemini AI (cloud-based) or Ollama (completely local and offline):
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Initial Setup / Reset (prompts you to choose between Gemini or local Ollama)
|
|
129
|
+
sim -ai --setup
|
|
130
|
+
sim -ai --reset
|
|
131
|
+
|
|
132
|
+
# Ask questions directly based on your saved notes
|
|
133
|
+
sim -ai "which server port was mentioned in the api note?"
|
|
134
|
+
sim -ai "summarize the highlights of the book list note"
|
|
135
|
+
|
|
136
|
+
# Start an interactive chat session
|
|
137
|
+
sim -ai
|
|
138
|
+
```
|
|
139
|
+
- **Gemini**: Setup prompts for a Google AI Studio API key (free tier available).
|
|
140
|
+
- **Ollama**: Setup prompts for your local server endpoint (default: `http://localhost:11434`) and local model name (default: `llama3`). Ensure you have downloaded the model first (e.g. run `ollama run llama3` in your terminal).
|
|
141
|
+
|
|
142
|
+
## Git Auto-Sync (`sim --sync`)
|
|
143
|
+
|
|
144
|
+
Sync your notes across multiple devices using a private Git repository:
|
|
145
|
+
```bash
|
|
146
|
+
# Setup remote URL (SSH preferred)
|
|
147
|
+
sim --sync --setup
|
|
148
|
+
|
|
149
|
+
# Manually trigger a full push/pull sync
|
|
150
|
+
sim --sync
|
|
151
|
+
```
|
|
152
|
+
Once configured, `saveinme` will **automatically run background syncs** when you save or delete notes, keeping everything synced silently!
|
|
153
|
+
*Note: Config files (such as those storing secret API keys) are automatically ignored by Git to protect your security.*
|
|
154
|
+
|
|
97
155
|
## Storage
|
|
98
156
|
|
|
99
157
|
Notes are stored permanently at:
|
package/bin/saveinme.js
CHANGED
|
@@ -19,7 +19,10 @@ import {
|
|
|
19
19
|
showHelp,
|
|
20
20
|
} from '../src/display.js';
|
|
21
21
|
|
|
22
|
-
import { openEditor, saveFromClipboard, savePipedInput } from '../src/editor.js';
|
|
22
|
+
import { openEditor, saveFromClipboard, savePipedInput, interactiveGroupDelete, interactiveSetDefaultNotebook, interactiveViewNote } from '../src/editor.js';
|
|
23
|
+
import { syncNotes, triggerAutoSync } from '../src/sync.js';
|
|
24
|
+
import { queryAI, chatAI, setupApiKey } from '../src/ai.js';
|
|
25
|
+
import { launchTUI } from '../src/tui.js';
|
|
23
26
|
|
|
24
27
|
// ─── Parse args ───────────────────────────────────────────────────────────────
|
|
25
28
|
|
|
@@ -95,7 +98,9 @@ async function main() {
|
|
|
95
98
|
break;
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
// ──
|
|
101
|
+
// ── Default target notebook ────────────────────────────────────────────
|
|
102
|
+
case '-d':
|
|
103
|
+
case '--default':
|
|
99
104
|
case '-t':
|
|
100
105
|
case '--target': {
|
|
101
106
|
if (rest) {
|
|
@@ -107,12 +112,7 @@ async function main() {
|
|
|
107
112
|
showSuccess(`Target notebook set to "${rest}".\n New notes will default to this notebook.`);
|
|
108
113
|
}
|
|
109
114
|
} else {
|
|
110
|
-
|
|
111
|
-
if (current) {
|
|
112
|
-
showInfo(`Current default notebook is "${current}".\n Change it with: sim -t <notebook>\n Clear it with: sim -t --clear`);
|
|
113
|
-
} else {
|
|
114
|
-
showInfo('No default notebook set. New notes will be uncategorized.\n Set one with: sim -t <notebook>');
|
|
115
|
-
}
|
|
115
|
+
await interactiveSetDefaultNotebook();
|
|
116
116
|
}
|
|
117
117
|
break;
|
|
118
118
|
}
|
|
@@ -120,24 +120,57 @@ async function main() {
|
|
|
120
120
|
// ── View ───────────────────────────────────────────────────────────────
|
|
121
121
|
case '-v':
|
|
122
122
|
case '--view': {
|
|
123
|
-
if (!rest) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
if (!rest) {
|
|
124
|
+
await interactiveViewNote();
|
|
125
|
+
} else {
|
|
126
|
+
const note = getNoteByTitle(rest);
|
|
127
|
+
if (!note) showError(`No note found with title "${rest}".`);
|
|
128
|
+
else showNote(note);
|
|
129
|
+
}
|
|
127
130
|
break;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
|
-
// ── Delete
|
|
131
|
-
case '-
|
|
133
|
+
// ── Remove / Delete ────────────────────────────────────────────────────
|
|
134
|
+
case '-rm':
|
|
135
|
+
case '--remove':
|
|
132
136
|
case '--delete': {
|
|
133
|
-
if (!rest) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
showError(`No note found with title "${rest}".`);
|
|
137
|
+
if (!rest) {
|
|
138
|
+
await interactiveGroupDelete();
|
|
139
|
+
triggerAutoSync();
|
|
137
140
|
} else {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
const titles = argv.slice(1);
|
|
142
|
+
const singleTitle = titles.join(' ').trim();
|
|
143
|
+
const note = getNoteByTitle(singleTitle);
|
|
144
|
+
if (note) {
|
|
145
|
+
const ok = deleteNoteById(note.id);
|
|
146
|
+
if (ok) {
|
|
147
|
+
showSuccess(`Note "${note.title}" deleted.`);
|
|
148
|
+
triggerAutoSync();
|
|
149
|
+
} else {
|
|
150
|
+
showError(`Delete failed for "${note.title}".`);
|
|
151
|
+
}
|
|
152
|
+
} else if (titles.length > 1) {
|
|
153
|
+
let deletedAny = false;
|
|
154
|
+
for (const t of titles) {
|
|
155
|
+
const n = getNoteByTitle(t);
|
|
156
|
+
if (n) {
|
|
157
|
+
const ok = deleteNoteById(n.id);
|
|
158
|
+
if (ok) {
|
|
159
|
+
showSuccess(`Note "${n.title}" deleted.`);
|
|
160
|
+
deletedAny = true;
|
|
161
|
+
} else {
|
|
162
|
+
showError(`Delete failed for "${n.title}".`);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
showError(`No note found with title "${t}".`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (deletedAny) {
|
|
169
|
+
triggerAutoSync();
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
showError(`No note found with title "${singleTitle}".`);
|
|
173
|
+
}
|
|
141
174
|
}
|
|
142
175
|
break;
|
|
143
176
|
}
|
|
@@ -152,6 +185,7 @@ async function main() {
|
|
|
152
185
|
} else {
|
|
153
186
|
const nowPinned = togglePin(note.id);
|
|
154
187
|
showSuccess(`Note "${rest}" ${nowPinned ? '📌 pinned' : 'unpinned'}.`);
|
|
188
|
+
triggerAutoSync();
|
|
155
189
|
}
|
|
156
190
|
break;
|
|
157
191
|
}
|
|
@@ -166,6 +200,36 @@ async function main() {
|
|
|
166
200
|
else showNotesList(results, `Search results for "${rest}"`);
|
|
167
201
|
break;
|
|
168
202
|
}
|
|
203
|
+
// ── TUI Dashboard ──────────────────────────────────────────────────────
|
|
204
|
+
case '-ui':
|
|
205
|
+
case '--ui':
|
|
206
|
+
case '--tui': {
|
|
207
|
+
launchTUI();
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Git Sync ───────────────────────────────────────────────────────────
|
|
212
|
+
case '--sync': {
|
|
213
|
+
if (rest === '--setup' || rest === 'setup') {
|
|
214
|
+
await syncNotes(true);
|
|
215
|
+
} else {
|
|
216
|
+
await syncNotes();
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── AI Copilot ─────────────────────────────────────────────────────────
|
|
222
|
+
case '-ai':
|
|
223
|
+
case '--ai': {
|
|
224
|
+
if (rest === '--setup' || rest === 'setup' || rest === '-reset' || rest === '--reset' || rest === 'reset') {
|
|
225
|
+
await setupApiKey();
|
|
226
|
+
} else if (!rest) {
|
|
227
|
+
await chatAI();
|
|
228
|
+
} else {
|
|
229
|
+
await queryAI(rest);
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
169
233
|
|
|
170
234
|
// ── Storage path ───────────────────────────────────────────────────────
|
|
171
235
|
case '--path': {
|
|
@@ -181,10 +245,21 @@ async function main() {
|
|
|
181
245
|
break;
|
|
182
246
|
}
|
|
183
247
|
|
|
184
|
-
// ── Unknown
|
|
248
|
+
// ── Unknown / Direct View ──────────────────────────────────────────────
|
|
185
249
|
default: {
|
|
186
|
-
|
|
187
|
-
|
|
250
|
+
if (flag && flag.startsWith('-')) {
|
|
251
|
+
showError(`Unknown flag "${flag}". Run saveinme -h for help.`);
|
|
252
|
+
process.exitCode = 1;
|
|
253
|
+
} else {
|
|
254
|
+
const fullTitle = argv.join(' ').trim();
|
|
255
|
+
const note = getNoteByTitle(fullTitle);
|
|
256
|
+
if (note) {
|
|
257
|
+
showNote(note);
|
|
258
|
+
} else {
|
|
259
|
+
showError(`No note found with title "${fullTitle}".`);
|
|
260
|
+
process.exitCode = 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
188
263
|
}
|
|
189
264
|
}
|
|
190
265
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saveinme",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Your personal terminal notebook — save anything, permanently.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/saveinme.js",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"start": "node bin/saveinme.js"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@google/generative-ai": "^0.24.1",
|
|
20
21
|
"@inquirer/prompts": "^5.0.0",
|
|
21
22
|
"boxen": "^7.1.1",
|
|
22
23
|
"chalk": "^5.3.0",
|