pnote 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1548 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command6 } from "commander";
5
+
6
+ // src/lib/errors.ts
7
+ import pc from "picocolors";
8
+ var ExitCode = {
9
+ SUCCESS: 0,
10
+ GENERAL_ERROR: 1,
11
+ AUTH_ERROR: 2,
12
+ NOT_FOUND: 3,
13
+ NETWORK_ERROR: 4
14
+ };
15
+ var CLIError = class extends Error {
16
+ constructor(message, exitCode = ExitCode.GENERAL_ERROR, hint) {
17
+ super(message);
18
+ this.exitCode = exitCode;
19
+ this.hint = hint;
20
+ this.name = "CLIError";
21
+ }
22
+ };
23
+ var AuthError = class extends CLIError {
24
+ constructor(message, hint) {
25
+ super(message, ExitCode.AUTH_ERROR, hint);
26
+ this.name = "AuthError";
27
+ }
28
+ };
29
+ var NotFoundError = class extends CLIError {
30
+ constructor(resource, id) {
31
+ super(`${resource} not found (id: ${id})`, ExitCode.NOT_FOUND);
32
+ this.name = "NotFoundError";
33
+ }
34
+ };
35
+ var NetworkError = class extends CLIError {
36
+ constructor(message = "Network error. Check your connection.") {
37
+ super(message, ExitCode.NETWORK_ERROR);
38
+ this.name = "NetworkError";
39
+ }
40
+ };
41
+ function formatError(error, noColor = false) {
42
+ const red = noColor ? (s) => s : pc.red;
43
+ const dim = noColor ? (s) => s : pc.dim;
44
+ if (error instanceof CLIError) {
45
+ let message = `${red("Error:")} ${error.message}`;
46
+ if (error.hint) {
47
+ message += `
48
+ ${dim("Hint:")} ${error.hint}`;
49
+ }
50
+ return message;
51
+ }
52
+ if (error instanceof Error) {
53
+ return `${red("Error:")} ${error.message}`;
54
+ }
55
+ return `${red("Error:")} ${String(error)}`;
56
+ }
57
+ function handleError(error, noColor = false) {
58
+ console.error(formatError(error, noColor));
59
+ if (error instanceof CLIError) {
60
+ process.exit(error.exitCode);
61
+ }
62
+ process.exit(ExitCode.GENERAL_ERROR);
63
+ }
64
+
65
+ // src/commands/auth/index.ts
66
+ import { Command } from "commander";
67
+
68
+ // src/commands/auth/login.ts
69
+ import pc2 from "picocolors";
70
+
71
+ // src/lib/config.ts
72
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, chmodSync } from "fs";
73
+ import { homedir } from "os";
74
+ import { join } from "path";
75
+ function getConfigDir() {
76
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
77
+ if (xdgConfig) {
78
+ return join(xdgConfig, "pnote");
79
+ }
80
+ const platform = process.platform;
81
+ if (platform === "darwin") {
82
+ const macPath = join(homedir(), "Library", "Application Support", "pnote");
83
+ if (existsSync(macPath)) {
84
+ return macPath;
85
+ }
86
+ }
87
+ return join(homedir(), ".config", "pnote");
88
+ }
89
+ function ensureConfigDir() {
90
+ const dir = getConfigDir();
91
+ if (!existsSync(dir)) {
92
+ mkdirSync(dir, { recursive: true, mode: 448 });
93
+ }
94
+ return dir;
95
+ }
96
+ var CREDENTIALS_FILE = "credentials.json";
97
+ function getCredentialsPath() {
98
+ return join(getConfigDir(), CREDENTIALS_FILE);
99
+ }
100
+ function loadCredentials() {
101
+ const envToken = process.env.PNOTE_TOKEN;
102
+ if (envToken) {
103
+ return {
104
+ token: envToken,
105
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
106
+ };
107
+ }
108
+ const path = getCredentialsPath();
109
+ if (!existsSync(path)) {
110
+ return null;
111
+ }
112
+ try {
113
+ const content = readFileSync(path, "utf-8");
114
+ return JSON.parse(content);
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+ function saveCredentials(credentials) {
120
+ const dir = ensureConfigDir();
121
+ const path = join(dir, CREDENTIALS_FILE);
122
+ writeFileSync(path, JSON.stringify(credentials, null, 2), "utf-8");
123
+ chmodSync(path, 384);
124
+ }
125
+ function deleteCredentials() {
126
+ const path = getCredentialsPath();
127
+ if (existsSync(path)) {
128
+ unlinkSync(path);
129
+ return true;
130
+ }
131
+ return false;
132
+ }
133
+ function getToken() {
134
+ const creds = loadCredentials();
135
+ return creds?.token ?? null;
136
+ }
137
+ function isLoggedIn() {
138
+ return getToken() !== null;
139
+ }
140
+ function validateTokenFormat(token) {
141
+ return /^pn_[A-Za-z0-9_-]{20,50}$/.test(token);
142
+ }
143
+ var CONFIG_FILE = "config.json";
144
+ function loadConfig() {
145
+ const path = join(getConfigDir(), CONFIG_FILE);
146
+ if (!existsSync(path)) {
147
+ return {};
148
+ }
149
+ try {
150
+ const content = readFileSync(path, "utf-8");
151
+ return JSON.parse(content);
152
+ } catch {
153
+ return {};
154
+ }
155
+ }
156
+ var DEFAULT_API_ENDPOINT = "https://tafnybfhwybgijozwfyq.supabase.co/functions/v1/promptnote-mcp/mcp";
157
+ function getApiEndpoint() {
158
+ const config = loadConfig();
159
+ return config.api_endpoint ?? process.env.PNOTE_API_ENDPOINT ?? DEFAULT_API_ENDPOINT;
160
+ }
161
+
162
+ // src/commands/auth/login.ts
163
+ async function loginAction() {
164
+ if (isLoggedIn()) {
165
+ console.log(pc2.yellow("Already logged in."));
166
+ console.log(pc2.dim(`Credentials stored at: ${getCredentialsPath()}`));
167
+ console.log("");
168
+ console.log(`Run ${pc2.cyan("'pnote auth logout'")} to logout first.`);
169
+ return;
170
+ }
171
+ console.log(pc2.bold("Login to PromptNote"));
172
+ console.log("");
173
+ console.log("To authenticate, you need a Personal Access Token (PAT).");
174
+ console.log("");
175
+ console.log(pc2.dim("Steps:"));
176
+ console.log(" 1. Open " + pc2.cyan("https://app.promptnoteapp.com"));
177
+ console.log(" 2. Go to Settings > API Tokens");
178
+ console.log(" 3. Generate a new token");
179
+ console.log(" 4. Run: " + pc2.cyan("pnote auth token <your-token>"));
180
+ console.log("");
181
+ console.log(pc2.dim("Or set the PROMTIE_TOKEN environment variable."));
182
+ }
183
+
184
+ // src/commands/auth/token.ts
185
+ import pc3 from "picocolors";
186
+
187
+ // src/lib/api.ts
188
+ function getRestApiBase() {
189
+ const mcpEndpoint = getApiEndpoint();
190
+ return mcpEndpoint.replace(/\/mcp$/, "/v1");
191
+ }
192
+ async function callRestApi(method, path, body, options = {}) {
193
+ const token = getToken();
194
+ if (!token) {
195
+ throw new AuthError(
196
+ "Not logged in",
197
+ "Run 'pnote auth login' or 'pnote auth token <token>' to authenticate"
198
+ );
199
+ }
200
+ const baseUrl = getRestApiBase();
201
+ const url = `${baseUrl}${path}`;
202
+ const headers = {
203
+ "Content-Type": "application/json",
204
+ Authorization: `Bearer ${token}`
205
+ };
206
+ if (options.pin) {
207
+ headers["X-PromptNote-PIN"] = options.pin;
208
+ }
209
+ let response;
210
+ try {
211
+ const fetchOptions = {
212
+ method,
213
+ headers,
214
+ signal: AbortSignal.timeout(options.timeout ?? 3e4)
215
+ };
216
+ if (body && (method === "POST" || method === "PATCH")) {
217
+ fetchOptions.body = JSON.stringify(body);
218
+ }
219
+ response = await fetch(url, fetchOptions);
220
+ } catch (error) {
221
+ if (error instanceof Error) {
222
+ if (error.name === "TimeoutError" || error.name === "AbortError") {
223
+ throw new NetworkError("Request timed out. Please try again.");
224
+ }
225
+ if (error.message.includes("fetch")) {
226
+ throw new NetworkError("Unable to connect to PromptNote API. Check your internet connection.");
227
+ }
228
+ }
229
+ throw new NetworkError();
230
+ }
231
+ if (!response.ok) {
232
+ const errorBody = await response.json().catch(() => ({}));
233
+ if (response.status === 401) {
234
+ throw new AuthError(
235
+ errorBody.message || "Invalid or expired token",
236
+ "Run 'pnote auth login' to re-authenticate"
237
+ );
238
+ }
239
+ if (response.status === 403) {
240
+ throw new CLIError("Permission denied", ExitCode.AUTH_ERROR);
241
+ }
242
+ if (response.status === 404) {
243
+ throw new CLIError(errorBody.message || "Resource not found", ExitCode.NOT_FOUND);
244
+ }
245
+ if (response.status === 400) {
246
+ throw new CLIError(errorBody.message || "Invalid request", ExitCode.GENERAL_ERROR);
247
+ }
248
+ throw new CLIError(`API error (${response.status}): ${errorBody.message || "Unknown error"}`);
249
+ }
250
+ return response.json();
251
+ }
252
+ async function listNotes(params = {}, options = {}) {
253
+ const query = new URLSearchParams();
254
+ if (params.tag) query.set("tag", params.tag);
255
+ if (params.archived !== void 0) query.set("archived", String(params.archived));
256
+ if (params.pinned !== void 0) query.set("pinned", String(params.pinned));
257
+ if (params.deleted !== void 0) query.set("deleted", String(params.deleted));
258
+ if (params.protected !== void 0) query.set("protected", String(params.protected));
259
+ if (params.search) query.set("search", params.search);
260
+ if (params.limit) query.set("limit", String(params.limit));
261
+ if (params.offset) query.set("offset", String(params.offset));
262
+ const queryString = query.toString();
263
+ return callRestApi("GET", `/notes${queryString ? `?${queryString}` : ""}`, void 0, options);
264
+ }
265
+ async function getNote(id, options = {}) {
266
+ return callRestApi("GET", `/notes/${id}`, void 0, options);
267
+ }
268
+ async function createNote(data, options = {}) {
269
+ return callRestApi("POST", "/notes", data, options);
270
+ }
271
+ async function updateNote(id, data, options = {}) {
272
+ return callRestApi("PATCH", `/notes/${id}`, data, options);
273
+ }
274
+ async function deleteNote(id, options = {}) {
275
+ return callRestApi("DELETE", `/notes/${id}`, void 0, options);
276
+ }
277
+ async function listSnippets(params, options = {}) {
278
+ const query = new URLSearchParams();
279
+ query.set("note_id", params.note_id);
280
+ if (params.include_deleted !== void 0) query.set("include_deleted", String(params.include_deleted));
281
+ if (params.limit) query.set("limit", String(params.limit));
282
+ return callRestApi("GET", `/snippets?${query.toString()}`, void 0, options);
283
+ }
284
+ async function createSnippet(data, options = {}) {
285
+ return callRestApi("POST", "/snippets", data, options);
286
+ }
287
+ async function toggleFavorite(id, options = {}) {
288
+ return callRestApi("POST", `/snippets/${id}/favorite`, {}, options);
289
+ }
290
+ async function listTags(params = {}, options = {}) {
291
+ const query = new URLSearchParams();
292
+ if (params.include_archived !== void 0) query.set("include_archived", String(params.include_archived));
293
+ const queryString = query.toString();
294
+ return callRestApi("GET", `/tags${queryString ? `?${queryString}` : ""}`, void 0, options);
295
+ }
296
+ async function renameTag(data, options = {}) {
297
+ return callRestApi("POST", "/tags/rename", data, options);
298
+ }
299
+ async function mergeTags(data, options = {}) {
300
+ return callRestApi("POST", "/tags/merge", data, options);
301
+ }
302
+ async function search(params, options = {}) {
303
+ const query = new URLSearchParams();
304
+ query.set("q", params.query);
305
+ if (params.search_notes !== void 0) query.set("search_notes", String(params.search_notes));
306
+ if (params.search_snippets !== void 0) query.set("search_snippets", String(params.search_snippets));
307
+ if (params.limit) query.set("limit", String(params.limit));
308
+ return callRestApi("GET", `/search?${query.toString()}`, void 0, options);
309
+ }
310
+ var requestId = 0;
311
+ function getNextId() {
312
+ return ++requestId;
313
+ }
314
+ async function callMCP(toolName, args = {}) {
315
+ const token = getToken();
316
+ if (!token) {
317
+ throw new AuthError(
318
+ "Not logged in",
319
+ "Run 'pnote auth login' or 'pnote auth token <token>' to authenticate"
320
+ );
321
+ }
322
+ const endpoint = getApiEndpoint();
323
+ const request = {
324
+ jsonrpc: "2.0",
325
+ id: getNextId(),
326
+ method: "tools/call",
327
+ params: {
328
+ name: toolName,
329
+ arguments: args
330
+ }
331
+ };
332
+ let response;
333
+ try {
334
+ response = await fetch(endpoint, {
335
+ method: "POST",
336
+ headers: {
337
+ "Content-Type": "application/json",
338
+ Authorization: `Bearer ${token}`
339
+ },
340
+ body: JSON.stringify(request),
341
+ signal: AbortSignal.timeout(3e4)
342
+ });
343
+ } catch (error) {
344
+ if (error instanceof Error) {
345
+ if (error.name === "TimeoutError" || error.name === "AbortError") {
346
+ throw new NetworkError("Request timed out. Please try again.");
347
+ }
348
+ if (error.message.includes("fetch")) {
349
+ throw new NetworkError("Unable to connect to PromptNote API. Check your internet connection.");
350
+ }
351
+ }
352
+ throw new NetworkError();
353
+ }
354
+ if (!response.ok) {
355
+ const body = await response.text();
356
+ if (response.status === 401) {
357
+ throw new AuthError(
358
+ "Invalid or expired token",
359
+ "Run 'pnote auth login' to re-authenticate"
360
+ );
361
+ }
362
+ if (response.status === 403) {
363
+ throw new CLIError("Permission denied", ExitCode.AUTH_ERROR);
364
+ }
365
+ throw new CLIError(`API error (${response.status}): ${body}`);
366
+ }
367
+ const mcpResponse = await response.json();
368
+ if (mcpResponse.error) {
369
+ throw new CLIError(`MCP error: ${mcpResponse.error.message}`);
370
+ }
371
+ if (!mcpResponse.result) {
372
+ throw new CLIError("Invalid response from API");
373
+ }
374
+ const content = mcpResponse.result.content;
375
+ if (!content || content.length === 0) {
376
+ throw new CLIError("Empty response from API");
377
+ }
378
+ if (mcpResponse.result.isError) {
379
+ const errorText = content[0]?.text || "Unknown error";
380
+ if (errorText.includes("not found") || errorText.includes("PGRST116")) {
381
+ throw new CLIError("Resource not found", ExitCode.NOT_FOUND);
382
+ }
383
+ if (errorText.includes("Unauthorized")) {
384
+ throw new AuthError(errorText);
385
+ }
386
+ throw new CLIError(errorText);
387
+ }
388
+ const textContent = content[0]?.text;
389
+ if (!textContent) {
390
+ throw new CLIError("No content in response");
391
+ }
392
+ try {
393
+ return JSON.parse(textContent);
394
+ } catch {
395
+ return textContent;
396
+ }
397
+ }
398
+ async function verifyToken(token) {
399
+ const baseUrl = getApiEndpoint().replace(/\/mcp$/, "/v1");
400
+ try {
401
+ const response = await fetch(`${baseUrl}/notes?limit=1`, {
402
+ method: "GET",
403
+ headers: {
404
+ "Content-Type": "application/json",
405
+ Authorization: `Bearer ${token}`
406
+ },
407
+ signal: AbortSignal.timeout(1e4)
408
+ });
409
+ if (response.status === 401) {
410
+ return { valid: false };
411
+ }
412
+ if (!response.ok) {
413
+ return { valid: false };
414
+ }
415
+ return { valid: true };
416
+ } catch {
417
+ throw new NetworkError("Unable to verify token. Check your internet connection.");
418
+ }
419
+ }
420
+
421
+ // src/commands/auth/token.ts
422
+ async function tokenAction(token) {
423
+ if (!validateTokenFormat(token)) {
424
+ throw new CLIError(
425
+ "Invalid token format",
426
+ 1,
427
+ "Token should start with 'pn_' followed by 20-50 characters"
428
+ );
429
+ }
430
+ console.log(pc3.dim("Verifying token..."));
431
+ try {
432
+ const result = await verifyToken(token);
433
+ if (!result.valid) {
434
+ throw new CLIError(
435
+ "Invalid token",
436
+ 2,
437
+ "The token was rejected by the server. Please generate a new one."
438
+ );
439
+ }
440
+ saveCredentials({
441
+ token,
442
+ email: result.email,
443
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
444
+ });
445
+ console.log(pc3.green("\u2713") + " Token saved successfully!");
446
+ console.log(pc3.dim(`Credentials stored at: ${getCredentialsPath()}`));
447
+ console.log("");
448
+ console.log(`You can now use ${pc3.cyan("pnote")} commands.`);
449
+ } catch (error) {
450
+ handleError(error);
451
+ }
452
+ }
453
+
454
+ // src/commands/auth/logout.ts
455
+ import pc4 from "picocolors";
456
+ async function logoutAction() {
457
+ if (!isLoggedIn()) {
458
+ console.log(pc4.dim("Not logged in."));
459
+ return;
460
+ }
461
+ const deleted = deleteCredentials();
462
+ if (deleted) {
463
+ console.log(pc4.green("\u2713") + " Logged out successfully.");
464
+ } else {
465
+ console.log(pc4.dim("No credentials to remove."));
466
+ }
467
+ if (process.env.PROMTIE_TOKEN) {
468
+ console.log("");
469
+ console.log(
470
+ pc4.yellow("Note:") + " PROMTIE_TOKEN environment variable is still set."
471
+ );
472
+ console.log(pc4.dim("Unset it with: unset PROMTIE_TOKEN"));
473
+ }
474
+ }
475
+
476
+ // src/commands/auth/whoami.ts
477
+ import pc5 from "picocolors";
478
+ async function whoamiAction() {
479
+ if (!isLoggedIn()) {
480
+ throw new AuthError(
481
+ "Not logged in",
482
+ "Run 'pnote auth login' to authenticate"
483
+ );
484
+ }
485
+ const creds = loadCredentials();
486
+ if (!creds) {
487
+ throw new AuthError("No credentials found");
488
+ }
489
+ try {
490
+ const result = await callMCP("promptnote_list_notes", {
491
+ limit: 1
492
+ });
493
+ console.log(pc5.green("\u2713") + " Authenticated");
494
+ console.log("");
495
+ if (creds.email) {
496
+ console.log(pc5.dim("Email: ") + creds.email);
497
+ }
498
+ console.log(pc5.dim("Token: ") + creds.token.slice(0, 7) + "..." + creds.token.slice(-4));
499
+ console.log(pc5.dim("Stored: ") + getCredentialsPath());
500
+ if (process.env.PROMTIE_TOKEN) {
501
+ console.log(pc5.dim("Source: ") + "PROMTIE_TOKEN environment variable");
502
+ } else {
503
+ console.log(pc5.dim("Source: ") + "credentials file");
504
+ }
505
+ console.log("");
506
+ console.log(pc5.dim(`Total notes: ${result.count}`));
507
+ } catch (error) {
508
+ handleError(error);
509
+ }
510
+ }
511
+
512
+ // src/commands/auth/index.ts
513
+ var authCommand = new Command("auth").description("Manage authentication").addHelpText(
514
+ "after",
515
+ `
516
+ Examples:
517
+ $ pnote auth login Open browser to login
518
+ $ pnote auth token pn_xxx Set token directly
519
+ $ pnote auth whoami Show current user
520
+ $ pnote auth logout Remove credentials
521
+ `
522
+ );
523
+ authCommand.command("login").description("Login to PromptNote (opens browser)").action(loginAction);
524
+ authCommand.command("token").description("Set API token directly").argument("<token>", "Personal Access Token (pn_xxx)").action(tokenAction);
525
+ authCommand.command("logout").description("Remove stored credentials").action(logoutAction);
526
+ authCommand.command("whoami").description("Show current authenticated user").action(whoamiAction);
527
+
528
+ // src/commands/notes/index.ts
529
+ import { Command as Command2 } from "commander";
530
+
531
+ // src/lib/output.ts
532
+ import pc6 from "picocolors";
533
+ function shouldUseColor(ctx) {
534
+ if (ctx.noColor) return false;
535
+ if (process.env.NO_COLOR) return false;
536
+ if (process.env.TERM === "dumb") return false;
537
+ return process.stdout.isTTY ?? false;
538
+ }
539
+ function isHumanOutput(ctx) {
540
+ if (ctx.json) return false;
541
+ if (ctx.plain) return false;
542
+ return process.stdout.isTTY ?? false;
543
+ }
544
+ function getColors(ctx) {
545
+ const useColor = shouldUseColor(ctx);
546
+ return {
547
+ dim: useColor ? pc6.dim : (s) => s,
548
+ bold: useColor ? pc6.bold : (s) => s,
549
+ green: useColor ? pc6.green : (s) => s,
550
+ yellow: useColor ? pc6.yellow : (s) => s,
551
+ blue: useColor ? pc6.blue : (s) => s,
552
+ cyan: useColor ? pc6.cyan : (s) => s,
553
+ red: useColor ? pc6.red : (s) => s,
554
+ magenta: useColor ? pc6.magenta : (s) => s
555
+ };
556
+ }
557
+ function formatRelativeTime(dateStr) {
558
+ const date = new Date(dateStr);
559
+ const now = /* @__PURE__ */ new Date();
560
+ const diffMs = now.getTime() - date.getTime();
561
+ const diffSecs = Math.floor(diffMs / 1e3);
562
+ const diffMins = Math.floor(diffSecs / 60);
563
+ const diffHours = Math.floor(diffMins / 60);
564
+ const diffDays = Math.floor(diffHours / 24);
565
+ if (diffDays > 30) {
566
+ return date.toLocaleDateString();
567
+ }
568
+ if (diffDays > 0) {
569
+ return `${diffDays}d ago`;
570
+ }
571
+ if (diffHours > 0) {
572
+ return `${diffHours}h ago`;
573
+ }
574
+ if (diffMins > 0) {
575
+ return `${diffMins}m ago`;
576
+ }
577
+ return "just now";
578
+ }
579
+ function truncate(str, maxLen) {
580
+ if (str.length <= maxLen) return str;
581
+ return str.slice(0, maxLen - 1) + "\u2026";
582
+ }
583
+ function pad(str, width) {
584
+ if (str.length >= width) return str.slice(0, width);
585
+ return str + " ".repeat(width - str.length);
586
+ }
587
+ function outputJson(data) {
588
+ console.log(JSON.stringify(data, null, 2));
589
+ }
590
+ function outputNotes(notes, ctx) {
591
+ if (ctx.json) {
592
+ outputJson(notes);
593
+ return;
594
+ }
595
+ if (notes.length === 0) {
596
+ const c2 = getColors(ctx);
597
+ console.log(c2.dim("No notes found."));
598
+ return;
599
+ }
600
+ const c = getColors(ctx);
601
+ if (isHumanOutput(ctx)) {
602
+ console.log(
603
+ c.dim(pad("ID", 12)) + " " + c.dim(pad("TITLE", 30)) + " " + c.dim(pad("TAGS", 20)) + " " + c.dim("UPDATED")
604
+ );
605
+ for (const note of notes) {
606
+ const id = truncate(note.id.slice(0, 8), 12);
607
+ const title = truncate(note.title || "(untitled)", 30);
608
+ const tags = truncate(note.tags.join(", ") || "-", 20);
609
+ const updated = formatRelativeTime(note.updated_at);
610
+ let line = pad(id, 12) + " " + pad(title, 30) + " " + pad(tags, 20) + " " + updated;
611
+ const indicators = [];
612
+ if (note.pinned) indicators.push(c.yellow("*"));
613
+ if (note.archived) indicators.push(c.dim("[A]"));
614
+ if (note.is_protected) indicators.push(c.magenta("[P]"));
615
+ if (indicators.length > 0) {
616
+ line = indicators.join("") + " " + line;
617
+ }
618
+ console.log(line);
619
+ }
620
+ } else {
621
+ for (const note of notes) {
622
+ console.log(
623
+ [note.id, note.title, note.tags.join(","), note.updated_at].join(" ")
624
+ );
625
+ }
626
+ }
627
+ }
628
+ function outputNote(note, snippet, ctx) {
629
+ if (ctx.json) {
630
+ outputJson({ ...note, latest_snippet: snippet });
631
+ return;
632
+ }
633
+ const c = getColors(ctx);
634
+ if (isHumanOutput(ctx)) {
635
+ console.log(c.bold(note.title || "(untitled)"));
636
+ console.log(c.dim("ID: ") + note.id);
637
+ console.log(c.dim("Tags: ") + (note.tags.length > 0 ? note.tags.join(", ") : "-"));
638
+ console.log(c.dim("Created: ") + new Date(note.created_at).toLocaleString());
639
+ console.log(c.dim("Updated: ") + new Date(note.updated_at).toLocaleString());
640
+ const status = [];
641
+ if (note.pinned) status.push(c.yellow("pinned"));
642
+ if (note.archived) status.push(c.dim("archived"));
643
+ if (note.is_protected) status.push(c.magenta("protected"));
644
+ if (status.length > 0) {
645
+ console.log(c.dim("Status: ") + status.join(", "));
646
+ }
647
+ if (snippet) {
648
+ console.log("");
649
+ console.log(c.dim(`--- Snippet v${snippet.version} ---`));
650
+ console.log(snippet.content);
651
+ } else {
652
+ console.log("");
653
+ console.log(c.dim("(no content)"));
654
+ }
655
+ } else {
656
+ console.log(`Title: ${note.title}`);
657
+ console.log(`ID: ${note.id}`);
658
+ console.log(`Tags: ${note.tags.join(", ")}`);
659
+ if (snippet) {
660
+ console.log("");
661
+ console.log(snippet.content);
662
+ }
663
+ }
664
+ }
665
+ function outputSnippet(snippet, ctx) {
666
+ if (ctx.json) {
667
+ outputJson(snippet);
668
+ return;
669
+ }
670
+ console.log(snippet.content);
671
+ }
672
+ function outputSnippets(snippets, ctx) {
673
+ if (ctx.json) {
674
+ outputJson(snippets);
675
+ return;
676
+ }
677
+ if (snippets.length === 0) {
678
+ const c2 = getColors(ctx);
679
+ console.log(c2.dim("No snippets found."));
680
+ return;
681
+ }
682
+ const c = getColors(ctx);
683
+ if (isHumanOutput(ctx)) {
684
+ for (const snippet of snippets) {
685
+ const header = `v${snippet.version}` + (snippet.favorite ? c.yellow(" \u2605") : "") + c.dim(` (${formatRelativeTime(snippet.created_at)})`);
686
+ console.log(c.bold(header));
687
+ console.log(snippet.content);
688
+ console.log("");
689
+ }
690
+ } else {
691
+ for (const snippet of snippets) {
692
+ console.log(`v${snippet.version} ${snippet.id} ${snippet.content.slice(0, 100)}`);
693
+ }
694
+ }
695
+ }
696
+ function outputTags(tags, hierarchy, ctx) {
697
+ if (ctx.json) {
698
+ outputJson({ tags, hierarchy });
699
+ return;
700
+ }
701
+ if (tags.length === 0) {
702
+ const c2 = getColors(ctx);
703
+ console.log(c2.dim("No tags found."));
704
+ return;
705
+ }
706
+ const c = getColors(ctx);
707
+ if (isHumanOutput(ctx)) {
708
+ let printTree2 = function(tagPath, prefix, isLast) {
709
+ const count = tagCountMap.get(tagPath) || 0;
710
+ const name = tagPath.split("/").pop() || tagPath;
711
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
712
+ console.log(prefix + connector + c.cyan(name) + c.dim(` (${count})`));
713
+ const children = childrenMap.get(tagPath) || [];
714
+ children.sort();
715
+ const newPrefix = prefix + (isLast ? " " : "\u2502 ");
716
+ children.forEach((child, i) => {
717
+ printTree2(child, newPrefix, i === children.length - 1);
718
+ });
719
+ };
720
+ var printTree = printTree2;
721
+ const roots = [];
722
+ const childrenMap = /* @__PURE__ */ new Map();
723
+ for (const tag of tags) {
724
+ const parts = tag.tag.split("/");
725
+ if (parts.length === 1) {
726
+ roots.push(tag.tag);
727
+ } else {
728
+ const parent = parts.slice(0, -1).join("/");
729
+ const children = childrenMap.get(parent) || [];
730
+ children.push(tag.tag);
731
+ childrenMap.set(parent, children);
732
+ }
733
+ }
734
+ roots.sort();
735
+ const tagCountMap = new Map(tags.map((t) => [t.tag, t.count]));
736
+ roots.forEach((root, i) => {
737
+ const count = tagCountMap.get(root) || 0;
738
+ console.log(c.cyan(root) + c.dim(` (${count})`));
739
+ const children = childrenMap.get(root) || [];
740
+ children.sort();
741
+ children.forEach((child, j) => {
742
+ printTree2(child, "", j === children.length - 1);
743
+ });
744
+ });
745
+ } else {
746
+ for (const tag of tags) {
747
+ console.log(`${tag.tag} ${tag.count}`);
748
+ }
749
+ }
750
+ }
751
+ function outputSearchResults(query, notes, snippets, ctx) {
752
+ if (ctx.json) {
753
+ outputJson({ query, notes, snippets });
754
+ return;
755
+ }
756
+ const c = getColors(ctx);
757
+ if (notes.length === 0 && snippets.length === 0) {
758
+ console.log(c.dim(`No results for "${query}"`));
759
+ return;
760
+ }
761
+ if (notes.length > 0) {
762
+ console.log(c.bold(`Notes (${notes.length})`));
763
+ console.log("");
764
+ for (const note of notes) {
765
+ console.log(
766
+ " " + c.cyan(note.id.slice(0, 8)) + " " + note.title + " " + c.dim(note.tags.join(", "))
767
+ );
768
+ }
769
+ console.log("");
770
+ }
771
+ if (snippets.length > 0) {
772
+ console.log(c.bold(`Snippets (${snippets.length})`));
773
+ console.log("");
774
+ for (const snippet of snippets) {
775
+ console.log(
776
+ " " + c.cyan(snippet.note_id.slice(0, 8)) + " " + snippet.note_title + c.dim(` v${snippet.version}`)
777
+ );
778
+ console.log(" " + c.dim(truncate(snippet.content_preview, 60)));
779
+ }
780
+ }
781
+ }
782
+ function outputMessage(message, ctx) {
783
+ if (ctx.json) {
784
+ outputJson({ message });
785
+ return;
786
+ }
787
+ const c = getColors(ctx);
788
+ console.log(c.green("\u2713") + " " + message);
789
+ }
790
+ function logStatus(message) {
791
+ if (process.stderr.isTTY) {
792
+ console.error(pc6.dim(message));
793
+ }
794
+ }
795
+
796
+ // src/commands/notes/list.ts
797
+ async function listNotesAction(options, ctx) {
798
+ try {
799
+ const result = await listNotes(
800
+ {
801
+ tag: options.tag,
802
+ archived: options.archived ?? false,
803
+ pinned: options.pinned,
804
+ deleted: options.deleted ?? false,
805
+ search: options.search,
806
+ limit: options.limit ? parseInt(options.limit, 10) : 50
807
+ },
808
+ { pin: ctx.pin }
809
+ );
810
+ outputNotes(result.notes, ctx);
811
+ if (result.warning && !ctx.json) {
812
+ console.error("");
813
+ console.error(result.warning);
814
+ }
815
+ } catch (error) {
816
+ handleError(error, ctx.noColor);
817
+ }
818
+ }
819
+
820
+ // src/commands/notes/get.ts
821
+ async function getNoteAction(id, ctx) {
822
+ try {
823
+ const result = await getNote(id, { pin: ctx.pin });
824
+ if (!result) {
825
+ throw new NotFoundError("Note", id);
826
+ }
827
+ outputNote(result, result.latest_snippet, ctx);
828
+ if (result.protection_status?.error && !ctx.json) {
829
+ console.error("");
830
+ console.error("Warning: " + result.protection_status.error);
831
+ }
832
+ } catch (error) {
833
+ handleError(error, ctx.noColor);
834
+ }
835
+ }
836
+
837
+ // src/commands/notes/create.ts
838
+ async function createNoteAction(title, options, ctx) {
839
+ try {
840
+ let content = options.content;
841
+ if (!content && !process.stdin.isTTY) {
842
+ const chunks = [];
843
+ for await (const chunk of process.stdin) {
844
+ chunks.push(chunk);
845
+ }
846
+ const stdinContent = Buffer.concat(chunks).toString("utf-8").trim();
847
+ if (stdinContent) {
848
+ content = stdinContent;
849
+ }
850
+ }
851
+ const result = await createNote(
852
+ {
853
+ title,
854
+ tags: options.tag && options.tag.length > 0 ? options.tag : void 0,
855
+ content
856
+ },
857
+ { pin: ctx.pin }
858
+ );
859
+ if (ctx.json) {
860
+ outputJson(result);
861
+ } else {
862
+ outputMessage(`Created note: ${result.note.title} (${result.note.id})`, ctx);
863
+ }
864
+ } catch (error) {
865
+ handleError(error, ctx.noColor);
866
+ }
867
+ }
868
+
869
+ // src/commands/notes/archive.ts
870
+ async function archiveNoteAction(id, options, ctx) {
871
+ try {
872
+ const archived = !options.undo;
873
+ const result = await updateNote(
874
+ id,
875
+ { archived },
876
+ { pin: ctx.pin }
877
+ );
878
+ if (ctx.json) {
879
+ outputJson(result);
880
+ } else {
881
+ const action = archived ? "Archived" : "Unarchived";
882
+ outputMessage(`${action} note: ${result.note.title}`, ctx);
883
+ }
884
+ } catch (error) {
885
+ handleError(error, ctx.noColor);
886
+ }
887
+ }
888
+
889
+ // src/commands/notes/pin.ts
890
+ async function pinNoteAction(id, ctx) {
891
+ try {
892
+ const note = await getNote(id, { pin: ctx.pin });
893
+ const newPinned = !note.pinned;
894
+ const result = await updateNote(
895
+ id,
896
+ { pinned: newPinned },
897
+ { pin: ctx.pin }
898
+ );
899
+ if (ctx.json) {
900
+ outputJson(result);
901
+ } else {
902
+ const action = newPinned ? "Pinned" : "Unpinned";
903
+ outputMessage(`${action} note: ${result.note.title}`, ctx);
904
+ }
905
+ } catch (error) {
906
+ handleError(error, ctx.noColor);
907
+ }
908
+ }
909
+
910
+ // src/commands/notes/delete.ts
911
+ import pc7 from "picocolors";
912
+ import { createInterface } from "readline";
913
+ async function confirm(message) {
914
+ if (!process.stdin.isTTY) {
915
+ return false;
916
+ }
917
+ const rl = createInterface({
918
+ input: process.stdin,
919
+ output: process.stdout
920
+ });
921
+ return new Promise((resolve) => {
922
+ rl.question(message + " [y/N] ", (answer) => {
923
+ rl.close();
924
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
925
+ });
926
+ });
927
+ }
928
+ async function deleteNoteAction(id, options, ctx) {
929
+ try {
930
+ const note = await getNote(id, { pin: ctx.pin });
931
+ if (!options.force && !ctx.json) {
932
+ console.log(`About to delete: ${pc7.bold(note.title)}`);
933
+ const confirmed = await confirm("Are you sure?");
934
+ if (!confirmed) {
935
+ console.log(pc7.dim("Cancelled."));
936
+ return;
937
+ }
938
+ }
939
+ const result = await deleteNote(id, { pin: ctx.pin });
940
+ if (ctx.json) {
941
+ outputJson(result);
942
+ } else {
943
+ outputMessage(`Deleted note: ${note.title}`, ctx);
944
+ }
945
+ } catch (error) {
946
+ handleError(error, ctx.noColor);
947
+ }
948
+ }
949
+
950
+ // src/lib/pin.ts
951
+ import { createInterface as createInterface2 } from "readline";
952
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, statSync } from "fs";
953
+ import { tmpdir } from "os";
954
+ import { join as join2 } from "path";
955
+ import pc8 from "picocolors";
956
+ var PIN_CACHE_TTL_MS = 5 * 60 * 1e3;
957
+ var PIN_MAX_ATTEMPTS = 3;
958
+ function getPinCachePath() {
959
+ const uid = process.getuid?.() ?? "unknown";
960
+ return join2(tmpdir(), `pnote-pin-${uid}`);
961
+ }
962
+ function getCachedPin() {
963
+ const cachePath = getPinCachePath();
964
+ if (!existsSync2(cachePath)) {
965
+ return null;
966
+ }
967
+ try {
968
+ const stat = statSync(cachePath);
969
+ const age = Date.now() - stat.mtimeMs;
970
+ if (age > PIN_CACHE_TTL_MS) {
971
+ unlinkSync2(cachePath);
972
+ return null;
973
+ }
974
+ const content = readFileSync2(cachePath, "utf-8").trim();
975
+ return content || null;
976
+ } catch {
977
+ return null;
978
+ }
979
+ }
980
+ function setCachedPin(pin) {
981
+ const cachePath = getPinCachePath();
982
+ try {
983
+ writeFileSync2(cachePath, pin, { mode: 384 });
984
+ } catch {
985
+ }
986
+ }
987
+ async function readPinFromStdin() {
988
+ const chunks = [];
989
+ for await (const chunk of process.stdin) {
990
+ chunks.push(chunk);
991
+ const content2 = Buffer.concat(chunks).toString("utf-8");
992
+ if (content2.includes("\n")) {
993
+ break;
994
+ }
995
+ }
996
+ const content = Buffer.concat(chunks).toString("utf-8");
997
+ const firstLine = content.split("\n")[0]?.trim() || "";
998
+ if (!firstLine) {
999
+ throw new Error("No PIN provided via stdin");
1000
+ }
1001
+ return firstLine;
1002
+ }
1003
+ async function promptForPin(noteTitle, hint, maxAttempts = PIN_MAX_ATTEMPTS) {
1004
+ if (!process.stdin.isTTY) {
1005
+ return null;
1006
+ }
1007
+ if (noteTitle) {
1008
+ console.error(pc8.yellow("\u{1F512}") + ` Note "${noteTitle}" is PIN-protected.`);
1009
+ } else {
1010
+ console.error(pc8.yellow("\u{1F512}") + " This action requires your PIN.");
1011
+ }
1012
+ if (hint) {
1013
+ console.error(pc8.dim(`Hint: ${hint}`));
1014
+ }
1015
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1016
+ const pin = await readPinHidden(
1017
+ attempt > 1 ? `Enter PIN (${attempt}/${maxAttempts}): ` : "Enter PIN: "
1018
+ );
1019
+ if (pin) {
1020
+ return pin;
1021
+ }
1022
+ if (attempt < maxAttempts) {
1023
+ console.error(pc8.red("Invalid or empty PIN. Try again."));
1024
+ }
1025
+ }
1026
+ console.error(pc8.red("Too many attempts."));
1027
+ return null;
1028
+ }
1029
+ async function readPinHidden(prompt) {
1030
+ return new Promise((resolve) => {
1031
+ const rl = createInterface2({
1032
+ input: process.stdin,
1033
+ output: process.stderr,
1034
+ terminal: true
1035
+ });
1036
+ if (process.stdin.isTTY) {
1037
+ process.stdin.setRawMode?.(true);
1038
+ }
1039
+ process.stderr.write(prompt);
1040
+ let pin = "";
1041
+ const onKeypress = (key) => {
1042
+ const char = key.toString();
1043
+ if (char === "\r" || char === "\n") {
1044
+ process.stderr.write("\n");
1045
+ cleanup();
1046
+ resolve(pin);
1047
+ } else if (char === "") {
1048
+ process.stderr.write("\n");
1049
+ cleanup();
1050
+ process.exit(130);
1051
+ } else if (char === "\x7F" || char === "\b") {
1052
+ if (pin.length > 0) {
1053
+ pin = pin.slice(0, -1);
1054
+ process.stderr.write("\b \b");
1055
+ }
1056
+ } else if (char.length === 1 && char >= " ") {
1057
+ pin += char;
1058
+ process.stderr.write("*");
1059
+ }
1060
+ };
1061
+ const cleanup = () => {
1062
+ process.stdin.removeListener("data", onKeypress);
1063
+ if (process.stdin.isTTY) {
1064
+ process.stdin.setRawMode?.(false);
1065
+ }
1066
+ rl.close();
1067
+ };
1068
+ process.stdin.on("data", onKeypress);
1069
+ });
1070
+ }
1071
+ async function resolvePin(options) {
1072
+ const { pinArg, pinFromStdin, noteTitle, hint, skipCache, skipPrompt } = options;
1073
+ if (pinArg) {
1074
+ return pinArg;
1075
+ }
1076
+ if (pinFromStdin) {
1077
+ try {
1078
+ return await readPinFromStdin();
1079
+ } catch {
1080
+ return null;
1081
+ }
1082
+ }
1083
+ const envPin = process.env.PNOTE_PIN;
1084
+ if (envPin) {
1085
+ return envPin;
1086
+ }
1087
+ if (!skipCache) {
1088
+ const cached = getCachedPin();
1089
+ if (cached) {
1090
+ return cached;
1091
+ }
1092
+ }
1093
+ if (!skipPrompt && process.stdin.isTTY) {
1094
+ const pin = await promptForPin(noteTitle, hint);
1095
+ if (pin) {
1096
+ setCachedPin(pin);
1097
+ return pin;
1098
+ }
1099
+ }
1100
+ return null;
1101
+ }
1102
+
1103
+ // src/commands/notes/index.ts
1104
+ async function buildContext(globalOpts) {
1105
+ const pin = await resolvePin({
1106
+ pinArg: globalOpts.pin,
1107
+ pinFromStdin: globalOpts.pinStdin,
1108
+ skipPrompt: true
1109
+ // Don't prompt interactively for list/search operations
1110
+ });
1111
+ return {
1112
+ json: globalOpts.json ?? false,
1113
+ noColor: globalOpts.noColor ?? false,
1114
+ plain: globalOpts.plain ?? false,
1115
+ pin: pin ?? void 0
1116
+ };
1117
+ }
1118
+ var notesCommand = new Command2("notes").description("List and manage notes").option("--tag <tag>", 'Filter by tag (e.g., "AI/art")').option("--archived", "Show archived notes").option("--pinned", "Show only pinned notes").option("--deleted", "Show deleted notes").option("--search <query>", "Search notes by title").option("--limit <n>", "Limit number of results", "50").action(async (options, cmd) => {
1119
+ const globalOpts = cmd.parent?.opts() || {};
1120
+ const ctx = await buildContext(globalOpts);
1121
+ await listNotesAction(options, ctx);
1122
+ }).addHelpText(
1123
+ "after",
1124
+ `
1125
+ Examples:
1126
+ $ pnote notes List all active notes
1127
+ $ pnote notes --tag "AI/art" Filter by tag
1128
+ $ pnote notes --archived Show archived notes
1129
+ $ pnote notes --pinned Show only pinned notes
1130
+ $ pnote notes get abc123 Get note details
1131
+ $ pnote notes create "My Note" Create a new note
1132
+ `
1133
+ );
1134
+ notesCommand.command("get").description("Get a note with its latest snippet").argument("<id>", "Note ID").action(async (id, _options, cmd) => {
1135
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1136
+ const ctx = await buildContext(globalOpts);
1137
+ await getNoteAction(id, ctx);
1138
+ });
1139
+ notesCommand.command("create").description("Create a new note").argument("<title>", "Note title").option("--tag <tags...>", "Tags for the note").option("--content <content>", "Initial snippet content").action(async (title, options, cmd) => {
1140
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1141
+ const ctx = await buildContext(globalOpts);
1142
+ await createNoteAction(title, options, ctx);
1143
+ });
1144
+ notesCommand.command("archive").description("Archive or unarchive a note").argument("<id>", "Note ID").option("--undo", "Unarchive the note").action(async (id, options, cmd) => {
1145
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1146
+ const ctx = await buildContext(globalOpts);
1147
+ await archiveNoteAction(id, options, ctx);
1148
+ });
1149
+ notesCommand.command("pin").description("Pin or unpin a note").argument("<id>", "Note ID").action(async (id, _options, cmd) => {
1150
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1151
+ const ctx = await buildContext(globalOpts);
1152
+ await pinNoteAction(id, ctx);
1153
+ });
1154
+ notesCommand.command("delete").description("Delete a note (soft delete)").argument("<id>", "Note ID").option("--force", "Skip confirmation").action(async (id, options, cmd) => {
1155
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1156
+ const ctx = await buildContext(globalOpts);
1157
+ await deleteNoteAction(id, options, ctx);
1158
+ });
1159
+
1160
+ // src/commands/snippet/index.ts
1161
+ import { Command as Command3 } from "commander";
1162
+
1163
+ // src/commands/snippet/show.ts
1164
+ async function showSnippetAction(noteId, options, ctx) {
1165
+ try {
1166
+ if (options.all) {
1167
+ const result = await listSnippets(
1168
+ { note_id: noteId, limit: 100 },
1169
+ { pin: ctx.pin }
1170
+ );
1171
+ outputSnippets(result.snippets, ctx);
1172
+ } else {
1173
+ const result = await getNote(noteId, { pin: ctx.pin });
1174
+ if (result.latest_snippet) {
1175
+ outputSnippet(result.latest_snippet, ctx);
1176
+ } else {
1177
+ if (!ctx.json) {
1178
+ console.error("No snippet content found.");
1179
+ }
1180
+ process.exit(3);
1181
+ }
1182
+ }
1183
+ } catch (error) {
1184
+ handleError(error, ctx.noColor);
1185
+ }
1186
+ }
1187
+
1188
+ // src/commands/snippet/copy.ts
1189
+ import pc9 from "picocolors";
1190
+
1191
+ // src/lib/clipboard.ts
1192
+ import { default as clipboardy } from "clipboardy";
1193
+ async function copyToClipboard(text) {
1194
+ try {
1195
+ await clipboardy.write(text);
1196
+ } catch (error) {
1197
+ if (error instanceof Error && error.message.includes("xsel")) {
1198
+ throw new CLIError(
1199
+ "Clipboard not available. Install xsel or xclip on Linux.",
1200
+ 1,
1201
+ "Content will be printed to stdout instead."
1202
+ );
1203
+ }
1204
+ throw new CLIError("Failed to copy to clipboard");
1205
+ }
1206
+ }
1207
+
1208
+ // src/commands/snippet/copy.ts
1209
+ async function copySnippetAction(noteId, options, ctx) {
1210
+ try {
1211
+ let snippet = null;
1212
+ if (options.version) {
1213
+ const versionNum = parseInt(options.version, 10);
1214
+ if (isNaN(versionNum)) {
1215
+ throw new CLIError("Invalid version number");
1216
+ }
1217
+ const result = await listSnippets(
1218
+ { note_id: noteId, limit: 100 },
1219
+ { pin: ctx.pin }
1220
+ );
1221
+ snippet = result.snippets.find((s) => s.version === versionNum) || null;
1222
+ if (!snippet) {
1223
+ throw new CLIError(`Version ${versionNum} not found`);
1224
+ }
1225
+ } else {
1226
+ const result = await getNote(noteId, { pin: ctx.pin });
1227
+ snippet = result.latest_snippet;
1228
+ }
1229
+ if (!snippet) {
1230
+ throw new CLIError("No snippet content found");
1231
+ }
1232
+ try {
1233
+ await copyToClipboard(snippet.content);
1234
+ logStatus("Copied to clipboard");
1235
+ if (ctx.json) {
1236
+ console.log(JSON.stringify({ copied: true, content: snippet.content }));
1237
+ }
1238
+ } catch (clipboardError) {
1239
+ if (!ctx.json) {
1240
+ console.error(pc9.yellow("Clipboard not available, printing to stdout:"));
1241
+ console.error("");
1242
+ }
1243
+ console.log(snippet.content);
1244
+ }
1245
+ } catch (error) {
1246
+ handleError(error, ctx.noColor);
1247
+ }
1248
+ }
1249
+
1250
+ // src/commands/snippet/add.ts
1251
+ async function addSnippetAction(noteId, options, ctx) {
1252
+ try {
1253
+ let content;
1254
+ if (process.stdin.isTTY) {
1255
+ throw new CLIError(
1256
+ "No content provided",
1257
+ 1,
1258
+ "Pipe content to this command: echo 'content' | pnote snippet add <note-id>"
1259
+ );
1260
+ }
1261
+ const chunks = [];
1262
+ for await (const chunk of process.stdin) {
1263
+ chunks.push(chunk);
1264
+ }
1265
+ content = Buffer.concat(chunks).toString("utf-8");
1266
+ if (content.endsWith("\n")) {
1267
+ content = content.slice(0, -1);
1268
+ }
1269
+ if (!content) {
1270
+ throw new CLIError("Empty content provided");
1271
+ }
1272
+ const result = await createSnippet(
1273
+ {
1274
+ note_id: noteId,
1275
+ content,
1276
+ title: options.title
1277
+ },
1278
+ { pin: ctx.pin }
1279
+ );
1280
+ if (ctx.json) {
1281
+ outputJson(result);
1282
+ } else {
1283
+ outputMessage(`Created snippet v${result.snippet.version}`, ctx);
1284
+ }
1285
+ } catch (error) {
1286
+ handleError(error, ctx.noColor);
1287
+ }
1288
+ }
1289
+
1290
+ // src/commands/snippet/favorite.ts
1291
+ async function favoriteSnippetAction(snippetId, ctx) {
1292
+ try {
1293
+ const result = await toggleFavorite(snippetId, { pin: ctx.pin });
1294
+ if (ctx.json) {
1295
+ outputJson(result);
1296
+ } else {
1297
+ const status = result.snippet.favorite ? "favorited" : "unfavorited";
1298
+ outputMessage(`Snippet ${status}`, ctx);
1299
+ }
1300
+ } catch (error) {
1301
+ handleError(error, ctx.noColor);
1302
+ }
1303
+ }
1304
+
1305
+ // src/commands/snippet/index.ts
1306
+ async function buildContext2(globalOpts) {
1307
+ const pin = await resolvePin({
1308
+ pinArg: globalOpts.pin,
1309
+ pinFromStdin: globalOpts.pinStdin,
1310
+ skipPrompt: true
1311
+ });
1312
+ return {
1313
+ json: globalOpts.json ?? false,
1314
+ noColor: globalOpts.noColor ?? false,
1315
+ plain: globalOpts.plain ?? false,
1316
+ pin: pin ?? void 0
1317
+ };
1318
+ }
1319
+ var snippetCommand = new Command3("snippet").description("Read and manage snippets (note versions)").argument("[note-id]", "Note ID to show snippet for").option("--all", "Show all snippet versions").action(async (noteId, options, cmd) => {
1320
+ const globalOpts = cmd.parent?.opts() || {};
1321
+ const ctx = await buildContext2(globalOpts);
1322
+ if (noteId) {
1323
+ await showSnippetAction(noteId, options, ctx);
1324
+ } else {
1325
+ cmd.help();
1326
+ }
1327
+ }).addHelpText(
1328
+ "after",
1329
+ `
1330
+ Examples:
1331
+ $ pnote snippet abc123 Show latest snippet
1332
+ $ pnote snippet abc123 --all Show all versions
1333
+ $ pnote snippet copy abc123 Copy to clipboard
1334
+ $ cat file.txt | pnote snippet add abc123
1335
+ `
1336
+ );
1337
+ snippetCommand.command("copy").description("Copy snippet content to clipboard").argument("<note-id>", "Note ID").option("--version <n>", "Specific version number").action(async (noteId, options, cmd) => {
1338
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1339
+ const ctx = await buildContext2(globalOpts);
1340
+ await copySnippetAction(noteId, options, ctx);
1341
+ });
1342
+ snippetCommand.command("add").description("Add a new snippet version (from stdin)").argument("<note-id>", "Note ID").option("--title <title>", "Snippet title").action(async (noteId, options, cmd) => {
1343
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1344
+ const ctx = await buildContext2(globalOpts);
1345
+ await addSnippetAction(noteId, options, ctx);
1346
+ });
1347
+ snippetCommand.command("favorite").description("Toggle favorite status on a snippet").argument("<snippet-id>", "Snippet ID").action(async (snippetId, _options, cmd) => {
1348
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1349
+ const ctx = await buildContext2(globalOpts);
1350
+ await favoriteSnippetAction(snippetId, ctx);
1351
+ });
1352
+
1353
+ // src/commands/tags/index.ts
1354
+ import { Command as Command4 } from "commander";
1355
+
1356
+ // src/commands/tags/list.ts
1357
+ async function listTagsAction(options, ctx) {
1358
+ try {
1359
+ const result = await listTags(
1360
+ { include_archived: options.includeArchived ?? false },
1361
+ { pin: ctx.pin }
1362
+ );
1363
+ outputTags(result.tags, result.hierarchy, ctx);
1364
+ } catch (error) {
1365
+ handleError(error, ctx.noColor);
1366
+ }
1367
+ }
1368
+
1369
+ // src/commands/tags/rename.ts
1370
+ async function renameTagAction(oldTag, newTag, ctx) {
1371
+ try {
1372
+ const result = await renameTag(
1373
+ { old_tag: oldTag, new_tag: newTag },
1374
+ { pin: ctx.pin }
1375
+ );
1376
+ if (ctx.json) {
1377
+ outputJson(result);
1378
+ } else {
1379
+ outputMessage(
1380
+ `Renamed "${oldTag}" to "${newTag}" (${result.notes_updated} notes updated)`,
1381
+ ctx
1382
+ );
1383
+ }
1384
+ } catch (error) {
1385
+ handleError(error, ctx.noColor);
1386
+ }
1387
+ }
1388
+
1389
+ // src/commands/tags/merge.ts
1390
+ async function mergeTagsAction(sourceTags, targetTag, ctx) {
1391
+ try {
1392
+ if (sourceTags.includes(targetTag)) {
1393
+ throw new CLIError("Source tags cannot include the target tag");
1394
+ }
1395
+ if (sourceTags.length === 0) {
1396
+ throw new CLIError("At least one source tag is required");
1397
+ }
1398
+ const result = await mergeTags(
1399
+ { source_tags: sourceTags, target_tag: targetTag },
1400
+ { pin: ctx.pin }
1401
+ );
1402
+ if (ctx.json) {
1403
+ outputJson(result);
1404
+ } else {
1405
+ const sourceList = sourceTags.map((t) => `"${t}"`).join(", ");
1406
+ outputMessage(
1407
+ `Merged ${sourceList} into "${targetTag}" (${result.notes_updated} notes updated)`,
1408
+ ctx
1409
+ );
1410
+ }
1411
+ } catch (error) {
1412
+ handleError(error, ctx.noColor);
1413
+ }
1414
+ }
1415
+
1416
+ // src/commands/tags/index.ts
1417
+ async function buildContext3(globalOpts) {
1418
+ const pin = await resolvePin({
1419
+ pinArg: globalOpts.pin,
1420
+ pinFromStdin: globalOpts.pinStdin,
1421
+ skipPrompt: true
1422
+ });
1423
+ return {
1424
+ json: globalOpts.json ?? false,
1425
+ noColor: globalOpts.noColor ?? false,
1426
+ plain: globalOpts.plain ?? false,
1427
+ pin: pin ?? void 0
1428
+ };
1429
+ }
1430
+ var tagsCommand = new Command4("tags").description("List and manage tags").option("--include-archived", "Include tags from archived notes").action(async (options, cmd) => {
1431
+ const globalOpts = cmd.parent?.opts() || {};
1432
+ const ctx = await buildContext3(globalOpts);
1433
+ await listTagsAction(options, ctx);
1434
+ }).addHelpText(
1435
+ "after",
1436
+ `
1437
+ Examples:
1438
+ $ pnote tags List all tags
1439
+ $ pnote tags rename "old" "new" Rename a tag
1440
+ $ pnote tags merge "tag1" "tag2" --into "target"
1441
+ `
1442
+ );
1443
+ tagsCommand.command("rename").description("Rename a tag across all notes").argument("<old-tag>", "Current tag name").argument("<new-tag>", "New tag name").action(async (oldTag, newTag, _options, cmd) => {
1444
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1445
+ const ctx = await buildContext3(globalOpts);
1446
+ await renameTagAction(oldTag, newTag, ctx);
1447
+ });
1448
+ tagsCommand.command("merge").description("Merge multiple tags into one").argument("<source-tags...>", "Tags to merge (space-separated)").requiredOption("--into <target>", "Target tag to merge into").action(async (sourceTags, options, cmd) => {
1449
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1450
+ const ctx = await buildContext3(globalOpts);
1451
+ await mergeTagsAction(sourceTags, options.into, ctx);
1452
+ });
1453
+
1454
+ // src/commands/search.ts
1455
+ import { Command as Command5 } from "commander";
1456
+ async function searchAction(query, options, ctx) {
1457
+ try {
1458
+ let search_notes = true;
1459
+ let search_snippets = true;
1460
+ if (options.notesOnly) {
1461
+ search_notes = true;
1462
+ search_snippets = false;
1463
+ } else if (options.snippetsOnly) {
1464
+ search_notes = false;
1465
+ search_snippets = true;
1466
+ }
1467
+ const result = await search(
1468
+ {
1469
+ query,
1470
+ search_notes,
1471
+ search_snippets,
1472
+ limit: options.limit ? parseInt(options.limit, 10) : 20
1473
+ },
1474
+ { pin: ctx.pin }
1475
+ );
1476
+ outputSearchResults(query, result.notes, result.snippets, ctx);
1477
+ } catch (error) {
1478
+ handleError(error, ctx.noColor);
1479
+ }
1480
+ }
1481
+ var searchCommand = new Command5("search").description("Search notes and snippets").argument("<query>", "Search query").option("--notes-only", "Only search note titles and tags").option("--snippets-only", "Only search snippet content").option("--limit <n>", "Limit results", "20").action(async (query, options, cmd) => {
1482
+ const globalOpts = cmd.parent?.opts() || {};
1483
+ const ctx = {
1484
+ json: globalOpts.json ?? false,
1485
+ noColor: globalOpts.noColor ?? false,
1486
+ plain: globalOpts.plain ?? false,
1487
+ pin: globalOpts.pin
1488
+ };
1489
+ await searchAction(query, options, ctx);
1490
+ }).addHelpText(
1491
+ "after",
1492
+ `
1493
+ Examples:
1494
+ $ pnote search "portrait" Search all
1495
+ $ pnote search "AI" --notes-only Search note titles only
1496
+ $ pnote search "code" --limit 50 Limit results
1497
+ `
1498
+ );
1499
+
1500
+ // src/index.ts
1501
+ var program = new Command6();
1502
+ program.name("pnote").description("pnote - The PromptNote CLI").version("0.1.0", "-V, --version", "Show version number").option("--json", "Output as JSON (for scripting)").option("--no-color", "Disable colored output").option("--plain", "Force plain text output (no formatting)").option("-p, --pin <pin>", "PIN for accessing protected notes").option("--pin-stdin", "Read PIN from stdin (first line only)").configureHelp({
1503
+ sortSubcommands: true,
1504
+ sortOptions: true
1505
+ }).addHelpText(
1506
+ "after",
1507
+ `
1508
+ PIN Protection:
1509
+ Protected notes require a PIN to access their content.
1510
+ You can provide the PIN in several ways:
1511
+ -p, --pin <pin> Pass PIN directly (visible in history)
1512
+ --pin-stdin Read PIN from stdin
1513
+ PNOTE_PIN Set as environment variable (recommended)
1514
+
1515
+ Examples:
1516
+ $ pnote notes List all notes
1517
+ $ pnote notes --tag "AI/art" Filter notes by tag
1518
+ $ pnote notes get abc123 -p 1234 Get protected note with PIN
1519
+ $ export PNOTE_PIN=1234 && pnote notes
1520
+ $ pnote snippet copy abc123 Copy snippet to clipboard
1521
+ $ pnote search "portrait" Search notes and snippets
1522
+
1523
+ Documentation: https://promptnoteapp.com/docs
1524
+ `
1525
+ );
1526
+ program.addCommand(authCommand);
1527
+ program.addCommand(notesCommand);
1528
+ program.addCommand(snippetCommand);
1529
+ program.addCommand(tagsCommand);
1530
+ program.addCommand(searchCommand);
1531
+ process.on("SIGINT", () => {
1532
+ console.error("\nInterrupted");
1533
+ process.exit(130);
1534
+ });
1535
+ process.on("SIGTERM", () => {
1536
+ process.exit(0);
1537
+ });
1538
+ process.stdout.on("error", (err) => {
1539
+ if (err.code === "EPIPE") {
1540
+ process.exit(0);
1541
+ }
1542
+ throw err;
1543
+ });
1544
+ program.parseAsync(process.argv).catch((error) => {
1545
+ const noColor = program.opts().noColor ?? process.env.NO_COLOR !== void 0;
1546
+ handleError(error, noColor);
1547
+ });
1548
+ //# sourceMappingURL=index.js.map