pnote 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command6 } from "commander";
4
+ import { Command as Command8 } from "commander";
5
5
 
6
6
  // src/lib/errors.ts
7
7
  import pc from "picocolors";
@@ -105,12 +105,12 @@ function loadCredentials() {
105
105
  created_at: (/* @__PURE__ */ new Date()).toISOString()
106
106
  };
107
107
  }
108
- const path = getCredentialsPath();
109
- if (!existsSync(path)) {
108
+ const path3 = getCredentialsPath();
109
+ if (!existsSync(path3)) {
110
110
  return null;
111
111
  }
112
112
  try {
113
- const content = readFileSync(path, "utf-8");
113
+ const content = readFileSync(path3, "utf-8");
114
114
  return JSON.parse(content);
115
115
  } catch {
116
116
  return null;
@@ -118,14 +118,14 @@ function loadCredentials() {
118
118
  }
119
119
  function saveCredentials(credentials) {
120
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);
121
+ const path3 = join(dir, CREDENTIALS_FILE);
122
+ writeFileSync(path3, JSON.stringify(credentials, null, 2), "utf-8");
123
+ chmodSync(path3, 384);
124
124
  }
125
125
  function deleteCredentials() {
126
- const path = getCredentialsPath();
127
- if (existsSync(path)) {
128
- unlinkSync(path);
126
+ const path3 = getCredentialsPath();
127
+ if (existsSync(path3)) {
128
+ unlinkSync(path3);
129
129
  return true;
130
130
  }
131
131
  return false;
@@ -142,12 +142,12 @@ function validateTokenFormat(token) {
142
142
  }
143
143
  var CONFIG_FILE = "config.json";
144
144
  function loadConfig() {
145
- const path = join(getConfigDir(), CONFIG_FILE);
146
- if (!existsSync(path)) {
145
+ const path3 = join(getConfigDir(), CONFIG_FILE);
146
+ if (!existsSync(path3)) {
147
147
  return {};
148
148
  }
149
149
  try {
150
- const content = readFileSync(path, "utf-8");
150
+ const content = readFileSync(path3, "utf-8");
151
151
  return JSON.parse(content);
152
152
  } catch {
153
153
  return {};
@@ -178,7 +178,7 @@ async function loginAction() {
178
178
  console.log(" 3. Generate a new token");
179
179
  console.log(" 4. Run: " + pc2.cyan("pnote auth token <your-token>"));
180
180
  console.log("");
181
- console.log(pc2.dim("Or set the PROMTIE_TOKEN environment variable."));
181
+ console.log(pc2.dim("Or set the PNOTE_TOKEN environment variable."));
182
182
  }
183
183
 
184
184
  // src/commands/auth/token.ts
@@ -189,7 +189,7 @@ function getRestApiBase() {
189
189
  const mcpEndpoint = getApiEndpoint();
190
190
  return mcpEndpoint.replace(/\/mcp$/, "/v1");
191
191
  }
192
- async function callRestApi(method, path, body, options = {}) {
192
+ async function callRestApi(method, path3, body, options = {}) {
193
193
  const token = getToken();
194
194
  if (!token) {
195
195
  throw new AuthError(
@@ -198,7 +198,7 @@ async function callRestApi(method, path, body, options = {}) {
198
198
  );
199
199
  }
200
200
  const baseUrl = getRestApiBase();
201
- const url = `${baseUrl}${path}`;
201
+ const url = `${baseUrl}${path3}`;
202
202
  const headers = {
203
203
  "Content-Type": "application/json",
204
204
  Authorization: `Bearer ${token}`
@@ -256,6 +256,7 @@ async function listNotes(params = {}, options = {}) {
256
256
  if (params.pinned !== void 0) query.set("pinned", String(params.pinned));
257
257
  if (params.deleted !== void 0) query.set("deleted", String(params.deleted));
258
258
  if (params.protected !== void 0) query.set("protected", String(params.protected));
259
+ if (params.note_type) query.set("note_type", params.note_type);
259
260
  if (params.search) query.set("search", params.search);
260
261
  if (params.limit) query.set("limit", String(params.limit));
261
262
  if (params.offset) query.set("offset", String(params.offset));
@@ -284,6 +285,9 @@ async function listSnippets(params, options = {}) {
284
285
  async function createSnippet(data, options = {}) {
285
286
  return callRestApi("POST", "/snippets", data, options);
286
287
  }
288
+ async function updateSnippet(id, data, options = {}) {
289
+ return callRestApi("PATCH", `/snippets/${id}`, data, options);
290
+ }
287
291
  async function toggleFavorite(id, options = {}) {
288
292
  return callRestApi("POST", `/snippets/${id}/favorite`, {}, options);
289
293
  }
@@ -307,93 +311,20 @@ async function search(params, options = {}) {
307
311
  if (params.limit) query.set("limit", String(params.limit));
308
312
  return callRestApi("GET", `/search?${query.toString()}`, void 0, options);
309
313
  }
310
- var requestId = 0;
311
- function getNextId() {
312
- return ++requestId;
314
+ async function listSharedTags(options = {}) {
315
+ return callRestApi("GET", "/share/tags", void 0, options);
313
316
  }
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
- }
317
+ async function getSharedTagNotes(id, params = {}, options = {}) {
318
+ const query = new URLSearchParams();
319
+ if (params.limit) query.set("limit", String(params.limit));
320
+ const queryString = query.toString();
321
+ return callRestApi("GET", `/share/tags/${id}/notes${queryString ? `?${queryString}` : ""}`, void 0, options);
322
+ }
323
+ async function listSharedNotes(params = {}, options = {}) {
324
+ const query = new URLSearchParams();
325
+ if (params.include_revoked !== void 0) query.set("include_revoked", String(params.include_revoked));
326
+ const queryString = query.toString();
327
+ return callRestApi("GET", `/share/notes${queryString ? `?${queryString}` : ""}`, void 0, options);
397
328
  }
398
329
  async function verifyToken(token) {
399
330
  const baseUrl = getApiEndpoint().replace(/\/mcp$/, "/v1");
@@ -464,12 +395,12 @@ async function logoutAction() {
464
395
  } else {
465
396
  console.log(pc4.dim("No credentials to remove."));
466
397
  }
467
- if (process.env.PROMTIE_TOKEN) {
398
+ if (process.env.PNOTE_TOKEN) {
468
399
  console.log("");
469
400
  console.log(
470
- pc4.yellow("Note:") + " PROMTIE_TOKEN environment variable is still set."
401
+ pc4.yellow("Note:") + " PNOTE_TOKEN environment variable is still set."
471
402
  );
472
- console.log(pc4.dim("Unset it with: unset PROMTIE_TOKEN"));
403
+ console.log(pc4.dim("Unset it with: unset PNOTE_TOKEN"));
473
404
  }
474
405
  }
475
406
 
@@ -487,9 +418,7 @@ async function whoamiAction() {
487
418
  throw new AuthError("No credentials found");
488
419
  }
489
420
  try {
490
- const result = await callMCP("promptnote_list_notes", {
491
- limit: 1
492
- });
421
+ const result = await listNotes({ limit: 1 });
493
422
  console.log(pc5.green("\u2713") + " Authenticated");
494
423
  console.log("");
495
424
  if (creds.email) {
@@ -497,8 +426,8 @@ async function whoamiAction() {
497
426
  }
498
427
  console.log(pc5.dim("Token: ") + creds.token.slice(0, 7) + "..." + creds.token.slice(-4));
499
428
  console.log(pc5.dim("Stored: ") + getCredentialsPath());
500
- if (process.env.PROMTIE_TOKEN) {
501
- console.log(pc5.dim("Source: ") + "PROMTIE_TOKEN environment variable");
429
+ if (process.env.PNOTE_TOKEN) {
430
+ console.log(pc5.dim("Source: ") + "PNOTE_TOKEN environment variable");
502
431
  } else {
503
432
  console.log(pc5.dim("Source: ") + "credentials file");
504
433
  }
@@ -779,6 +708,103 @@ function outputSearchResults(query, notes, snippets, ctx) {
779
708
  }
780
709
  }
781
710
  }
711
+ function outputSharedTags(data, ctx) {
712
+ if (ctx.json) {
713
+ outputJson(data);
714
+ return;
715
+ }
716
+ const c = getColors(ctx);
717
+ const totalCount = data.owned.count + data.shared_with_me.count;
718
+ if (totalCount === 0) {
719
+ console.log(c.dim("No shared tags."));
720
+ return;
721
+ }
722
+ if (isHumanOutput(ctx)) {
723
+ if (data.owned.count > 0) {
724
+ console.log(c.bold(`Owned (${data.owned.count})`));
725
+ console.log("");
726
+ for (const tag of data.owned.tags) {
727
+ console.log(
728
+ " " + c.cyan(tag.id.slice(0, 8)) + " " + pad(tag.tag_path, 24) + " " + c.dim(formatRelativeTime(tag.created_at))
729
+ );
730
+ }
731
+ console.log("");
732
+ }
733
+ if (data.shared_with_me.count > 0) {
734
+ console.log(c.bold(`Shared with me (${data.shared_with_me.count})`));
735
+ console.log("");
736
+ for (const tag of data.shared_with_me.tags) {
737
+ console.log(
738
+ " " + c.cyan(tag.id.slice(0, 8)) + " " + pad(tag.tag_path, 24) + " " + c.dim("joined " + formatRelativeTime(tag.joined_at))
739
+ );
740
+ }
741
+ }
742
+ } else {
743
+ for (const tag of data.owned.tags) {
744
+ console.log(["owned", tag.id, tag.tag_path, tag.created_at].join(" "));
745
+ }
746
+ for (const tag of data.shared_with_me.tags) {
747
+ console.log(["shared", tag.id, tag.tag_path, tag.joined_at].join(" "));
748
+ }
749
+ }
750
+ }
751
+ function outputSharedTagNotes(data, ctx) {
752
+ if (ctx.json) {
753
+ outputJson(data);
754
+ return;
755
+ }
756
+ const c = getColors(ctx);
757
+ if (isHumanOutput(ctx)) {
758
+ const role = data.shared_tag.is_owner ? "owner" : "member";
759
+ console.log(c.bold(data.shared_tag.tag_path) + c.dim(` (${role}, ${data.members_count} members)`));
760
+ console.log("");
761
+ if (data.notes.length === 0) {
762
+ console.log(c.dim(" No notes in this shared tag."));
763
+ return;
764
+ }
765
+ console.log(
766
+ " " + c.dim(pad("ID", 10)) + " " + c.dim(pad("TITLE", 24)) + " " + c.dim(pad("AUTHOR", 20)) + " " + c.dim("UPDATED")
767
+ );
768
+ for (const note of data.notes) {
769
+ const author = note.is_own ? "you" : note.author.email || "unknown";
770
+ console.log(
771
+ " " + pad(note.id.slice(0, 8), 10) + " " + pad(truncate(note.title, 24), 24) + " " + pad(truncate(author, 20), 20) + " " + formatRelativeTime(note.updated_at)
772
+ );
773
+ }
774
+ } else {
775
+ for (const note of data.notes) {
776
+ const author = note.is_own ? "you" : note.author.email || "unknown";
777
+ console.log([note.id, note.title, author, note.updated_at].join(" "));
778
+ }
779
+ }
780
+ }
781
+ function outputSharedNotes(data, ctx) {
782
+ if (ctx.json) {
783
+ outputJson(data);
784
+ return;
785
+ }
786
+ const c = getColors(ctx);
787
+ if (data.count === 0) {
788
+ console.log(c.dim("No shared notes."));
789
+ return;
790
+ }
791
+ if (isHumanOutput(ctx)) {
792
+ console.log(c.bold(`Shared Notes (${data.count})`));
793
+ console.log("");
794
+ for (const share of data.shares) {
795
+ const revoked = share.is_revoked ? c.red(" [R]") : "";
796
+ console.log(
797
+ " " + truncate(share.note_title, 20) + " " + c.cyan(share.share_url) + " " + c.dim(`${share.view_count} views`) + " " + c.dim(formatRelativeTime(share.created_at)) + revoked
798
+ );
799
+ }
800
+ } else {
801
+ for (const share of data.shares) {
802
+ console.log(
803
+ [share.note_title, share.share_url, share.view_count, share.is_revoked, share.created_at].join(" ")
804
+ );
805
+ }
806
+ }
807
+ }
782
808
  function outputMessage(message, ctx) {
783
809
  if (ctx.json) {
784
810
  outputJson({ message });
@@ -851,7 +877,7 @@ async function createNoteAction(title, options, ctx) {
851
877
  const result = await createNote(
852
878
  {
853
879
  title,
854
- tags: options.tag && options.tag.length > 0 ? options.tag : void 0,
880
+ tags: options.tags && options.tags.length > 0 ? options.tags : void 0,
855
881
  content
856
882
  },
857
883
  { pin: ctx.pin }
@@ -911,17 +937,14 @@ async function pinNoteAction(id, ctx) {
911
937
  import pc7 from "picocolors";
912
938
  import { createInterface } from "readline";
913
939
  async function confirm(message) {
914
- if (!process.stdin.isTTY) {
915
- return false;
916
- }
917
940
  const rl = createInterface({
918
941
  input: process.stdin,
919
942
  output: process.stdout
920
943
  });
921
- return new Promise((resolve) => {
944
+ return new Promise((resolve2) => {
922
945
  rl.question(message + " [y/N] ", (answer) => {
923
946
  rl.close();
924
- resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
947
+ resolve2(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
925
948
  });
926
949
  });
927
950
  }
@@ -929,6 +952,13 @@ async function deleteNoteAction(id, options, ctx) {
929
952
  try {
930
953
  const note = await getNote(id, { pin: ctx.pin });
931
954
  if (!options.force && !ctx.json) {
955
+ if (!process.stdin.isTTY) {
956
+ throw new CLIError(
957
+ "Cannot confirm deletion in non-interactive mode",
958
+ ExitCode.GENERAL_ERROR,
959
+ "Use --force for non-interactive deletion"
960
+ );
961
+ }
932
962
  console.log(`About to delete: ${pc7.bold(note.title)}`);
933
963
  const confirmed = await confirm("Are you sure?");
934
964
  if (!confirmed) {
@@ -947,6 +977,30 @@ async function deleteNoteAction(id, options, ctx) {
947
977
  }
948
978
  }
949
979
 
980
+ // src/commands/notes/update.ts
981
+ async function updateNoteAction(id, options, ctx) {
982
+ try {
983
+ if (!options.title && !options.tags) {
984
+ throw new CLIError(
985
+ "No changes provided",
986
+ 1,
987
+ "Use --title or --tags to specify what to update"
988
+ );
989
+ }
990
+ const data = {};
991
+ if (options.title) data.title = options.title;
992
+ if (options.tags) data.tags = options.tags;
993
+ const result = await updateNote(id, data, { pin: ctx.pin });
994
+ if (ctx.json) {
995
+ outputJson(result);
996
+ } else {
997
+ outputMessage(`Updated note: ${result.note.title}`, ctx);
998
+ }
999
+ } catch (error) {
1000
+ handleError(error, ctx.noColor);
1001
+ }
1002
+ }
1003
+
950
1004
  // src/lib/pin.ts
951
1005
  import { createInterface as createInterface2 } from "readline";
952
1006
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, statSync } from "fs";
@@ -1027,7 +1081,7 @@ async function promptForPin(noteTitle, hint, maxAttempts = PIN_MAX_ATTEMPTS) {
1027
1081
  return null;
1028
1082
  }
1029
1083
  async function readPinHidden(prompt) {
1030
- return new Promise((resolve) => {
1084
+ return new Promise((resolve2) => {
1031
1085
  const rl = createInterface2({
1032
1086
  input: process.stdin,
1033
1087
  output: process.stderr,
@@ -1043,7 +1097,7 @@ async function readPinHidden(prompt) {
1043
1097
  if (char === "\r" || char === "\n") {
1044
1098
  process.stderr.write("\n");
1045
1099
  cleanup();
1046
- resolve(pin);
1100
+ resolve2(pin);
1047
1101
  } else if (char === "") {
1048
1102
  process.stderr.write("\n");
1049
1103
  cleanup();
@@ -1129,6 +1183,7 @@ Examples:
1129
1183
  $ pnote notes --pinned Show only pinned notes
1130
1184
  $ pnote notes get abc123 Get note details
1131
1185
  $ pnote notes create "My Note" Create a new note
1186
+ $ pnote notes update abc123 --title "New Title"
1132
1187
  `
1133
1188
  );
1134
1189
  notesCommand.command("get").description("Get a note with its latest snippet").argument("<id>", "Note ID").action(async (id, _options, cmd) => {
@@ -1136,7 +1191,7 @@ notesCommand.command("get").description("Get a note with its latest snippet").ar
1136
1191
  const ctx = await buildContext(globalOpts);
1137
1192
  await getNoteAction(id, ctx);
1138
1193
  });
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) => {
1194
+ notesCommand.command("create").description("Create a new note").argument("<title>", "Note title").option("--tags <tags...>", "Tags for the note").option("--content <content>", "Initial snippet content").action(async (title, options, cmd) => {
1140
1195
  const globalOpts = cmd.parent?.parent?.opts() || {};
1141
1196
  const ctx = await buildContext(globalOpts);
1142
1197
  await createNoteAction(title, options, ctx);
@@ -1151,6 +1206,11 @@ notesCommand.command("pin").description("Pin or unpin a note").argument("<id>",
1151
1206
  const ctx = await buildContext(globalOpts);
1152
1207
  await pinNoteAction(id, ctx);
1153
1208
  });
1209
+ notesCommand.command("update").description("Update a note title or tags").argument("<id>", "Note ID").option("--title <title>", "New title").option("--tags <tags...>", "New tags (replaces existing)").action(async (id, options, cmd) => {
1210
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1211
+ const ctx = await buildContext(globalOpts);
1212
+ await updateNoteAction(id, options, ctx);
1213
+ });
1154
1214
  notesCommand.command("delete").description("Delete a note (soft delete)").argument("<id>", "Note ID").option("--force", "Skip confirmation").action(async (id, options, cmd) => {
1155
1215
  const globalOpts = cmd.parent?.parent?.opts() || {};
1156
1216
  const ctx = await buildContext(globalOpts);
@@ -1287,6 +1347,48 @@ async function addSnippetAction(noteId, options, ctx) {
1287
1347
  }
1288
1348
  }
1289
1349
 
1350
+ // src/commands/snippet/update.ts
1351
+ async function updateSnippetAction(snippetId, options, ctx) {
1352
+ try {
1353
+ let content;
1354
+ if (!process.stdin.isTTY) {
1355
+ const chunks = [];
1356
+ for await (const chunk of process.stdin) {
1357
+ chunks.push(chunk);
1358
+ }
1359
+ content = Buffer.concat(chunks).toString("utf-8");
1360
+ if (content.endsWith("\n")) {
1361
+ content = content.slice(0, -1);
1362
+ }
1363
+ if (!content) {
1364
+ content = void 0;
1365
+ }
1366
+ }
1367
+ if (!content && !options.title) {
1368
+ throw new CLIError(
1369
+ "No content or title provided",
1370
+ 1,
1371
+ "Pipe content to this command: echo 'content' | pnote snippet update <snippet-id>"
1372
+ );
1373
+ }
1374
+ const data = {};
1375
+ if (content) data.content = content;
1376
+ if (options.title) data.title = options.title;
1377
+ const result = await updateSnippet(
1378
+ snippetId,
1379
+ data,
1380
+ { pin: ctx.pin }
1381
+ );
1382
+ if (ctx.json) {
1383
+ outputJson(result);
1384
+ } else {
1385
+ outputMessage("Updated snippet", ctx);
1386
+ }
1387
+ } catch (error) {
1388
+ handleError(error, ctx.noColor);
1389
+ }
1390
+ }
1391
+
1290
1392
  // src/commands/snippet/favorite.ts
1291
1393
  async function favoriteSnippetAction(snippetId, ctx) {
1292
1394
  try {
@@ -1344,6 +1446,11 @@ snippetCommand.command("add").description("Add a new snippet version (from stdin
1344
1446
  const ctx = await buildContext2(globalOpts);
1345
1447
  await addSnippetAction(noteId, options, ctx);
1346
1448
  });
1449
+ snippetCommand.command("update").description("Update an existing snippet (from stdin)").argument("<snippet-id>", "Snippet ID").option("--title <title>", "New snippet title").action(async (snippetId, options, cmd) => {
1450
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1451
+ const ctx = await buildContext2(globalOpts);
1452
+ await updateSnippetAction(snippetId, options, ctx);
1453
+ });
1347
1454
  snippetCommand.command("favorite").description("Toggle favorite status on a snippet").argument("<snippet-id>", "Snippet ID").action(async (snippetId, _options, cmd) => {
1348
1455
  const globalOpts = cmd.parent?.parent?.opts() || {};
1349
1456
  const ctx = await buildContext2(globalOpts);
@@ -1480,11 +1587,16 @@ async function searchAction(query, options, ctx) {
1480
1587
  }
1481
1588
  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
1589
  const globalOpts = cmd.parent?.opts() || {};
1590
+ const pin = await resolvePin({
1591
+ pinArg: globalOpts.pin,
1592
+ pinFromStdin: globalOpts.pinStdin,
1593
+ skipPrompt: true
1594
+ });
1483
1595
  const ctx = {
1484
1596
  json: globalOpts.json ?? false,
1485
1597
  noColor: globalOpts.noColor ?? false,
1486
1598
  plain: globalOpts.plain ?? false,
1487
- pin: globalOpts.pin
1599
+ pin: pin ?? void 0
1488
1600
  };
1489
1601
  await searchAction(query, options, ctx);
1490
1602
  }).addHelpText(
@@ -1497,8 +1609,326 @@ Examples:
1497
1609
  `
1498
1610
  );
1499
1611
 
1612
+ // src/commands/share/index.ts
1613
+ import { Command as Command6 } from "commander";
1614
+
1615
+ // src/commands/share/tags.ts
1616
+ async function listSharedTagsAction(ctx) {
1617
+ try {
1618
+ const result = await listSharedTags({ pin: ctx.pin });
1619
+ outputSharedTags(result, ctx);
1620
+ } catch (error) {
1621
+ handleError(error, ctx.noColor);
1622
+ }
1623
+ }
1624
+ async function getSharedTagNotesAction(id, options, ctx) {
1625
+ try {
1626
+ const result = await getSharedTagNotes(
1627
+ id,
1628
+ { limit: options.limit ? parseInt(options.limit, 10) : void 0 },
1629
+ { pin: ctx.pin }
1630
+ );
1631
+ outputSharedTagNotes(result, ctx);
1632
+ } catch (error) {
1633
+ handleError(error, ctx.noColor);
1634
+ }
1635
+ }
1636
+
1637
+ // src/commands/share/notes.ts
1638
+ async function listSharedNotesAction(options, ctx) {
1639
+ try {
1640
+ const result = await listSharedNotes(
1641
+ { include_revoked: options.includeRevoked },
1642
+ { pin: ctx.pin }
1643
+ );
1644
+ outputSharedNotes(result, ctx);
1645
+ } catch (error) {
1646
+ handleError(error, ctx.noColor);
1647
+ }
1648
+ }
1649
+
1650
+ // src/commands/share/index.ts
1651
+ async function buildContext4(globalOpts) {
1652
+ const pin = await resolvePin({
1653
+ pinArg: globalOpts.pin,
1654
+ pinFromStdin: globalOpts.pinStdin,
1655
+ skipPrompt: true
1656
+ });
1657
+ return {
1658
+ json: globalOpts.json ?? false,
1659
+ noColor: globalOpts.noColor ?? false,
1660
+ plain: globalOpts.plain ?? false,
1661
+ pin: pin ?? void 0
1662
+ };
1663
+ }
1664
+ var shareCommand = new Command6("share").description("View shared tags and shared note links").addHelpText(
1665
+ "after",
1666
+ `
1667
+ Examples:
1668
+ $ pnote share tags List shared tags
1669
+ $ pnote share tags abc123 View notes in shared tag
1670
+ $ pnote share notes List public share links
1671
+ $ pnote share notes --include-revoked
1672
+ `
1673
+ );
1674
+ shareCommand.command("tags").description("List shared tags or view notes in a shared tag").argument("[id]", "Shared tag ID to view notes").option("--limit <n>", "Limit notes returned").action(async (id, options, cmd) => {
1675
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1676
+ const ctx = await buildContext4(globalOpts);
1677
+ if (id) {
1678
+ await getSharedTagNotesAction(id, options, ctx);
1679
+ } else {
1680
+ await listSharedTagsAction(ctx);
1681
+ }
1682
+ });
1683
+ shareCommand.command("notes").description("List public share links for your notes").option("--include-revoked", "Include revoked share links").action(async (options, cmd) => {
1684
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1685
+ const ctx = await buildContext4(globalOpts);
1686
+ await listSharedNotesAction(options, ctx);
1687
+ });
1688
+
1689
+ // src/commands/skills/index.ts
1690
+ import { Command as Command7 } from "commander";
1691
+
1692
+ // src/commands/skills/list.ts
1693
+ import pc10 from "picocolors";
1694
+ async function listSkillsAction(ctx) {
1695
+ try {
1696
+ const result = await listNotes(
1697
+ { note_type: "skill", limit: 200 },
1698
+ { pin: ctx.pin }
1699
+ );
1700
+ const skills = result.notes;
1701
+ if (ctx.json) {
1702
+ const grouped2 = {};
1703
+ for (const note of skills) {
1704
+ const slashIdx = note.title.indexOf("/");
1705
+ const skillName = slashIdx > 0 ? note.title.slice(0, slashIdx) : note.title;
1706
+ if (!grouped2[skillName]) grouped2[skillName] = [];
1707
+ grouped2[skillName].push(note);
1708
+ }
1709
+ outputJson(grouped2);
1710
+ return;
1711
+ }
1712
+ if (skills.length === 0) {
1713
+ console.log(pc10.dim("No skills found. Create one with:"));
1714
+ console.log(pc10.dim(" pnote skills push ./my-skill"));
1715
+ return;
1716
+ }
1717
+ const grouped = /* @__PURE__ */ new Map();
1718
+ for (const note of skills) {
1719
+ const slashIdx = note.title.indexOf("/");
1720
+ const skillName = slashIdx > 0 ? note.title.slice(0, slashIdx) : note.title;
1721
+ if (!grouped.has(skillName)) grouped.set(skillName, []);
1722
+ grouped.get(skillName).push(note);
1723
+ }
1724
+ console.log(pc10.bold(`Skills (${grouped.size})`));
1725
+ console.log("");
1726
+ for (const [name, files] of grouped) {
1727
+ const fileNames = files.map((f) => {
1728
+ const slashIdx = f.title.indexOf("/");
1729
+ return slashIdx > 0 ? f.title.slice(slashIdx + 1) : f.title;
1730
+ });
1731
+ console.log(
1732
+ " " + pc10.cyan(name) + pc10.dim(` (${files.length} file${files.length !== 1 ? "s" : ""})`) + " " + pc10.dim(fileNames.join(", "))
1733
+ );
1734
+ }
1735
+ console.log("");
1736
+ console.log(pc10.dim("Pull to local: pnote skills pull"));
1737
+ } catch (error) {
1738
+ handleError(error, ctx.noColor);
1739
+ }
1740
+ }
1741
+
1742
+ // src/commands/skills/pull.ts
1743
+ import * as fs from "fs";
1744
+ import * as path from "path";
1745
+ import * as os from "os";
1746
+ import pc11 from "picocolors";
1747
+ async function pullSkillsAction(skillName, options, ctx) {
1748
+ try {
1749
+ const baseDir = options.dir || path.join(os.homedir(), ".claude", "skills");
1750
+ const result = await listNotes(
1751
+ { note_type: "skill", limit: 200 },
1752
+ { pin: ctx.pin }
1753
+ );
1754
+ let skills = result.notes;
1755
+ if (skillName) {
1756
+ skills = skills.filter((n) => {
1757
+ const prefix = n.title.split("/")[0];
1758
+ return prefix === skillName;
1759
+ });
1760
+ if (skills.length === 0) {
1761
+ console.error(pc11.red(`No skill found with name "${skillName}"`));
1762
+ process.exit(1);
1763
+ }
1764
+ }
1765
+ if (skills.length === 0) {
1766
+ console.log(pc11.dim("No skills to pull."));
1767
+ return;
1768
+ }
1769
+ let pulled = 0;
1770
+ let skipped = 0;
1771
+ for (const note of skills) {
1772
+ const slashIdx = note.title.indexOf("/");
1773
+ if (slashIdx <= 0) {
1774
+ logStatus(`Skipping "${note.title}" (invalid title format, expected "name/file")`);
1775
+ skipped++;
1776
+ continue;
1777
+ }
1778
+ const skillDir = note.title.slice(0, slashIdx);
1779
+ const fileName = note.title.slice(slashIdx + 1);
1780
+ const filePath = path.join(baseDir, skillDir, fileName);
1781
+ const noteData = await getNote(note.id, { pin: ctx.pin });
1782
+ const content = noteData.latest_snippet?.content;
1783
+ if (!content) {
1784
+ logStatus(`Skipping "${note.title}" (no snippet content)`);
1785
+ skipped++;
1786
+ continue;
1787
+ }
1788
+ if (options.dryRun) {
1789
+ console.log(`${pc11.dim("would write")} ${filePath} ${pc11.dim(`(${content.length} chars)`)}`);
1790
+ pulled++;
1791
+ continue;
1792
+ }
1793
+ const dir = path.dirname(filePath);
1794
+ fs.mkdirSync(dir, { recursive: true });
1795
+ fs.writeFileSync(filePath, content, "utf-8");
1796
+ console.log(`${pc11.green("\u2713")} ${filePath}`);
1797
+ pulled++;
1798
+ }
1799
+ console.log("");
1800
+ if (options.dryRun) {
1801
+ console.log(pc11.dim(`Dry run: ${pulled} file(s) would be written, ${skipped} skipped`));
1802
+ } else {
1803
+ console.log(`Pulled ${pulled} file(s) to ${baseDir}` + (skipped > 0 ? `, ${skipped} skipped` : ""));
1804
+ }
1805
+ } catch (error) {
1806
+ handleError(error, ctx.noColor);
1807
+ }
1808
+ }
1809
+
1810
+ // src/commands/skills/push.ts
1811
+ import * as fs2 from "fs";
1812
+ import * as path2 from "path";
1813
+ import pc12 from "picocolors";
1814
+ async function pushSkillsAction(dir, options, ctx) {
1815
+ try {
1816
+ const resolvedDir = path2.resolve(dir);
1817
+ if (!fs2.existsSync(resolvedDir) || !fs2.statSync(resolvedDir).isDirectory()) {
1818
+ console.error(pc12.red(`Not a directory: ${resolvedDir}`));
1819
+ process.exit(1);
1820
+ }
1821
+ const skillMdPath = path2.join(resolvedDir, "SKILL.md");
1822
+ if (!fs2.existsSync(skillMdPath)) {
1823
+ console.error(pc12.red(`No SKILL.md found in ${resolvedDir}`));
1824
+ console.error(pc12.dim("A valid skill directory must contain a SKILL.md file."));
1825
+ process.exit(1);
1826
+ }
1827
+ const skillName = options.name || path2.basename(resolvedDir);
1828
+ const files = collectFiles(resolvedDir, resolvedDir);
1829
+ if (files.length === 0) {
1830
+ console.log(pc12.dim("No files to push."));
1831
+ return;
1832
+ }
1833
+ const existing = await listNotes(
1834
+ { note_type: "skill", limit: 200 },
1835
+ { pin: ctx.pin }
1836
+ );
1837
+ const existingMap = /* @__PURE__ */ new Map();
1838
+ for (const note of existing.notes) {
1839
+ existingMap.set(note.title, note.id);
1840
+ }
1841
+ let created = 0;
1842
+ let updated = 0;
1843
+ for (const relPath of files) {
1844
+ const noteTitle = `${skillName}/${relPath}`;
1845
+ const filePath = path2.join(resolvedDir, relPath);
1846
+ const content = fs2.readFileSync(filePath, "utf-8");
1847
+ const existingId = existingMap.get(noteTitle);
1848
+ if (existingId) {
1849
+ await createSnippet(
1850
+ { note_id: existingId, content },
1851
+ { pin: ctx.pin }
1852
+ );
1853
+ console.log(`${pc12.yellow("\u21BB")} ${noteTitle} ${pc12.dim("(new version)")}`);
1854
+ updated++;
1855
+ } else {
1856
+ await createNote(
1857
+ { title: noteTitle, content, note_type: "skill" },
1858
+ { pin: ctx.pin }
1859
+ );
1860
+ console.log(`${pc12.green("+")} ${noteTitle}`);
1861
+ created++;
1862
+ }
1863
+ }
1864
+ console.log("");
1865
+ console.log(
1866
+ `Pushed ${pc12.bold(skillName)}: ` + (created > 0 ? `${created} created` : "") + (created > 0 && updated > 0 ? ", " : "") + (updated > 0 ? `${updated} updated` : "")
1867
+ );
1868
+ } catch (error) {
1869
+ handleError(error, ctx.noColor);
1870
+ }
1871
+ }
1872
+ function collectFiles(baseDir, currentDir) {
1873
+ const files = [];
1874
+ const entries = fs2.readdirSync(currentDir, { withFileTypes: true });
1875
+ for (const entry of entries) {
1876
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1877
+ const fullPath = path2.join(currentDir, entry.name);
1878
+ const relPath = path2.relative(baseDir, fullPath);
1879
+ if (entry.isFile()) {
1880
+ files.push(relPath);
1881
+ } else if (entry.isDirectory()) {
1882
+ files.push(...collectFiles(baseDir, fullPath));
1883
+ }
1884
+ }
1885
+ return files;
1886
+ }
1887
+
1888
+ // src/commands/skills/index.ts
1889
+ async function buildContext5(globalOpts) {
1890
+ const pin = await resolvePin({
1891
+ pinArg: globalOpts.pin,
1892
+ pinFromStdin: globalOpts.pinStdin,
1893
+ skipPrompt: true
1894
+ });
1895
+ return {
1896
+ json: globalOpts.json ?? false,
1897
+ noColor: globalOpts.noColor ?? false,
1898
+ plain: globalOpts.plain ?? false,
1899
+ pin: pin ?? void 0
1900
+ };
1901
+ }
1902
+ var skillsCommand = new Command7("skills").description("Manage agent skills (sync between cloud and local)").action(async (_options, cmd) => {
1903
+ const globalOpts = cmd.parent?.opts() || {};
1904
+ const ctx = await buildContext5(globalOpts);
1905
+ await listSkillsAction(ctx);
1906
+ }).addHelpText(
1907
+ "after",
1908
+ `
1909
+ Examples:
1910
+ $ pnote skills List all skills in cloud
1911
+ $ pnote skills pull Download all skills to ~/.claude/skills/
1912
+ $ pnote skills pull myskill Download a specific skill
1913
+ $ pnote skills push ./my-skill Upload a local skill directory
1914
+
1915
+ Skills are notes with type "skill" and title format "skill-name/filename.md".
1916
+ They sync to ~/.claude/skills/<skill-name>/<filename> for use with Claude Code.
1917
+ `
1918
+ );
1919
+ skillsCommand.command("pull").description("Download skills from cloud to local (~/.claude/skills/)").argument("[skill-name]", "Specific skill to pull (pulls all if omitted)").option("--dir <path>", "Custom output directory (default: ~/.claude/skills)").option("--dry-run", "Show what would be downloaded without writing files").action(async (skillName, options, cmd) => {
1920
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1921
+ const ctx = await buildContext5(globalOpts);
1922
+ await pullSkillsAction(skillName, options, ctx);
1923
+ });
1924
+ skillsCommand.command("push").description("Upload a local skill directory to cloud").argument("<dir>", "Path to skill directory (must contain SKILL.md)").option("--name <name>", "Override skill name (default: directory name)").action(async (dir, options, cmd) => {
1925
+ const globalOpts = cmd.parent?.parent?.opts() || {};
1926
+ const ctx = await buildContext5(globalOpts);
1927
+ await pushSkillsAction(dir, options, ctx);
1928
+ });
1929
+
1500
1930
  // src/index.ts
1501
- var program = new Command6();
1931
+ var program = new Command8();
1502
1932
  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
1933
  sortSubcommands: true,
1504
1934
  sortOptions: true
@@ -1528,6 +1958,8 @@ program.addCommand(notesCommand);
1528
1958
  program.addCommand(snippetCommand);
1529
1959
  program.addCommand(tagsCommand);
1530
1960
  program.addCommand(searchCommand);
1961
+ program.addCommand(shareCommand);
1962
+ program.addCommand(skillsCommand);
1531
1963
  process.on("SIGINT", () => {
1532
1964
  console.error("\nInterrupted");
1533
1965
  process.exit(130);