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/src/ai.js ADDED
@@ -0,0 +1,447 @@
1
+ import { GoogleGenerativeAI } from '@google/generative-ai';
2
+ import { input, select, confirm } from '@inquirer/prompts';
3
+ import chalk from 'chalk';
4
+ import { execSync, exec, spawnSync } from 'child_process';
5
+ import { getConfig, saveConfig, getAllNotes } from './store.js';
6
+ import { showHeader, showSuccess, showError, showInfo } from './display.js';
7
+
8
+ /**
9
+ * Retrieves the Gemini API Key from environment variables or saved configuration.
10
+ */
11
+ function getApiKey() {
12
+ return process.env.GEMINI_API_KEY || getConfig().geminiApiKey || null;
13
+ }
14
+
15
+ /**
16
+ * Checks if local Ollama command is installed and in PATH.
17
+ */
18
+ function isOllamaInstalled() {
19
+ try {
20
+ execSync('ollama --version', { stdio: 'ignore' });
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Opens a URL in the user's default browser.
29
+ */
30
+ function openUrl(url) {
31
+ const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
32
+ exec(`${start} ${url}`);
33
+ }
34
+
35
+ /**
36
+ * Prompts the user to set up and save their preferred AI Provider (Gemini or Ollama).
37
+ */
38
+ export async function setupApiKey() {
39
+ showHeader();
40
+ console.log(chalk.bold.yellow('\n🤖 AI Copilot Setup'));
41
+
42
+ const providerChoice = await select({
43
+ message: 'Select AI Provider:',
44
+ choices: [
45
+ { name: '🌐 Google Gemini (Cloud API)', value: 'gemini' },
46
+ { name: '📴 Ollama (Local Offline LLM)', value: 'ollama' },
47
+ ],
48
+ });
49
+
50
+ if (providerChoice === 'gemini') {
51
+ console.log(chalk.dim('\n To use Gemini, you need a Gemini API Key.'));
52
+ console.log(chalk.dim(' You can get one for FREE from Google AI Studio at:'));
53
+ console.log(chalk.cyan(' https://aistudio.google.com/\n'));
54
+
55
+ const key = await input({
56
+ message: 'Enter your Gemini API Key:',
57
+ validate: v => v.trim().length > 0 || 'API Key cannot be empty.',
58
+ });
59
+
60
+ saveConfig({
61
+ aiProvider: 'gemini',
62
+ geminiApiKey: key.trim(),
63
+ ollamaUrl: undefined,
64
+ ollamaModel: undefined,
65
+ });
66
+ showSuccess('API Key saved successfully! You can now use "sim -ai".');
67
+ return true;
68
+ } else {
69
+ if (!isOllamaInstalled()) {
70
+ console.log(chalk.yellow('\n❌ Ollama is not installed or not found in your PATH.'));
71
+ const download = await confirm({
72
+ message: 'Would you like to open the Ollama website to download and install it?',
73
+ default: true,
74
+ });
75
+
76
+ if (download) {
77
+ showInfo('Opening https://ollama.com/ in your browser...');
78
+ openUrl('https://ollama.com/');
79
+ showInfo('Once installed, start the Ollama application and run: sim -ai --setup');
80
+ return false;
81
+ } else {
82
+ showInfo('Proceeding with manual setup (useful if your Ollama server is hosted on a remote machine).');
83
+ }
84
+ }
85
+
86
+ const url = await input({
87
+ message: 'Ollama Endpoint URL:',
88
+ default: getConfig().ollamaUrl || 'http://localhost:11434',
89
+ });
90
+
91
+ let detectedModels = [];
92
+ let connectionFailed = false;
93
+ showInfo('Fetching locally downloaded models from Ollama...');
94
+ try {
95
+ const controller = new AbortController();
96
+ const timeoutId = setTimeout(() => controller.abort(), 2000); // 2 second timeout
97
+ const res = await fetch(`${url.trim()}/api/tags`, { signal: controller.signal });
98
+ clearTimeout(timeoutId);
99
+
100
+ if (res.ok) {
101
+ const data = await res.json();
102
+ if (data.models && data.models.length > 0) {
103
+ detectedModels = data.models.map(m => m.name);
104
+ }
105
+ }
106
+ } catch {
107
+ connectionFailed = true;
108
+ }
109
+
110
+ let model = '';
111
+ if (connectionFailed) {
112
+ console.log(chalk.yellow('\n⚠️ Could not connect to Ollama at that URL.'));
113
+ console.log(chalk.dim(' Make sure you run "ollama serve" in a separate terminal window.\n'));
114
+ model = await input({
115
+ message: 'Ollama Model Name (e.g. llama3, gemma2, phi3):',
116
+ default: getConfig().ollamaModel || 'llama3',
117
+ validate: v => v.trim().length > 0 || 'Model name cannot be empty.',
118
+ });
119
+ } else if (detectedModels.length > 0) {
120
+ console.log(chalk.green(`\n✔ Connected to Ollama! Found ${detectedModels.length} local model(s).`));
121
+ const choices = [
122
+ ...detectedModels.map(m => ({ name: `🦙 ${m}`, value: m })),
123
+ { name: chalk.cyan('+ Use another model (not listed)…'), value: '__other__' }
124
+ ];
125
+
126
+ const modelChoice = await select({
127
+ message: 'Select a locally installed model:',
128
+ choices,
129
+ });
130
+
131
+ if (modelChoice === '__other__') {
132
+ model = await input({
133
+ message: 'Ollama Model Name (e.g. llama3, gemma2):',
134
+ validate: v => v.trim().length > 0 || 'Model name cannot be empty.',
135
+ });
136
+ } else {
137
+ model = modelChoice;
138
+ }
139
+ } else {
140
+ console.log(chalk.yellow('\n⚠️ No local models found on your Ollama server.'));
141
+
142
+ const downloadChoice = await select({
143
+ message: 'Select a popular model to download (pull):',
144
+ choices: [
145
+ { name: '🦙 llama3 (8B - Smart & Balanced)', value: 'llama3' },
146
+ { name: '💎 gemma2 (9B - Google\'s local LLM)', value: 'gemma2' },
147
+ { name: '⚡ phi3 (3.8B - Lightweight & Fast)', value: 'phi3' },
148
+ { name: '🌪️ mistral (7B - Great coder/conversational)', value: 'mistral' },
149
+ { name: '✍️ Enter model name manually', value: '__custom__' },
150
+ ],
151
+ });
152
+
153
+ let modelToPull = downloadChoice;
154
+ if (downloadChoice === '__custom__') {
155
+ modelToPull = await input({
156
+ message: 'Ollama Model Name to pull:',
157
+ validate: v => v.trim().length > 0 || 'Model name cannot be empty.',
158
+ });
159
+ }
160
+
161
+ model = modelToPull.trim();
162
+
163
+ const shouldPull = await confirm({
164
+ message: `Would you like to download (pull) "${model}" now?`,
165
+ default: true,
166
+ });
167
+
168
+ if (shouldPull) {
169
+ console.log(chalk.cyan(`\n📥 Downloading model "${model}" via Ollama...`));
170
+ console.log(chalk.dim(' This can take a few minutes depending on your internet connection.\n'));
171
+
172
+ // Run ollama pull directly in the terminal, sharing stdout/stderr
173
+ const pullProcess = spawnSync('ollama', ['pull', model], { stdio: 'inherit' });
174
+
175
+ if (pullProcess.status === 0) {
176
+ showSuccess(`Model "${model}" downloaded successfully!`);
177
+ } else {
178
+ showError(`Failed to download model "${model}". We will save the configuration anyway, but you may need to run "ollama pull ${model}" manually.`);
179
+ }
180
+ } else {
181
+ showInfo(`Skipped download. Make sure to run "ollama pull ${model}" before querying.`);
182
+ }
183
+ }
184
+
185
+ saveConfig({
186
+ aiProvider: 'ollama',
187
+ ollamaUrl: url.trim(),
188
+ ollamaModel: model.trim(),
189
+ geminiApiKey: undefined,
190
+ });
191
+ showSuccess(`Ollama local provider saved successfully! Model: "${model.trim()}".`);
192
+ return true;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Formats notes list as a clean context string for the AI.
198
+ */
199
+ function buildNotesContext() {
200
+ const notes = getAllNotes();
201
+ if (notes.length === 0) {
202
+ return 'The user does not have any notes saved yet.';
203
+ }
204
+
205
+ let context = 'Here is the user\'s local terminal notes database:\n\n';
206
+ notes.forEach((note, index) => {
207
+ context += `--- NOTE #${index + 1} ---\n`;
208
+ context += `Title: ${note.title}\n`;
209
+ if (note.category) context += `Notebook/Category: ${note.category}\n`;
210
+ if (note.tags && note.tags.length > 0) context += `Tags: ${note.tags.join(', ')}\n`;
211
+ context += `Priority: ${note.priority || 'medium'}\n`;
212
+ context += `Pinned: ${note.pinned ? 'Yes' : 'No'}\n`;
213
+ context += `Last Updated: ${note.updatedAt}\n`;
214
+ context += `Content:\n${note.content}\n`;
215
+ context += `-----------------\n\n`;
216
+ });
217
+ return context;
218
+ }
219
+
220
+ /**
221
+ * Helper to stream response from a local Ollama server.
222
+ */
223
+ async function streamOllama(prompt) {
224
+ const config = getConfig();
225
+ const url = config.ollamaUrl || 'http://localhost:11434';
226
+ const model = config.ollamaModel || 'llama3';
227
+
228
+ try {
229
+ const res = await fetch(`${url}/api/generate`, {
230
+ method: 'POST',
231
+ headers: { 'Content-Type': 'application/json' },
232
+ body: JSON.stringify({
233
+ model: model,
234
+ prompt: prompt,
235
+ stream: true,
236
+ }),
237
+ });
238
+
239
+ if (!res.ok) {
240
+ let errMsg = '';
241
+ try {
242
+ // Try parsing JSON error from Ollama
243
+ const errJson = await res.json();
244
+ errMsg = errJson.error || '';
245
+ } catch {}
246
+
247
+ showError(`\nOllama server returned status ${res.status}${errMsg ? ': ' + errMsg : ''}`);
248
+
249
+ if (res.status === 404 || errMsg.toLowerCase().includes('not found') || errMsg.toLowerCase().includes('pull')) {
250
+ console.log(chalk.bold.yellow('\n💡 How to fix:'));
251
+ console.log(` 1. Make sure you have pulled the model locally. Run this in a separate terminal:`);
252
+ console.log(chalk.cyan(` ollama pull ${model}`));
253
+ console.log(` 2. To see your downloaded models, run:`);
254
+ console.log(chalk.cyan(` ollama list`));
255
+ console.log(` 3. If you want to use a different model in saveinme, run:`);
256
+ console.log(chalk.cyan(` sim -ai --setup\n`));
257
+ }
258
+ return;
259
+ }
260
+
261
+ if (res.body.getReader) {
262
+ const reader = res.body.getReader();
263
+ const decoder = new TextDecoder();
264
+ let buffer = '';
265
+ while (true) {
266
+ const { done, value } = await reader.read();
267
+ if (done) break;
268
+ buffer += decoder.decode(value, { stream: true });
269
+ const lines = buffer.split('\n');
270
+ buffer = lines.pop() || '';
271
+ for (const line of lines) {
272
+ if (line.trim()) {
273
+ try {
274
+ const parsed = JSON.parse(line);
275
+ if (parsed.response) process.stdout.write(parsed.response);
276
+ } catch {}
277
+ }
278
+ }
279
+ }
280
+ } else {
281
+ let buffer = '';
282
+ for await (const chunk of res.body) {
283
+ buffer += chunk.toString();
284
+ const lines = buffer.split('\n');
285
+ buffer = lines.pop() || '';
286
+ for (const line of lines) {
287
+ if (line.trim()) {
288
+ try {
289
+ const parsed = JSON.parse(line);
290
+ if (parsed.response) process.stdout.write(parsed.response);
291
+ } catch {}
292
+ }
293
+ }
294
+ }
295
+ }
296
+ } catch (err) {
297
+ showError(`\nOllama Connection Error: ${err.message}`);
298
+ console.log(chalk.bold.yellow('\n💡 How to fix:'));
299
+ console.log(` 1. Make sure the Ollama application is running on your system.`);
300
+ console.log(` 2. Run the service in a separate terminal if it's not active:`);
301
+ console.log(chalk.cyan(' ollama serve'));
302
+ console.log(` 3. Check if Ollama is running at the configured endpoint: ${url}`);
303
+ console.log(` 4. To configure a different URL or model, run:`);
304
+ console.log(chalk.cyan(' sim -ai --setup\n'));
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Runs a query against the configured AI model.
310
+ * @param {string} question The question or prompt
311
+ */
312
+ export async function queryAI(question) {
313
+ const config = getConfig();
314
+ const provider = config.aiProvider || (getApiKey() ? 'gemini' : null);
315
+
316
+ if (!provider) {
317
+ showError('AI Copilot is not configured.');
318
+ const ok = await setupApiKey();
319
+ if (!ok) return;
320
+ return queryAI(question);
321
+ }
322
+
323
+ const notesContext = buildNotesContext();
324
+ const prompt = `You are "saveinme (sim)", a smart personal terminal notebook assistant.
325
+ You help the user summarize, extract, or search information from their notes database.
326
+ Keep your responses helpful, very concise, and clean for terminal output (use bolding, lists, and markdown spacing appropriately).
327
+
328
+ ${notesContext}
329
+
330
+ User Question: ${question}
331
+ Answer:`;
332
+
333
+ if (provider === 'gemini') {
334
+ const apiKey = getApiKey();
335
+ if (!apiKey) {
336
+ showError('Gemini API key is not configured.');
337
+ await setupApiKey();
338
+ return queryAI(question);
339
+ }
340
+
341
+ const genAI = new GoogleGenerativeAI(apiKey);
342
+ const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
343
+
344
+ showInfo('Thinking (Gemini)...');
345
+ try {
346
+ const result = await model.generateContentStream(prompt);
347
+ console.log(chalk.bold.cyan('\n🤖 saveinme AI:'));
348
+ for await (const chunk of result.stream) {
349
+ process.stdout.write(chunk.text());
350
+ }
351
+ console.log('\n');
352
+ } catch (err) {
353
+ showError(`Gemini API Error: ${err.message}`);
354
+ }
355
+ } else {
356
+ showInfo(`Thinking (Ollama: ${config.ollamaModel || 'llama3'})...`);
357
+ console.log(chalk.bold.cyan('\n🤖 saveinme AI:'));
358
+ await streamOllama(prompt);
359
+ console.log('\n');
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Launches an interactive chat session with the AI Copilot.
365
+ */
366
+ export async function chatAI() {
367
+ let config = getConfig();
368
+ let provider = config.aiProvider || (getApiKey() ? 'gemini' : null);
369
+
370
+ if (!provider) {
371
+ showError('AI Copilot is not configured.');
372
+ const ok = await setupApiKey();
373
+ if (!ok) return;
374
+ config = getConfig();
375
+ provider = config.aiProvider || (getApiKey() ? 'gemini' : null);
376
+ if (!provider) return;
377
+ }
378
+
379
+ showHeader();
380
+ let modelName = provider === 'gemini' ? 'gemini-1.5-flash' : (config.ollamaModel || 'llama3');
381
+ console.log(chalk.bold.yellow(`\n💬 saveinme AI Copilot Chat Session`));
382
+ console.log(chalk.dim(` Provider: ${provider === 'gemini' ? 'Gemini' : 'Ollama'} (${modelName})`));
383
+ console.log(chalk.dim(' Ask me anything about your notes. I will read your live database.'));
384
+ console.log(chalk.dim(' Type "/setup" to change the model/provider.'));
385
+ console.log(chalk.dim(' Type "exit" or "quit" to leave the chat.\n'));
386
+
387
+ while (true) {
388
+ const question = await input({
389
+ message: chalk.cyan('You:'),
390
+ });
391
+
392
+ const trimmed = question.trim();
393
+ if (trimmed.toLowerCase() === 'exit' || trimmed.toLowerCase() === 'quit') {
394
+ showInfo('AI Chat session closed.');
395
+ break;
396
+ }
397
+
398
+ if (trimmed.toLowerCase() === '/setup' || trimmed.toLowerCase() === '/model') {
399
+ await setupApiKey();
400
+ config = getConfig();
401
+ provider = config.aiProvider || (getApiKey() ? 'gemini' : null);
402
+ modelName = provider === 'gemini' ? 'gemini-1.5-flash' : (config.ollamaModel || 'llama3');
403
+ console.log(chalk.bold.yellow(`\n💬 AI Configuration Updated`));
404
+ console.log(chalk.dim(` Current Provider: ${provider === 'gemini' ? 'Gemini' : 'Ollama'} (${modelName})\n`));
405
+ continue;
406
+ }
407
+
408
+ if (!trimmed) continue;
409
+
410
+ const notesContext = buildNotesContext();
411
+ const prompt = `You are "saveinme (sim)", a smart personal terminal notebook assistant.
412
+ You help the user summarize, search, or extract information from their notes database.
413
+ Keep your responses helpful, concise, and formatted beautifully for terminal display.
414
+
415
+ ${notesContext}
416
+
417
+ User Question: ${trimmed}
418
+ Answer:`;
419
+
420
+ console.log(chalk.bold.magenta('\n🤖 saveinme AI:'));
421
+ if (provider === 'gemini') {
422
+ const apiKey = getApiKey();
423
+ if (!apiKey) {
424
+ showError('Gemini API key is not configured.');
425
+ await setupApiKey();
426
+ config = getConfig();
427
+ provider = config.aiProvider || (getApiKey() ? 'gemini' : null);
428
+ if (!provider) return;
429
+ continue;
430
+ }
431
+ const genAI = new GoogleGenerativeAI(apiKey);
432
+ const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
433
+ try {
434
+ const result = await model.generateContentStream(prompt);
435
+ for await (const chunk of result.stream) {
436
+ process.stdout.write(chunk.text());
437
+ }
438
+ console.log('\n');
439
+ } catch (err) {
440
+ showError(`Gemini API Error: ${err.message}`);
441
+ }
442
+ } else {
443
+ await streamOllama(prompt);
444
+ console.log('\n');
445
+ }
446
+ }
447
+ }
package/src/display.js CHANGED
@@ -142,10 +142,12 @@ export function showNotesList(notes, heading = null) {
142
142
  console.log(
143
143
  chalk.dim(
144
144
  `\n ${notes.length} note${notes.length !== 1 ? 's' : ''} saved` +
145
+ ` · sim -ui dashboard` +
146
+ ` · sim -ai ai copilot` +
145
147
  ` · sim -c create` +
146
148
  ` · sim -cl paste clip` +
147
- ` · sim -v <title> view` +
148
- ` · sim -f <term> search` +
149
+ ` · sim <title> view` +
150
+ ` · sim -rm remove` +
149
151
  ` · sim -h help\n`
150
152
  )
151
153
  );
@@ -244,13 +246,17 @@ export function showHelp() {
244
246
  ${chalk.dim(' * Note: You can use "saveinme" or the shortcut "sim" interchangeably *')}
245
247
 
246
248
  ${chalk.bold.white('sim')} List all saved notes (pinned first)
249
+ ${chalk.bold.white('sim <title>')} Directly view a note by its title
250
+ ${chalk.bold.white('sim -ui')} Launch Split-Pane TUI Dashboard
247
251
  ${chalk.bold.white('sim -c')} [${chalk.italic('title')}] Create/edit a note interactively
248
252
  ${chalk.bold.white('sim -cl')} [${chalk.italic('title')}] Instantly save text from your clipboard
249
- ${chalk.bold.white('sim -t')} [${chalk.italic('notebook')}] Set default target notebook (folder)
250
- ${chalk.bold.white('sim -v')} ${chalk.italic('<title>')} View a note's full content
251
- ${chalk.bold.white('sim -d')} ${chalk.italic('<title>')} Delete a note permanently
253
+ ${chalk.bold.white('sim -d')} [${chalk.italic('notebook')} | ${chalk.italic('--clear')}] Set default notebook / select from list / clear default
254
+ ${chalk.bold.white('sim -v')} [${chalk.italic('title')}] View a note's full content (or pick from list)
255
+ ${chalk.bold.white('sim -rm')} [${chalk.italic('title')}] Remove note(s) permanently (or pick from list)
252
256
  ${chalk.bold.white('sim -p')} ${chalk.italic('<title>')} Toggle pin status (stays at top)
253
257
  ${chalk.bold.white('sim -f')} ${chalk.italic('<term>')} Search title, content, category, or tags
258
+ ${chalk.bold.white('sim -ai')} [${chalk.italic('query')} | ${chalk.italic('--setup')} | ${chalk.italic('--reset')}] Ask/chat with AI Copilot / configure provider / reset settings
259
+ ${chalk.bold.white('sim --sync')} [${chalk.italic('--setup')}] Sync notes database with git remote / configure remote repo
254
260
  ${chalk.bold.white('sim --path')} Show database folder path on disk
255
261
  ${chalk.bold.white('sim -h')} Show this detailed help manual
256
262
 
@@ -282,17 +288,49 @@ export function showHelp() {
282
288
 
283
289
  ${chalk.bold.cyan('3. Default Target Notebooks')}
284
290
  ↳ Targets allow you to automatically group notes by category (like folder routing).
285
- - Set active notebook target: ${chalk.white('sim -t "<name>"')}
286
- - View current active target: ${chalk.white('sim -t')}
287
- - Clear target default: ${chalk.white('sim -t --clear')}
291
+ - Set active notebook target: ${chalk.white('sim -d "<name>"')}
292
+ - Select target from a list: ${chalk.white('sim -d')} ${chalk.dim('↳ Shows interactive notebook selector')}
293
+ - Clear target default: ${chalk.white('sim -d --clear')}
288
294
 
289
295
  ${chalk.yellow('Example (Set target to "Work"):')}
290
- $ sim -t "Work"
296
+ $ sim -d "Work"
291
297
  $ sim -c "meeting summary" ${chalk.dim('↳ Automatically saved inside notebook "Work"')}
292
298
  ${chalk.yellow('Example (Override target for single command using -n):')}
293
299
  $ sim -c "groceries" -n "Personal" ${chalk.dim('↳ Bypasses default target, saves to "Personal"')}
294
300
 
295
- ${chalk.bold.cyan('4. Piping Command Outputs (Advanced)')}
301
+ ${chalk.bold.cyan('4. Viewing Notes')}
302
+ ↳ You can view the contents of any saved note easily:
303
+ - View a note directly: ${chalk.white('sim "My Note"')}
304
+ - View note via flag: ${chalk.white('sim -v "My Note"')}
305
+ - Select note from a list: ${chalk.white('sim -v')} ${chalk.dim('↳ Shows interactive note selector')}
306
+
307
+ ${chalk.bold.cyan('5. Removing Notes (Single or Group)')}
308
+ ↳ Permanently delete one or more notes:
309
+ - Delete a single note: ${chalk.white('sim -rm "My Note"')}
310
+ - Delete multiple notes: ${chalk.white('sim -rm note1 note2')}
311
+ - Select notes to remove: ${chalk.white('sim -rm')} ${chalk.dim('↳ Opens multi-select checkbox menu')}
312
+
313
+ ${chalk.bold.cyan('6. Split-Pane TUI Dashboard')}
314
+ ↳ Run the full-screen terminal user interface to browse, read, and manage notes visually:
315
+ $ sim -ui
316
+ - Use Arrow keys (or j/k) to navigate notes on the left.
317
+ - View wrapped note contents and metadata on the right.
318
+ - Press [Enter] to edit, [c] to create, [p] to pin, [d] to delete, [/] to search, and [q] to exit.
319
+
320
+ ${chalk.bold.cyan('7. AI Copilot (Gemini / Ollama)')}
321
+ ↳ Query your notes using cloud Gemini or local offline Ollama models:
322
+ - Set up AI Provider: ${chalk.white('sim -ai --setup')}
323
+ - Reset AI settings: ${chalk.white('sim -ai --reset')}
324
+ - Query your notes directly: ${chalk.white('sim -ai "how do I configure the database?"')}
325
+ - Start interactive AI chat: ${chalk.white('sim -ai')}
326
+
327
+ ${chalk.bold.cyan('8. Git Sync & Backup')}
328
+ ↳ Keep notes backed up and synced across different machines:
329
+ - Set up remote git repo: ${chalk.white('sim --sync --setup')}
330
+ - Trigger manual sync: ${chalk.white('sim --sync')}
331
+ - Auto-Sync: Once remote is set up, notes will automatically sync in the background on save/delete!
332
+
333
+ ${chalk.bold.cyan('9. Piping Command Outputs (Advanced)')}
296
334
  ↳ Pipe stdout of any command directly into a note. Saves ${chalk.bold.green('instantly')} with ${chalk.bold.red('zero prompts')}!
297
335
  - Target: Default target notebook, unless overridden with ${chalk.white('-n')}.
298
336
  - Title: Uses custom title if provided, otherwise defaults to: