@andrzejchm/notion-cli 0.11.0 → 0.13.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/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { readFileSync as readFileSync2 } from "fs";
5
5
  import { dirname as dirname2, join as join4 } from "path";
6
6
  import { fileURLToPath } from "url";
7
- import { Command as Command26 } from "commander";
7
+ import { Command as Command30 } from "commander";
8
8
 
9
9
  // src/commands/append.ts
10
10
  import { Command } from "commander";
@@ -451,9 +451,24 @@ function isNotionValidationError(error2) {
451
451
  }
452
452
 
453
453
  // src/notion/client.ts
454
- import { APIErrorCode, Client, isNotionClientError } from "@notionhq/client";
454
+ import {
455
+ APIErrorCode,
456
+ Client,
457
+ isNotionClientError,
458
+ LogLevel
459
+ } from "@notionhq/client";
460
+ var stderrLogger = (level, message, extraInfo) => {
461
+ process.stderr.write(
462
+ `[notion-sdk] ${level}: ${message} ${JSON.stringify(extraInfo)}
463
+ `
464
+ );
465
+ };
455
466
  async function validateToken(token) {
456
- const notion = new Client({ auth: token });
467
+ const notion = new Client({
468
+ auth: token,
469
+ logLevel: LogLevel.WARN,
470
+ logger: stderrLogger
471
+ });
457
472
  try {
458
473
  const me = await notion.users.me({});
459
474
  const bot = me;
@@ -473,7 +488,12 @@ async function validateToken(token) {
473
488
  }
474
489
  }
475
490
  function createNotionClient(token) {
476
- return new Client({ auth: token, timeoutMs: 12e4 });
491
+ return new Client({
492
+ auth: token,
493
+ timeoutMs: 12e4,
494
+ logLevel: LogLevel.WARN,
495
+ logger: stderrLogger
496
+ });
477
497
  }
478
498
 
479
499
  // src/notion/url-parser.ts
@@ -542,7 +562,196 @@ function reportTokenSource(source) {
542
562
  stderrWrite(dim(`Using token from ${source}`));
543
563
  }
544
564
 
565
+ // src/services/upload.service.ts
566
+ import { createReadStream, existsSync, statSync } from "fs";
567
+ import { basename, extname } from "path";
568
+ var PART_SIZE = 20 * 1024 * 1024;
569
+ var MIME_MAP = {
570
+ ".png": "image/png",
571
+ ".jpg": "image/jpeg",
572
+ ".jpeg": "image/jpeg",
573
+ ".gif": "image/gif",
574
+ ".webp": "image/webp",
575
+ ".svg": "image/svg+xml",
576
+ ".bmp": "image/bmp",
577
+ ".ico": "image/x-icon",
578
+ ".tiff": "image/tiff",
579
+ ".tif": "image/tiff",
580
+ ".heic": "image/heic",
581
+ ".avif": "image/avif",
582
+ ".pdf": "application/pdf",
583
+ ".mp3": "audio/mpeg",
584
+ ".wav": "audio/wav",
585
+ ".ogg": "audio/ogg",
586
+ ".m4a": "audio/mp4",
587
+ ".flac": "audio/flac",
588
+ ".aac": "audio/aac",
589
+ ".mp4": "video/mp4",
590
+ ".mov": "video/quicktime",
591
+ ".avi": "video/x-msvideo",
592
+ ".webm": "video/webm",
593
+ ".mkv": "video/x-matroska",
594
+ ".csv": "text/csv",
595
+ ".json": "application/json",
596
+ ".txt": "text/plain",
597
+ ".md": "text/markdown",
598
+ ".html": "text/html",
599
+ ".xml": "application/xml",
600
+ ".zip": "application/zip",
601
+ ".gz": "application/gzip",
602
+ ".doc": "application/msword",
603
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
604
+ ".xls": "application/vnd.ms-excel",
605
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
606
+ ".ppt": "application/vnd.ms-powerpoint",
607
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
608
+ };
609
+ function detectMimeType(filePath) {
610
+ const ext = extname(filePath).toLowerCase();
611
+ return MIME_MAP[ext] ?? "application/octet-stream";
612
+ }
613
+ function resolveBlockType(contentType) {
614
+ const base = contentType.split(";")[0].trim().toLowerCase();
615
+ if (base.startsWith("image/")) return "image";
616
+ if (base.startsWith("audio/")) return "audio";
617
+ if (base.startsWith("video/")) return "video";
618
+ if (base === "application/pdf") return "pdf";
619
+ return "file";
620
+ }
621
+ async function readStreamChunk(filePath, start, end) {
622
+ return new Promise((resolve, reject) => {
623
+ const chunks = [];
624
+ const stream = createReadStream(filePath, { start, end });
625
+ stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
626
+ stream.on("end", () => resolve(Buffer.concat(chunks)));
627
+ stream.on("error", reject);
628
+ });
629
+ }
630
+ async function uploadSinglePart(client, filePath, filename, contentType, fileSize) {
631
+ const upload = await client.fileUploads.create({
632
+ mode: "single_part",
633
+ filename,
634
+ content_type: contentType
635
+ });
636
+ const data = await readStreamChunk(filePath, 0, fileSize - 1);
637
+ const blob = new Blob(
638
+ [
639
+ data.buffer.slice(
640
+ data.byteOffset,
641
+ data.byteOffset + data.byteLength
642
+ )
643
+ ],
644
+ { type: contentType }
645
+ );
646
+ await client.fileUploads.send({
647
+ file_upload_id: upload.id,
648
+ file: { data: blob, filename }
649
+ });
650
+ return upload.id;
651
+ }
652
+ async function uploadMultiPart(client, filePath, filename, contentType, fileSize) {
653
+ const numberOfParts = Math.ceil(fileSize / PART_SIZE);
654
+ const upload = await client.fileUploads.create({
655
+ mode: "multi_part",
656
+ filename,
657
+ content_type: contentType,
658
+ number_of_parts: numberOfParts
659
+ });
660
+ for (let part = 0; part < numberOfParts; part++) {
661
+ const start = part * PART_SIZE;
662
+ const end = Math.min(start + PART_SIZE, fileSize) - 1;
663
+ const data = await readStreamChunk(filePath, start, end);
664
+ const blob = new Blob(
665
+ [
666
+ data.buffer.slice(
667
+ data.byteOffset,
668
+ data.byteOffset + data.byteLength
669
+ )
670
+ ],
671
+ { type: contentType }
672
+ );
673
+ await client.fileUploads.send({
674
+ file_upload_id: upload.id,
675
+ file: { data: blob, filename },
676
+ part_number: String(part + 1)
677
+ });
678
+ }
679
+ await client.fileUploads.complete({ file_upload_id: upload.id });
680
+ return upload.id;
681
+ }
682
+ async function uploadFile(client, filePath) {
683
+ const filename = basename(filePath);
684
+ const contentType = detectMimeType(filePath);
685
+ const { size: fileSize } = statSync(filePath);
686
+ if (fileSize === 0) {
687
+ throw new CliError(
688
+ ErrorCodes.INVALID_ARG,
689
+ `Cannot upload empty file: ${filename}`,
690
+ "Provide a file with content"
691
+ );
692
+ }
693
+ const fileUploadId = fileSize <= PART_SIZE ? await uploadSinglePart(
694
+ client,
695
+ filePath,
696
+ filename,
697
+ contentType,
698
+ fileSize
699
+ ) : await uploadMultiPart(
700
+ client,
701
+ filePath,
702
+ filename,
703
+ contentType,
704
+ fileSize
705
+ );
706
+ return { fileUploadId, filename, contentType };
707
+ }
708
+ function buildFileBlock(fileUploadId, blockType, caption) {
709
+ const captionRichText = caption ? [{ type: "text", text: { content: caption } }] : [];
710
+ const fileContent = {
711
+ file_upload: { id: fileUploadId },
712
+ type: "file_upload",
713
+ caption: captionRichText
714
+ };
715
+ switch (blockType) {
716
+ case "image":
717
+ return { type: "image", image: fileContent };
718
+ case "audio":
719
+ return { type: "audio", audio: fileContent };
720
+ case "video":
721
+ return { type: "video", video: fileContent };
722
+ case "pdf":
723
+ return { type: "pdf", pdf: fileContent };
724
+ case "file":
725
+ return { type: "file", file: fileContent };
726
+ default:
727
+ throw new CliError(
728
+ ErrorCodes.INVALID_ARG,
729
+ `Unknown block type: ${blockType}`,
730
+ "Valid types are: image, file, pdf, audio, video"
731
+ );
732
+ }
733
+ }
734
+ async function uploadFilesAsBlocks(files, opts, client) {
735
+ for (const filePath of files) {
736
+ if (!existsSync(filePath)) {
737
+ throw new CliError(
738
+ ErrorCodes.INVALID_ARG,
739
+ `File not found: ${filePath}`,
740
+ "Provide a valid file path"
741
+ );
742
+ }
743
+ }
744
+ return Promise.all(
745
+ files.map(async (filePath) => {
746
+ const result = await uploadFile(client, filePath);
747
+ const blockType = opts.type ?? resolveBlockType(result.contentType);
748
+ return buildFileBlock(result.fileUploadId, blockType, opts.caption);
749
+ })
750
+ );
751
+ }
752
+
545
753
  // src/services/write.service.ts
754
+ import { existsSync as existsSync2 } from "fs";
546
755
  async function addComment(client, target, text, options = {}) {
547
756
  const richText = [
548
757
  {
@@ -661,22 +870,44 @@ async function replaceMarkdown(client, pageId, newMarkdown, options) {
661
870
  }
662
871
  });
663
872
  }
664
- function buildIconCover(options) {
873
+ async function buildIconCover(client, options) {
665
874
  const result = {};
666
875
  if (options?.icon) {
667
876
  const isUrl = /^https?:\/\//i.test(options.icon);
668
877
  if (isUrl) {
669
878
  result.icon = { type: "external", external: { url: options.icon } };
879
+ } else if (existsSync2(options.icon)) {
880
+ const uploaded = await uploadFile(client, options.icon);
881
+ result.icon = {
882
+ type: "file_upload",
883
+ file_upload: { id: uploaded.fileUploadId }
884
+ };
670
885
  } else {
671
886
  result.icon = { type: "emoji", emoji: options.icon };
672
887
  }
673
888
  }
674
889
  if (options?.cover) {
675
- result.cover = { type: "external", external: { url: options.cover } };
890
+ const isUrl = /^https?:\/\//i.test(options.cover);
891
+ if (isUrl) {
892
+ result.cover = { type: "external", external: { url: options.cover } };
893
+ } else if (existsSync2(options.cover)) {
894
+ const uploaded = await uploadFile(client, options.cover);
895
+ result.cover = {
896
+ type: "file_upload",
897
+ file_upload: { id: uploaded.fileUploadId }
898
+ };
899
+ } else {
900
+ throw new CliError(
901
+ ErrorCodes.INVALID_ARG,
902
+ `Cover not found: "${options.cover}" is not a valid URL or existing file path.`,
903
+ "Provide an http(s):// URL or a valid local file path for --cover"
904
+ );
905
+ }
676
906
  }
677
907
  return result;
678
908
  }
679
909
  async function createPage(client, parentId, title, markdown, options) {
910
+ const iconCover = await buildIconCover(client, options);
680
911
  const response = await client.pages.create({
681
912
  parent: { type: "page_id", page_id: parentId },
682
913
  properties: {
@@ -685,7 +916,7 @@ async function createPage(client, parentId, title, markdown, options) {
685
916
  }
686
917
  },
687
918
  ...markdown.trim() ? { markdown } : {},
688
- ...buildIconCover(options)
919
+ ...iconCover
689
920
  });
690
921
  const url = "url" in response ? response.url : response.id;
691
922
  return url;
@@ -697,11 +928,12 @@ async function createPageInDatabase(client, databaseId, titlePropName, title, ex
697
928
  title: [{ type: "text", text: { content: title, link: null } }]
698
929
  }
699
930
  };
931
+ const iconCover = await buildIconCover(client, options);
700
932
  const response = await client.pages.create({
701
933
  parent: { type: "database_id", database_id: databaseId },
702
934
  properties,
703
935
  ...markdown.trim() ? { markdown } : {},
704
- ...buildIconCover(options)
936
+ ...iconCover
705
937
  });
706
938
  const url = "url" in response ? response.url : response.id;
707
939
  return url;
@@ -717,54 +949,92 @@ async function readStdin() {
717
949
  }
718
950
 
719
951
  // src/commands/append.ts
952
+ function collectFiles(val, acc) {
953
+ acc.push(val);
954
+ return acc;
955
+ }
956
+ async function resolveMarkdown(message, hasFiles) {
957
+ if (message) return message;
958
+ if (!process.stdin.isTTY && !hasFiles) return readStdin();
959
+ if (!hasFiles) {
960
+ throw new CliError(
961
+ ErrorCodes.INVALID_ARG,
962
+ "No content to append.",
963
+ "Pass markdown via -m/--message, pipe it through stdin, or use --file to attach files"
964
+ );
965
+ }
966
+ return "";
967
+ }
968
+ async function appendMarkdownWithErrorHandling(client, uuid, markdown, after) {
969
+ try {
970
+ await appendMarkdown(client, uuid, markdown, after ? { after } : void 0);
971
+ } catch (error2) {
972
+ if (after && isNotionValidationError(error2)) {
973
+ throw new CliError(
974
+ ErrorCodes.INVALID_ARG,
975
+ `Selector not found: "${after}". ${error2.message}`,
976
+ SELECTOR_HINT,
977
+ error2
978
+ );
979
+ }
980
+ throw error2;
981
+ }
982
+ }
983
+ async function appendFileBlocks(client, uuid, filePaths) {
984
+ const blocks = await uploadFilesAsBlocks(filePaths, {}, client);
985
+ await client.blocks.children.append({
986
+ block_id: uuid,
987
+ children: blocks
988
+ });
989
+ }
720
990
  function appendCommand() {
721
991
  const cmd = new Command("append");
722
992
  cmd.description("append markdown content to a Notion page").argument("<id/url>", "Notion page ID or URL").option("-m, --message <markdown>", "markdown content to append").option(
723
993
  "--after <selector>",
724
994
  'insert after matched content \u2014 ellipsis selector, e.g. "## Section...end of section"'
995
+ ).option(
996
+ "--file <path>",
997
+ "attach a local file to the page (repeatable)",
998
+ collectFiles,
999
+ []
725
1000
  ).action(
726
1001
  withErrorHandling(
727
1002
  async (idOrUrl, opts) => {
728
1003
  const { token, source } = await resolveToken();
729
1004
  reportTokenSource(source);
730
1005
  const client = createNotionClient(token);
731
- let markdown = "";
732
- if (opts.message) {
733
- markdown = opts.message;
734
- } else if (!process.stdin.isTTY) {
735
- markdown = await readStdin();
736
- } else {
1006
+ const hasFiles = opts.file.length > 0;
1007
+ const markdown = await resolveMarkdown(opts.message, hasFiles);
1008
+ const uuid = toUuid(parseNotionId(idOrUrl));
1009
+ if (opts.after && !markdown.trim()) {
737
1010
  throw new CliError(
738
1011
  ErrorCodes.INVALID_ARG,
739
- "No content to append.",
740
- "Pass markdown via -m/--message or pipe it through stdin"
1012
+ "--after requires markdown content (-m or stdin)",
1013
+ "Provide markdown via -m/--message or pipe it through stdin"
741
1014
  );
742
1015
  }
743
- if (!markdown.trim()) {
744
- process.stdout.write("Nothing to append.\n");
745
- return;
746
- }
747
- const pageId = parseNotionId(idOrUrl);
748
- const uuid = toUuid(pageId);
749
- try {
750
- await appendMarkdown(
1016
+ if (markdown.trim()) {
1017
+ await appendMarkdownWithErrorHandling(
751
1018
  client,
752
1019
  uuid,
753
1020
  markdown,
754
- opts.after ? { after: opts.after } : void 0
1021
+ opts.after
755
1022
  );
756
- } catch (error2) {
757
- if (opts.after && isNotionValidationError(error2)) {
758
- throw new CliError(
759
- ErrorCodes.INVALID_ARG,
760
- `Selector not found: "${opts.after}". ${error2.message}`,
761
- SELECTOR_HINT,
762
- error2
763
- );
764
- }
765
- throw error2;
766
1023
  }
767
- process.stdout.write("Appended.\n");
1024
+ if (hasFiles) {
1025
+ await appendFileBlocks(client, uuid, opts.file);
1026
+ }
1027
+ if (hasFiles && markdown.trim()) {
1028
+ process.stdout.write(
1029
+ `Appended and attached ${opts.file.length} file(s).
1030
+ `
1031
+ );
1032
+ } else if (hasFiles) {
1033
+ process.stdout.write(`Attached ${opts.file.length} file(s).
1034
+ `);
1035
+ } else {
1036
+ process.stdout.write("Appended.\n");
1037
+ }
768
1038
  }
769
1039
  )
770
1040
  );
@@ -842,31 +1112,96 @@ function printWithPager(text) {
842
1112
  }
843
1113
 
844
1114
  // src/commands/archive.ts
1115
+ async function archiveEntity(client, uuid) {
1116
+ try {
1117
+ const result2 = await client.pages.update({
1118
+ page_id: uuid,
1119
+ in_trash: true
1120
+ });
1121
+ return { result: result2, kind: "page" };
1122
+ } catch {
1123
+ }
1124
+ try {
1125
+ const result2 = await client.dataSources.update({
1126
+ data_source_id: uuid,
1127
+ in_trash: true
1128
+ });
1129
+ return { result: result2, kind: "data_source" };
1130
+ } catch {
1131
+ }
1132
+ const result = await client.databases.update({
1133
+ database_id: uuid,
1134
+ in_trash: true
1135
+ });
1136
+ return { result, kind: "database" };
1137
+ }
845
1138
  function archiveCommand() {
846
1139
  const cmd = new Command2("archive");
847
- cmd.description("archive (trash) a Notion page").argument("<id/url>", "Notion page ID or URL").action(
1140
+ cmd.description("archive (trash) a Notion page or database").argument("<id/url>", "Notion page or database ID/URL").action(
848
1141
  withErrorHandling(async (idOrUrl) => {
849
1142
  const { token, source } = await resolveToken();
850
1143
  reportTokenSource(source);
851
1144
  const client = createNotionClient(token);
852
1145
  const id = parseNotionId(idOrUrl);
853
1146
  const uuid = toUuid(id);
854
- const updatedPage = await client.pages.update({
855
- page_id: uuid,
856
- archived: true
857
- });
1147
+ const { result, kind } = await archiveEntity(client, uuid);
858
1148
  const mode = getOutputMode();
859
1149
  if (mode === "json") {
860
- process.stdout.write(`${formatJSON(updatedPage)}
1150
+ process.stdout.write(`${formatJSON(result)}
861
1151
  `);
862
1152
  } else {
863
- process.stdout.write("Page archived.\n");
1153
+ const label = kind === "page" ? "Page" : "Database";
1154
+ process.stdout.write(`${label} archived.
1155
+ `);
864
1156
  }
865
1157
  })
866
1158
  );
867
1159
  return cmd;
868
1160
  }
869
1161
 
1162
+ // src/commands/attach.ts
1163
+ import { Command as Command3, Option } from "commander";
1164
+ function attachCommand() {
1165
+ const cmd = new Command3("attach");
1166
+ cmd.description("upload and attach file(s) to a Notion page").argument("<id/url>", "Notion page ID or URL").argument("<file>", "file to attach").argument("[files...]", "additional files to attach").option("--caption <text>", "caption for the file block(s)").addOption(
1167
+ new Option(
1168
+ "--type <type>",
1169
+ "override auto-detected block type (image|file|pdf|audio|video)"
1170
+ ).choices(["image", "file", "pdf", "audio", "video"])
1171
+ ).action(
1172
+ withErrorHandling(
1173
+ async (idOrUrl, firstFile, extraFiles, opts) => {
1174
+ const { token, source } = await resolveToken();
1175
+ reportTokenSource(source);
1176
+ const client = createNotionClient(token);
1177
+ const pageId = toUuid(parseNotionId(idOrUrl));
1178
+ const allFiles = [firstFile, ...extraFiles];
1179
+ const blocks = await uploadFilesAsBlocks(
1180
+ allFiles,
1181
+ { caption: opts.caption, type: opts.type },
1182
+ client
1183
+ );
1184
+ const response = await client.blocks.children.append({
1185
+ block_id: pageId,
1186
+ children: blocks
1187
+ });
1188
+ const mode = getOutputMode();
1189
+ if (mode === "json") {
1190
+ process.stdout.write(`${formatJSON(response)}
1191
+ `);
1192
+ } else {
1193
+ const pageUrl = `https://www.notion.so/${parseNotionId(idOrUrl)}`;
1194
+ process.stdout.write(
1195
+ `Attached ${allFiles.length} file(s) to ${pageUrl}
1196
+ `
1197
+ );
1198
+ }
1199
+ }
1200
+ )
1201
+ );
1202
+ return cmd;
1203
+ }
1204
+
870
1205
  // src/commands/auth/index.ts
871
1206
  function authDefaultAction(authCmd2) {
872
1207
  return async () => {
@@ -876,7 +1211,7 @@ function authDefaultAction(authCmd2) {
876
1211
 
877
1212
  // src/commands/auth/login.ts
878
1213
  import { input } from "@inquirer/prompts";
879
- import { Command as Command3 } from "commander";
1214
+ import { Command as Command4 } from "commander";
880
1215
 
881
1216
  // src/oauth/oauth-flow.ts
882
1217
  import { spawn } from "child_process";
@@ -1123,7 +1458,7 @@ Waiting for callback (up to 120 seconds)...
1123
1458
 
1124
1459
  // src/commands/auth/login.ts
1125
1460
  function loginCommand() {
1126
- const cmd = new Command3("login");
1461
+ const cmd = new Command4("login");
1127
1462
  cmd.description("authenticate with Notion via OAuth").option("--profile <name>", "profile name to store credentials in").option(
1128
1463
  "--manual",
1129
1464
  "print auth URL instead of opening browser (for headless OAuth)"
@@ -1186,7 +1521,7 @@ function loginCommand() {
1186
1521
 
1187
1522
  // src/commands/auth/logout.ts
1188
1523
  import { select } from "@inquirer/prompts";
1189
- import { Command as Command4 } from "commander";
1524
+ import { Command as Command5 } from "commander";
1190
1525
  function profileLabel(name, profile) {
1191
1526
  const parts = [];
1192
1527
  if (profile.oauth_access_token)
@@ -1199,7 +1534,7 @@ function profileLabel(name, profile) {
1199
1534
  return `${bold(name)} ${dim(authDesc)}${workspace}`;
1200
1535
  }
1201
1536
  function logoutCommand() {
1202
- const cmd = new Command4("logout");
1537
+ const cmd = new Command5("logout");
1203
1538
  cmd.description("remove a profile and its credentials").option(
1204
1539
  "--profile <name>",
1205
1540
  "profile name to remove (skips interactive selector)"
@@ -1257,9 +1592,9 @@ function logoutCommand() {
1257
1592
  }
1258
1593
 
1259
1594
  // src/commands/auth/status.ts
1260
- import { Command as Command5 } from "commander";
1595
+ import { Command as Command6 } from "commander";
1261
1596
  function statusCommand() {
1262
- const cmd = new Command5("status");
1597
+ const cmd = new Command6("status");
1263
1598
  cmd.description("show authentication status for the active profile").option("--profile <name>", "profile name to check").action(
1264
1599
  withErrorHandling(async (opts) => {
1265
1600
  let profileName = opts.profile;
@@ -1317,7 +1652,7 @@ function statusCommand() {
1317
1652
  }
1318
1653
 
1319
1654
  // src/commands/comment-add.ts
1320
- import { Command as Command6 } from "commander";
1655
+ import { Command as Command7 } from "commander";
1321
1656
  function resolveTarget(idOrUrl, opts) {
1322
1657
  const targetCount = [idOrUrl, opts.replyTo, opts.block].filter(
1323
1658
  Boolean
@@ -1345,7 +1680,7 @@ function resolveTarget(idOrUrl, opts) {
1345
1680
  );
1346
1681
  }
1347
1682
  function commentAddCommand() {
1348
- const cmd = new Command6("comment");
1683
+ const cmd = new Command7("comment");
1349
1684
  cmd.description("add a comment to a Notion page, block, or discussion thread").argument("[id/url]", "Notion page ID or URL").requiredOption("-m, --message <text>", "comment text to post").option(
1350
1685
  "--reply-to <discussion-id>",
1351
1686
  "reply to an existing discussion thread"
@@ -1367,7 +1702,7 @@ function commentAddCommand() {
1367
1702
  }
1368
1703
 
1369
1704
  // src/commands/comments.ts
1370
- import { Command as Command7 } from "commander";
1705
+ import { Command as Command8 } from "commander";
1371
1706
 
1372
1707
  // src/output/paginate.ts
1373
1708
  async function paginateResults(fetcher) {
@@ -1393,7 +1728,7 @@ function formatParent(parent) {
1393
1728
  }
1394
1729
  }
1395
1730
  function commentsCommand() {
1396
- const cmd = new Command7("comments");
1731
+ const cmd = new Command8("comments");
1397
1732
  cmd.description("list comments on a Notion page").argument("<id/url>", "Notion page ID or URL").option("--json", "output as JSON").action(
1398
1733
  withErrorHandling(async (idOrUrl, opts) => {
1399
1734
  if (opts.json) setOutputMode("json");
@@ -1430,7 +1765,7 @@ function commentsCommand() {
1430
1765
  }
1431
1766
 
1432
1767
  // src/commands/completion.ts
1433
- import { Command as Command8 } from "commander";
1768
+ import { Command as Command9 } from "commander";
1434
1769
  var BASH_COMPLETION = `# notion bash completion
1435
1770
  _notion_completion() {
1436
1771
  local cur prev words cword
@@ -1534,7 +1869,7 @@ complete -c notion -n '__fish_seen_subcommand_from completion' -a zsh -d 'zsh co
1534
1869
  complete -c notion -n '__fish_seen_subcommand_from completion' -a fish -d 'fish completion script'
1535
1870
  `;
1536
1871
  function completionCommand() {
1537
- const cmd = new Command8("completion");
1872
+ const cmd = new Command9("completion");
1538
1873
  cmd.description("output shell completion script").argument("<shell>", "shell type (bash, zsh, fish)").action(
1539
1874
  withErrorHandling(async (shell) => {
1540
1875
  switch (shell) {
@@ -1560,13 +1895,14 @@ function completionCommand() {
1560
1895
  }
1561
1896
 
1562
1897
  // src/commands/create-page.ts
1563
- import { Command as Command9 } from "commander";
1898
+ import { Command as Command10 } from "commander";
1564
1899
 
1565
1900
  // src/services/database.service.ts
1566
1901
  import { isFullPage } from "@notionhq/client";
1567
1902
  async function fetchDatabaseSchema(client, dbId) {
1568
- const ds = await client.dataSources.retrieve({ data_source_id: dbId });
1569
- const title = "title" in ds ? ds.title.map((rt) => rt.plain_text).join("") || dbId : dbId;
1903
+ const resolvedId = await resolveDataSourceId(client, dbId);
1904
+ const ds = await client.dataSources.retrieve({ data_source_id: resolvedId });
1905
+ const title = "title" in ds ? ds.title.map((rt) => rt.plain_text).join("") || resolvedId : resolvedId;
1570
1906
  const properties = {};
1571
1907
  if ("properties" in ds) {
1572
1908
  for (const [name, prop] of Object.entries(ds.properties)) {
@@ -1585,8 +1921,8 @@ async function fetchDatabaseSchema(client, dbId) {
1585
1921
  properties[name] = config;
1586
1922
  }
1587
1923
  }
1588
- const databaseId = "parent" in ds && ds.parent && typeof ds.parent === "object" && "database_id" in ds.parent ? ds.parent.database_id : dbId;
1589
- return { id: dbId, databaseId, title, properties };
1924
+ const databaseId = "parent" in ds && ds.parent && typeof ds.parent === "object" && "database_id" in ds.parent ? ds.parent.database_id : resolvedId;
1925
+ return { id: resolvedId, databaseId, title, properties };
1590
1926
  }
1591
1927
  async function queryDatabase(client, dbId, opts = {}) {
1592
1928
  const rawPages = await paginateResults(
@@ -1806,6 +2142,81 @@ async function resolveDataSourceId(client, idOrUrl) {
1806
2142
  `Could not find database or data source: ${id}`
1807
2143
  );
1808
2144
  }
2145
+ function buildDatabaseUpdatePayload(opts, schema) {
2146
+ const properties = {};
2147
+ for (const def of opts.addProps) {
2148
+ const { name, config } = parsePropertyDefinition(def);
2149
+ properties[name] = config;
2150
+ }
2151
+ for (const name of opts.removeProps) {
2152
+ properties[name] = null;
2153
+ }
2154
+ for (const raw of opts.renameProps) {
2155
+ const colonIdx = raw.indexOf(":");
2156
+ if (colonIdx === -1) {
2157
+ throw new CliError(
2158
+ ErrorCodes.INVALID_ARG,
2159
+ `Invalid rename format: "${raw}"`,
2160
+ 'Use format: --rename-prop "OldName:NewName"'
2161
+ );
2162
+ }
2163
+ const oldName = raw.slice(0, colonIdx).trim();
2164
+ const newName = raw.slice(colonIdx + 1).trim();
2165
+ const propConfig = schema.properties[oldName];
2166
+ if (!propConfig) {
2167
+ const available = Object.keys(schema.properties).join(", ");
2168
+ throw new CliError(
2169
+ ErrorCodes.INVALID_ARG,
2170
+ `Property "${oldName}" not found in schema`,
2171
+ `Available properties: ${available}`
2172
+ );
2173
+ }
2174
+ properties[propConfig.id] = { name: newName };
2175
+ }
2176
+ for (const raw of opts.setOptions) {
2177
+ const colonIdx = raw.indexOf(":");
2178
+ if (colonIdx === -1) {
2179
+ throw new CliError(
2180
+ ErrorCodes.INVALID_ARG,
2181
+ `Invalid set-options format: "${raw}"`,
2182
+ 'Use format: --set-options "PropertyName:opt1,opt2,opt3"'
2183
+ );
2184
+ }
2185
+ const propName = raw.slice(0, colonIdx).trim();
2186
+ const optionsStr = raw.slice(colonIdx + 1).trim();
2187
+ const propConfig = schema.properties[propName];
2188
+ if (!propConfig) {
2189
+ const available = Object.keys(schema.properties).join(", ");
2190
+ throw new CliError(
2191
+ ErrorCodes.INVALID_ARG,
2192
+ `Property "${propName}" not found in schema`,
2193
+ `Available properties: ${available}`
2194
+ );
2195
+ }
2196
+ if (propConfig.type !== "select" && propConfig.type !== "multi_select") {
2197
+ throw new CliError(
2198
+ ErrorCodes.INVALID_ARG,
2199
+ `Property "${propName}" is of type "${propConfig.type}" \u2014 only select and multi_select properties support --set-options`
2200
+ );
2201
+ }
2202
+ const options = optionsStr.split(",").map((opt) => ({ name: opt.trim() }));
2203
+ properties[propName] = { [propConfig.type]: { options } };
2204
+ }
2205
+ const payload = {};
2206
+ if (opts.title !== void 0) {
2207
+ payload.title = [{ type: "text", text: { content: opts.title } }];
2208
+ }
2209
+ if (Object.keys(properties).length > 0) {
2210
+ payload.properties = properties;
2211
+ }
2212
+ return payload;
2213
+ }
2214
+ async function updateDatabaseSchema(client, dataSourceId, payload) {
2215
+ return client.dataSources.update({
2216
+ data_source_id: dataSourceId,
2217
+ ...payload
2218
+ });
2219
+ }
1809
2220
  function displayPropertyValue(prop) {
1810
2221
  switch (prop.type) {
1811
2222
  case "title":
@@ -1848,6 +2259,7 @@ function displayPropertyValue(prop) {
1848
2259
  }
1849
2260
 
1850
2261
  // src/services/update.service.ts
2262
+ import { existsSync as existsSync3 } from "fs";
1851
2263
  var UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
1852
2264
  "relation",
1853
2265
  "formula",
@@ -1856,7 +2268,6 @@ var UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
1856
2268
  "created_by",
1857
2269
  "last_edited_time",
1858
2270
  "last_edited_by",
1859
- "files",
1860
2271
  "unique_id",
1861
2272
  "verification",
1862
2273
  "button"
@@ -1866,7 +2277,7 @@ function buildPropertyUpdate(propName, propType, value) {
1866
2277
  throw new CliError(
1867
2278
  ErrorCodes.INVALID_ARG,
1868
2279
  `Property "${propName}" has type "${propType}" which cannot be set via the CLI.`,
1869
- "Supported types: title, rich_text, select, status, multi_select, number, checkbox, url, email, phone_number, date"
2280
+ "Supported types: title, rich_text, select, status, multi_select, number, checkbox, url, email, phone_number, date, files"
1870
2281
  );
1871
2282
  }
1872
2283
  if (value === "") {
@@ -1912,41 +2323,54 @@ function buildPropertyUpdate(propName, propType, value) {
1912
2323
  const end = parts[1]?.trim();
1913
2324
  return { date: end ? { start, end } : { start } };
1914
2325
  }
2326
+ case "files":
2327
+ throw new CliError(
2328
+ ErrorCodes.INVALID_ARG,
2329
+ `Property "${propName}" has type "files" which requires async upload \u2014 use buildPropertiesPayloadAsync instead.`,
2330
+ "Use buildPropertiesPayloadAsync with a Notion client to handle file uploads"
2331
+ );
1915
2332
  default:
1916
2333
  throw new CliError(
1917
2334
  ErrorCodes.INVALID_ARG,
1918
2335
  `Property "${propName}" has unsupported type "${propType}".`,
1919
- "Supported types: title, rich_text, select, status, multi_select, number, checkbox, url, email, phone_number, date"
2336
+ "Supported types: title, rich_text, select, status, multi_select, number, checkbox, url, email, phone_number, date, files"
1920
2337
  );
1921
2338
  }
1922
2339
  }
1923
- function buildPropertiesPayload(propStrings, schemaOrPage) {
2340
+ function normalizeSchema(schemaOrPage) {
1924
2341
  const isPage = "object" in schemaOrPage && schemaOrPage.object === "page";
1925
- const schema = isPage ? schemaOrPage.properties : schemaOrPage;
2342
+ return isPage ? schemaOrPage.properties : schemaOrPage;
2343
+ }
2344
+ function parsePropString(propString) {
2345
+ const eqIdx = propString.indexOf("=");
2346
+ if (eqIdx === -1) {
2347
+ throw new CliError(
2348
+ ErrorCodes.INVALID_ARG,
2349
+ `Invalid --prop value: "${propString}". Expected format: "PropertyName=Value".`,
2350
+ 'Example: --prop "Status=Done"'
2351
+ );
2352
+ }
2353
+ return [propString.slice(0, eqIdx).trim(), propString.slice(eqIdx + 1)];
2354
+ }
2355
+ function lookupSchemaProp(propName, schema) {
2356
+ const schemaProp = schema[propName];
2357
+ if (!schemaProp) {
2358
+ const available = Object.keys(schema).join(", ");
2359
+ throw new CliError(
2360
+ ErrorCodes.INVALID_ARG,
2361
+ `Property "${propName}" not found on this page.`,
2362
+ `Available properties: ${available}`
2363
+ );
2364
+ }
2365
+ return schemaProp;
2366
+ }
2367
+ function buildPropertiesPayload(propStrings, schemaOrPage) {
2368
+ const schema = normalizeSchema(schemaOrPage);
1926
2369
  const result = {};
1927
2370
  for (const propString of propStrings) {
1928
- const eqIdx = propString.indexOf("=");
1929
- if (eqIdx === -1) {
1930
- throw new CliError(
1931
- ErrorCodes.INVALID_ARG,
1932
- `Invalid --prop value: "${propString}". Expected format: "PropertyName=Value".`,
1933
- 'Example: --prop "Status=Done"'
1934
- );
1935
- }
1936
- const propName = propString.slice(0, eqIdx).trim();
1937
- const value = propString.slice(eqIdx + 1);
1938
- const schemaProp = schema[propName];
1939
- if (!schemaProp) {
1940
- const available = Object.keys(schema).join(", ");
1941
- throw new CliError(
1942
- ErrorCodes.INVALID_ARG,
1943
- `Property "${propName}" not found on this page.`,
1944
- `Available properties: ${available}`
1945
- );
1946
- }
1947
- const propType = schemaProp.type;
1948
- const payload = buildPropertyUpdate(propName, propType, value);
1949
- result[propName] = payload;
2371
+ const [propName, value] = parsePropString(propString);
2372
+ const schemaProp = lookupSchemaProp(propName, schema);
2373
+ result[propName] = buildPropertyUpdate(propName, schemaProp.type, value);
1950
2374
  }
1951
2375
  return result;
1952
2376
  }
@@ -1959,7 +2383,7 @@ async function updatePageProperties(client, pageId, properties) {
1959
2383
  }
1960
2384
 
1961
2385
  // src/commands/create-page.ts
1962
- function collectProps(val, acc) {
2386
+ function collectValues(val, acc) {
1963
2387
  acc.push(val);
1964
2388
  return acc;
1965
2389
  }
@@ -1971,16 +2395,21 @@ async function tryGetDatabaseSchema(client, uuid) {
1971
2395
  }
1972
2396
  }
1973
2397
  function createPageCommand() {
1974
- const cmd = new Command9("create-page");
2398
+ const cmd = new Command10("create-page");
1975
2399
  cmd.description("create a new Notion page under a parent page or database").requiredOption("--parent <id/url>", "parent page or database ID/URL").requiredOption("--title <title>", "page title").option(
1976
2400
  "-m, --message <markdown>",
1977
2401
  "inline markdown content for the page body"
1978
2402
  ).option(
1979
2403
  "--prop <property=value>",
1980
2404
  "set a property value (repeatable, database parents only)",
1981
- collectProps,
2405
+ collectValues,
2406
+ []
2407
+ ).option("--icon <emoji-or-url>", "page icon \u2014 emoji character or image URL").option("--cover <url>", "page cover image URL").option(
2408
+ "--file <path>",
2409
+ "attach a local file to the page after creation (repeatable)",
2410
+ collectValues,
1982
2411
  []
1983
- ).option("--icon <emoji-or-url>", "page icon \u2014 emoji character or image URL").option("--cover <url>", "page cover image URL").action(
2412
+ ).action(
1984
2413
  withErrorHandling(async (opts) => {
1985
2414
  const { token, source } = await resolveToken();
1986
2415
  reportTokenSource(source);
@@ -1988,12 +2417,13 @@ function createPageCommand() {
1988
2417
  let markdown = "";
1989
2418
  if (opts.message) {
1990
2419
  markdown = opts.message;
1991
- } else if (!process.stdin.isTTY) {
2420
+ } else if (!process.stdin.isTTY && opts.file.length === 0) {
1992
2421
  markdown = await readStdin();
1993
2422
  }
1994
2423
  const parentUuid = toUuid(parseNotionId(opts.parent));
1995
2424
  const iconCover = { icon: opts.icon, cover: opts.cover };
1996
2425
  const dbSchema = await tryGetDatabaseSchema(client, parentUuid);
2426
+ let createdPageUrl;
1997
2427
  if (dbSchema) {
1998
2428
  const titleEntry = Object.entries(dbSchema.properties).find(
1999
2429
  ([, prop]) => prop.type === "title"
@@ -2007,7 +2437,7 @@ function createPageCommand() {
2007
2437
  }
2008
2438
  const [titlePropName] = titleEntry;
2009
2439
  const extraProperties = opts.prop.length > 0 ? buildPropertiesPayload(opts.prop, dbSchema.properties) : {};
2010
- const url = await createPageInDatabase(
2440
+ createdPageUrl = await createPageInDatabase(
2011
2441
  client,
2012
2442
  dbSchema.databaseId,
2013
2443
  titlePropName,
@@ -2016,8 +2446,6 @@ function createPageCommand() {
2016
2446
  markdown,
2017
2447
  iconCover
2018
2448
  );
2019
- process.stdout.write(`${url}
2020
- `);
2021
2449
  } else {
2022
2450
  if (opts.prop.length > 0) {
2023
2451
  throw new CliError(
@@ -2026,32 +2454,40 @@ function createPageCommand() {
2026
2454
  "To set properties, use a database ID/URL as --parent"
2027
2455
  );
2028
2456
  }
2029
- const url = await createPage(
2457
+ createdPageUrl = await createPage(
2030
2458
  client,
2031
2459
  parentUuid,
2032
2460
  opts.title,
2033
2461
  markdown,
2034
2462
  iconCover
2035
2463
  );
2036
- process.stdout.write(`${url}
2037
- `);
2038
2464
  }
2465
+ if (opts.file.length > 0) {
2466
+ const createdPageId = toUuid(parseNotionId(createdPageUrl));
2467
+ const blocks = await uploadFilesAsBlocks(opts.file, {}, client);
2468
+ await client.blocks.children.append({
2469
+ block_id: createdPageId,
2470
+ children: blocks
2471
+ });
2472
+ }
2473
+ process.stdout.write(`${createdPageUrl}
2474
+ `);
2039
2475
  })
2040
2476
  );
2041
2477
  return cmd;
2042
2478
  }
2043
2479
 
2044
2480
  // src/commands/db/create.ts
2045
- import { Command as Command10 } from "commander";
2046
- function collectProps2(val, acc) {
2481
+ import { Command as Command11 } from "commander";
2482
+ function collectProps(val, acc) {
2047
2483
  acc.push(val);
2048
2484
  return acc;
2049
2485
  }
2050
2486
  function dbCreateCommand() {
2051
- return new Command10("create").description("Create a new database under a parent page").requiredOption("--parent <id/url>", "parent page ID or URL").requiredOption("--title <title>", "database title").option(
2487
+ return new Command11("create").description("Create a new database under a parent page").requiredOption("--parent <id/url>", "parent page ID or URL").requiredOption("--title <title>", "database title").option(
2052
2488
  "--prop <definition>",
2053
2489
  'property definition (repeatable): --prop "Name:type:options"',
2054
- collectProps2,
2490
+ collectProps,
2055
2491
  []
2056
2492
  ).action(
2057
2493
  withErrorHandling(async (opts) => {
@@ -2080,7 +2516,7 @@ function dbCreateCommand() {
2080
2516
  }
2081
2517
 
2082
2518
  // src/commands/db/query.ts
2083
- import { Command as Command11 } from "commander";
2519
+ import { Command as Command12 } from "commander";
2084
2520
  var SKIP_TYPES_IN_AUTO = /* @__PURE__ */ new Set(["relation", "rich_text", "people"]);
2085
2521
  function autoSelectColumns(schema, entries) {
2086
2522
  const termWidth = process.stdout.columns || 120;
@@ -2108,7 +2544,7 @@ function autoSelectColumns(schema, entries) {
2108
2544
  return selected;
2109
2545
  }
2110
2546
  function dbQueryCommand() {
2111
- return new Command11("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option(
2547
+ return new Command12("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option(
2112
2548
  "--filter <filter>",
2113
2549
  'Filter entries (repeatable): --filter "Status=Done"',
2114
2550
  collect,
@@ -2163,9 +2599,9 @@ function collect(value, previous) {
2163
2599
  }
2164
2600
 
2165
2601
  // src/commands/db/schema.ts
2166
- import { Command as Command12 } from "commander";
2602
+ import { Command as Command13 } from "commander";
2167
2603
  function dbSchemaCommand() {
2168
- return new Command12("schema").description("Show database schema (property names, types, and options)").argument("<id>", "Notion database ID or URL").action(
2604
+ return new Command13("schema").description("Show database schema (property names, types, and options)").argument("<id>", "Notion database ID or URL").action(
2169
2605
  withErrorHandling(async (id) => {
2170
2606
  const { token } = await resolveToken();
2171
2607
  const client = createNotionClient(token);
@@ -2188,14 +2624,210 @@ function dbSchemaCommand() {
2188
2624
  );
2189
2625
  }
2190
2626
 
2627
+ // src/commands/db/update.ts
2628
+ import { Command as Command14 } from "commander";
2629
+ function collectProps2(val, acc) {
2630
+ acc.push(val);
2631
+ return acc;
2632
+ }
2633
+ function dbUpdateCommand() {
2634
+ return new Command14("update").description(
2635
+ "Update database schema (add/remove/rename properties, manage options)"
2636
+ ).argument("<id/url>", "database ID or URL").option(
2637
+ "--add-prop <definition>",
2638
+ 'add property (repeatable): --add-prop "Name:type:options"',
2639
+ collectProps2,
2640
+ []
2641
+ ).option(
2642
+ "--remove-prop <name>",
2643
+ "remove property (repeatable)",
2644
+ collectProps2,
2645
+ []
2646
+ ).option(
2647
+ "--rename-prop <old:new>",
2648
+ 'rename property (repeatable): --rename-prop "Old:New"',
2649
+ collectProps2,
2650
+ []
2651
+ ).option(
2652
+ "--set-options <prop:opts>",
2653
+ 'set select/multi_select options (repeatable): --set-options "Status:A,B,C"',
2654
+ collectProps2,
2655
+ []
2656
+ ).option("--title <title>", "update database title").action(
2657
+ withErrorHandling(async (id, opts) => {
2658
+ const hasOperations = opts.addProp.length > 0 || opts.removeProp.length > 0 || opts.renameProp.length > 0 || opts.setOptions.length > 0 || opts.title !== void 0;
2659
+ if (!hasOperations) {
2660
+ throw new CliError(
2661
+ ErrorCodes.INVALID_ARG,
2662
+ "No update operations specified",
2663
+ "Provide at least one of: --add-prop, --remove-prop, --rename-prop, --set-options, --title"
2664
+ );
2665
+ }
2666
+ const { token, source } = await resolveToken();
2667
+ reportTokenSource(source);
2668
+ const client = createNotionClient(token);
2669
+ const dsId = await resolveDataSourceId(client, id);
2670
+ const needsSchema = opts.renameProp.length > 0 || opts.setOptions.length > 0;
2671
+ const schema = needsSchema ? await fetchDatabaseSchema(client, dsId) : { id: dsId, databaseId: dsId, title: "", properties: {} };
2672
+ const payload = buildDatabaseUpdatePayload(
2673
+ {
2674
+ addProps: opts.addProp,
2675
+ removeProps: opts.removeProp,
2676
+ renameProps: opts.renameProp,
2677
+ setOptions: opts.setOptions,
2678
+ title: opts.title
2679
+ },
2680
+ schema
2681
+ );
2682
+ const response = await updateDatabaseSchema(client, dsId, payload);
2683
+ if (getOutputMode() === "json") {
2684
+ process.stdout.write(`${formatJSON(response)}
2685
+ `);
2686
+ return;
2687
+ }
2688
+ if ("url" in response) {
2689
+ process.stdout.write(`${response.url}
2690
+ `);
2691
+ }
2692
+ })
2693
+ );
2694
+ }
2695
+
2696
+ // src/commands/db/update-rows.ts
2697
+ import { Command as Command15 } from "commander";
2698
+ var LARGE_BATCH_WARNING_THRESHOLD = 10;
2699
+ var DEFAULT_CONCURRENCY = 3;
2700
+ function collectProps3(val, acc) {
2701
+ acc.push(val);
2702
+ return acc;
2703
+ }
2704
+ async function batchUpdate(items, fn, concurrency = DEFAULT_CONCURRENCY) {
2705
+ const results = [];
2706
+ for (let i = 0; i < items.length; i += concurrency) {
2707
+ const batch = items.slice(i, i + concurrency);
2708
+ results.push(...await Promise.all(batch.map(fn)));
2709
+ }
2710
+ return results;
2711
+ }
2712
+ function getPageTitle(page) {
2713
+ for (const prop of Object.values(page.properties)) {
2714
+ if (prop.type === "title") {
2715
+ return prop.title.map((r) => r.plain_text).join("") || page.id;
2716
+ }
2717
+ }
2718
+ return page.id;
2719
+ }
2720
+ function dbUpdateRowsCommand() {
2721
+ return new Command15("update-rows").description("Update properties on all matching rows in a database").argument("<id>", "Notion database ID or URL").option(
2722
+ "--filter <filter>",
2723
+ 'Filter rows (repeatable): --filter "Status=Done"',
2724
+ collectProps3,
2725
+ []
2726
+ ).option(
2727
+ "--prop <property=value>",
2728
+ "Set a property value on matching rows (repeatable, required)",
2729
+ collectProps3,
2730
+ []
2731
+ ).option("--dry-run", "Preview matching rows without making changes", false).action(
2732
+ withErrorHandling(async (id, opts) => {
2733
+ if (opts.prop.length === 0) {
2734
+ throw new CliError(
2735
+ ErrorCodes.INVALID_ARG,
2736
+ "No properties to update.",
2737
+ 'Provide at least one --prop "Name=Value"'
2738
+ );
2739
+ }
2740
+ const { token } = await resolveToken();
2741
+ const client = createNotionClient(token);
2742
+ const dsId = await resolveDataSourceId(client, id);
2743
+ const schema = await fetchDatabaseSchema(client, dsId);
2744
+ const filter = opts.filter.length ? buildFilter(opts.filter, schema) : void 0;
2745
+ const entries = await queryDatabase(client, dsId, { filter });
2746
+ if (opts.filter.length === 0 && entries.length > LARGE_BATCH_WARNING_THRESHOLD) {
2747
+ process.stderr.write(
2748
+ `Warning: no --filter specified \u2014 ${entries.length} rows will be updated.
2749
+ `
2750
+ );
2751
+ }
2752
+ if (opts.dryRun) {
2753
+ process.stderr.write(
2754
+ `Dry run: would update ${entries.length} row(s).
2755
+ `
2756
+ );
2757
+ const lines = entries.map((e) => `${e.id} ${getPageTitle(e.raw)}`).join("\n");
2758
+ if (entries.length > 0) {
2759
+ process.stdout.write(`${lines}
2760
+ `);
2761
+ }
2762
+ return;
2763
+ }
2764
+ const results = await batchUpdate(entries, async (entry) => {
2765
+ const title = getPageTitle(entry.raw);
2766
+ try {
2767
+ const properties = buildPropertiesPayload(
2768
+ opts.prop,
2769
+ entry.raw
2770
+ );
2771
+ await updatePageProperties(client, entry.id, properties);
2772
+ return { id: entry.id, title, success: true };
2773
+ } catch (err) {
2774
+ const message = err instanceof Error ? err.message : String(err);
2775
+ return { id: entry.id, title, success: false, error: message };
2776
+ }
2777
+ });
2778
+ const successCount = results.filter((r) => r.success).length;
2779
+ const failureCount = results.length - successCount;
2780
+ if (getOutputMode() === "json") {
2781
+ process.stdout.write(`${formatJSON(results)}
2782
+ `);
2783
+ return;
2784
+ }
2785
+ process.stdout.write(`Updated ${successCount} row(s).
2786
+ `);
2787
+ if (failureCount > 0) {
2788
+ process.stderr.write(`${failureCount} row(s) failed to update.
2789
+ `);
2790
+ for (const r of results.filter((res) => !res.success)) {
2791
+ process.stderr.write(` ${r.id} (${r.title}): ${r.error}
2792
+ `);
2793
+ }
2794
+ }
2795
+ })
2796
+ );
2797
+ }
2798
+
2799
+ // src/commands/delete-block.ts
2800
+ import { Command as Command16 } from "commander";
2801
+ function deleteBlockCommand() {
2802
+ const cmd = new Command16("delete-block");
2803
+ cmd.description("delete a Notion block by ID or URL").argument("<id/url>", "Notion block ID or URL").action(
2804
+ withErrorHandling(async (idOrUrl) => {
2805
+ const { token, source } = await resolveToken();
2806
+ reportTokenSource(source);
2807
+ const client = createNotionClient(token);
2808
+ const id = parseNotionId(idOrUrl);
2809
+ const uuid = toUuid(id);
2810
+ const deletedBlock = await client.blocks.delete({ block_id: uuid });
2811
+ const mode = getOutputMode();
2812
+ if (mode === "json") {
2813
+ process.stdout.write(`${formatJSON(deletedBlock)}
2814
+ `);
2815
+ } else {
2816
+ process.stdout.write("Block deleted.\n");
2817
+ }
2818
+ })
2819
+ );
2820
+ return cmd;
2821
+ }
2822
+
2191
2823
  // src/commands/edit-page.ts
2192
- import { Command as Command13 } from "commander";
2824
+ import { Command as Command17 } from "commander";
2193
2825
  function collect2(val, acc) {
2194
2826
  acc.push(val);
2195
2827
  return acc;
2196
2828
  }
2197
2829
  function editPageCommand() {
2198
- const cmd = new Command13("edit-page");
2830
+ const cmd = new Command17("edit-page");
2199
2831
  cmd.description(
2200
2832
  "replace a Notion page's content \u2014 full page or a targeted section"
2201
2833
  ).argument("<id/url>", "Notion page ID or URL").option(
@@ -2293,7 +2925,7 @@ function editPageCommand() {
2293
2925
 
2294
2926
  // src/commands/init.ts
2295
2927
  import { confirm, input as input2, password } from "@inquirer/prompts";
2296
- import { Command as Command14 } from "commander";
2928
+ import { Command as Command18 } from "commander";
2297
2929
  async function runInitFlow() {
2298
2930
  const profileName = await input2({
2299
2931
  message: "Profile name:",
@@ -2373,7 +3005,7 @@ async function runInitFlow() {
2373
3005
  stderrWrite(dim(" notion auth login"));
2374
3006
  }
2375
3007
  function initCommand() {
2376
- const cmd = new Command14("init");
3008
+ const cmd = new Command18("init");
2377
3009
  cmd.description("authenticate with Notion and save a profile").action(
2378
3010
  withErrorHandling(async () => {
2379
3011
  if (!process.stdin.isTTY) {
@@ -2391,7 +3023,7 @@ function initCommand() {
2391
3023
 
2392
3024
  // src/commands/ls.ts
2393
3025
  import { isFullPageOrDataSource } from "@notionhq/client";
2394
- import { Command as Command15 } from "commander";
3026
+ import { Command as Command19 } from "commander";
2395
3027
  function getTitle(item) {
2396
3028
  if (item.object === "data_source") {
2397
3029
  return item.title.map((t) => t.plain_text).join("") || "(untitled)";
@@ -2408,7 +3040,7 @@ function displayType(item) {
2408
3040
  return item.object === "data_source" ? "database" : item.object;
2409
3041
  }
2410
3042
  function lsCommand() {
2411
- const cmd = new Command15("ls");
3043
+ const cmd = new Command19("ls");
2412
3044
  cmd.description("list accessible Notion pages and databases").option(
2413
3045
  "--type <type>",
2414
3046
  "filter by object type (page or database)",
@@ -2457,7 +3089,11 @@ function lsCommand() {
2457
3089
  );
2458
3090
  }
2459
3091
  if (items.length === 0) {
2460
- process.stdout.write("No accessible content found\n");
3092
+ if (getOutputMode() === "json") {
3093
+ process.stdout.write("[]\n");
3094
+ } else {
3095
+ process.stdout.write("No accessible content found\n");
3096
+ }
2461
3097
  return;
2462
3098
  }
2463
3099
  const headers = ["TYPE", "TITLE", "ID", "MODIFIED"];
@@ -2482,9 +3118,9 @@ function lsCommand() {
2482
3118
  }
2483
3119
 
2484
3120
  // src/commands/move.ts
2485
- import { Command as Command16 } from "commander";
3121
+ import { Command as Command20 } from "commander";
2486
3122
  function moveCommand() {
2487
- const cmd = new Command16("move");
3123
+ const cmd = new Command20("move");
2488
3124
  cmd.description("move pages to a new parent").argument("<ids/urls...>", "Notion page IDs or URLs to move").option("--to <id/url>", "target page parent ID or URL").option(
2489
3125
  "--to-db <id/url>",
2490
3126
  "target database parent ID or URL (resolves to data source)"
@@ -2542,10 +3178,10 @@ function moveCommand() {
2542
3178
  // src/commands/open.ts
2543
3179
  import { exec } from "child_process";
2544
3180
  import { promisify } from "util";
2545
- import { Command as Command17 } from "commander";
3181
+ import { Command as Command21 } from "commander";
2546
3182
  var execAsync = promisify(exec);
2547
3183
  function openCommand() {
2548
- const cmd = new Command17("open");
3184
+ const cmd = new Command21("open");
2549
3185
  cmd.description("open a Notion page in the default browser").argument("<id/url>", "Notion page ID or URL").action(
2550
3186
  withErrorHandling(async (idOrUrl) => {
2551
3187
  const id = parseNotionId(idOrUrl);
@@ -2561,9 +3197,9 @@ function openCommand() {
2561
3197
  }
2562
3198
 
2563
3199
  // src/commands/profile/list.ts
2564
- import { Command as Command18 } from "commander";
3200
+ import { Command as Command22 } from "commander";
2565
3201
  function profileListCommand() {
2566
- const cmd = new Command18("list");
3202
+ const cmd = new Command22("list");
2567
3203
  cmd.description("list all authentication profiles").action(
2568
3204
  withErrorHandling(async () => {
2569
3205
  const config = await readGlobalConfig();
@@ -2592,9 +3228,9 @@ function profileListCommand() {
2592
3228
  }
2593
3229
 
2594
3230
  // src/commands/profile/remove.ts
2595
- import { Command as Command19 } from "commander";
3231
+ import { Command as Command23 } from "commander";
2596
3232
  function profileRemoveCommand() {
2597
- const cmd = new Command19("remove");
3233
+ const cmd = new Command23("remove");
2598
3234
  cmd.description("remove an authentication profile").argument("<name>", "profile name to remove").action(
2599
3235
  withErrorHandling(async (name) => {
2600
3236
  const config = await readGlobalConfig();
@@ -2620,9 +3256,9 @@ function profileRemoveCommand() {
2620
3256
  }
2621
3257
 
2622
3258
  // src/commands/profile/use.ts
2623
- import { Command as Command20 } from "commander";
3259
+ import { Command as Command24 } from "commander";
2624
3260
  function profileUseCommand() {
2625
- const cmd = new Command20("use");
3261
+ const cmd = new Command24("use");
2626
3262
  cmd.description("switch the active profile").argument("<name>", "profile name to activate").action(
2627
3263
  withErrorHandling(async (name) => {
2628
3264
  const config = await readGlobalConfig();
@@ -2645,7 +3281,7 @@ function profileUseCommand() {
2645
3281
  }
2646
3282
 
2647
3283
  // src/commands/read.ts
2648
- import { Command as Command21 } from "commander";
3284
+ import { Command as Command25 } from "commander";
2649
3285
 
2650
3286
  // src/output/markdown.ts
2651
3287
  import { Chalk as Chalk2 } from "chalk";
@@ -2775,17 +3411,71 @@ function renderInline(text) {
2775
3411
  }
2776
3412
 
2777
3413
  // src/services/page.service.ts
3414
+ var UNKNOWN_BOOKMARK_REGEX = /<unknown url="([^"]*)" alt="bookmark"\/>/g;
3415
+ function extractBlockIdFromUrl(url) {
3416
+ const fragment = new URL(url).hash.slice(1);
3417
+ const hex = fragment.replace(/-/g, "");
3418
+ return /^[0-9a-f]{32}$/i.test(hex) ? hex : null;
3419
+ }
3420
+ function richTextToPlainText(richText) {
3421
+ return richText.map((rt) => rt.plain_text ?? "").join("");
3422
+ }
3423
+ async function resolveUnknownBookmarks(client, markdown) {
3424
+ const matches = [...markdown.matchAll(UNKNOWN_BOOKMARK_REGEX)];
3425
+ if (matches.length === 0) return markdown;
3426
+ const tagToBlockId = /* @__PURE__ */ new Map();
3427
+ for (const match of matches) {
3428
+ const tag = match[0];
3429
+ if (tagToBlockId.has(tag)) continue;
3430
+ try {
3431
+ const blockId = extractBlockIdFromUrl(match[1]);
3432
+ tagToBlockId.set(tag, blockId);
3433
+ } catch {
3434
+ tagToBlockId.set(tag, null);
3435
+ }
3436
+ }
3437
+ const blockIdToReplacement = /* @__PURE__ */ new Map();
3438
+ await Promise.all(
3439
+ [...new Set(tagToBlockId.values())].map(async (blockId) => {
3440
+ if (!blockId) return;
3441
+ try {
3442
+ const block = await client.blocks.retrieve({
3443
+ block_id: toUuid(blockId)
3444
+ });
3445
+ if (block.type !== "bookmark" || !block.bookmark) return;
3446
+ const { url, caption } = block.bookmark;
3447
+ const captionText = richTextToPlainText(caption).trim();
3448
+ const replacement = captionText ? `[${captionText}](${url})` : url;
3449
+ blockIdToReplacement.set(blockId, replacement);
3450
+ } catch {
3451
+ }
3452
+ })
3453
+ );
3454
+ return markdown.replace(UNKNOWN_BOOKMARK_REGEX, (tag, notionUrl) => {
3455
+ try {
3456
+ const blockId = extractBlockIdFromUrl(notionUrl);
3457
+ if (!blockId) return tag;
3458
+ return blockIdToReplacement.get(blockId) ?? tag;
3459
+ } catch {
3460
+ return tag;
3461
+ }
3462
+ });
3463
+ }
2778
3464
  async function fetchPageMarkdown(client, pageId) {
2779
3465
  const [page, markdownResponse] = await Promise.all([
2780
3466
  client.pages.retrieve({ page_id: pageId }),
2781
3467
  client.pages.retrieveMarkdown({ page_id: pageId })
2782
3468
  ]);
2783
- return { page, markdown: markdownResponse.markdown };
3469
+ const markdown = await resolveUnknownBookmarks(
3470
+ client,
3471
+ markdownResponse.markdown
3472
+ );
3473
+ return { page, markdown };
2784
3474
  }
2785
3475
 
2786
3476
  // src/commands/read.ts
2787
3477
  function readCommand() {
2788
- return new Command21("read").description("Read a Notion page as markdown").argument("<id>", "Notion page ID or URL").action(
3478
+ return new Command25("read").description("Read a Notion page as markdown").argument("<id>", "Notion page ID or URL").action(
2789
3479
  withErrorHandling(async (id) => {
2790
3480
  const { token } = await resolveToken();
2791
3481
  const client = createNotionClient(token);
@@ -2811,7 +3501,7 @@ function readCommand() {
2811
3501
 
2812
3502
  // src/commands/search.ts
2813
3503
  import { isFullPageOrDataSource as isFullPageOrDataSource2 } from "@notionhq/client";
2814
- import { Command as Command22 } from "commander";
3504
+ import { Command as Command26 } from "commander";
2815
3505
  function getTitle2(item) {
2816
3506
  if (item.object === "data_source") {
2817
3507
  return item.title.map((t) => t.plain_text).join("") || "(untitled)";
@@ -2831,7 +3521,7 @@ function displayType2(item) {
2831
3521
  return item.object === "data_source" ? "database" : item.object;
2832
3522
  }
2833
3523
  function searchCommand() {
2834
- const cmd = new Command22("search");
3524
+ const cmd = new Command26("search");
2835
3525
  cmd.description("search Notion workspace by keyword").argument("<query>", "search keyword").option(
2836
3526
  "--type <type>",
2837
3527
  "filter by object type (page or database)",
@@ -2876,8 +3566,12 @@ function searchCommand() {
2876
3566
  (r) => isFullPageOrDataSource2(r)
2877
3567
  );
2878
3568
  if (fullResults.length === 0) {
2879
- process.stdout.write(`No results found for "${query}"
3569
+ if (getOutputMode() === "json") {
3570
+ process.stdout.write("[]\n");
3571
+ } else {
3572
+ process.stdout.write(`No results found for "${query}"
2880
3573
  `);
3574
+ }
2881
3575
  return;
2882
3576
  }
2883
3577
  const headers = ["TYPE", "TITLE", "ID", "MODIFIED"];
@@ -2904,7 +3598,7 @@ function searchCommand() {
2904
3598
  // src/commands/skill.ts
2905
3599
  import {
2906
3600
  copyFileSync,
2907
- existsSync,
3601
+ existsSync as existsSync4,
2908
3602
  mkdirSync,
2909
3603
  readFileSync,
2910
3604
  realpathSync
@@ -2912,7 +3606,7 @@ import {
2912
3606
  import { homedir as homedir2 } from "os";
2913
3607
  import { dirname, join as join3 } from "path";
2914
3608
  import chalk from "chalk";
2915
- import { Command as Command23 } from "commander";
3609
+ import { Command as Command27 } from "commander";
2916
3610
  function skillPath() {
2917
3611
  if (!process.argv[1]) {
2918
3612
  throw new Error("Cannot determine install path. Run with: notion skill");
@@ -2931,7 +3625,7 @@ function skillPath() {
2931
3625
  )
2932
3626
  ];
2933
3627
  for (const c2 of candidates) {
2934
- if (existsSync(c2)) return c2;
3628
+ if (existsSync4(c2)) return c2;
2935
3629
  }
2936
3630
  throw new Error(
2937
3631
  "SKILL.md not found. Reinstall with: npm install -g @andrzejchm/notion-cli"
@@ -2952,11 +3646,11 @@ function getAgentTargets() {
2952
3646
  ];
2953
3647
  return targets.map((t) => ({
2954
3648
  ...t,
2955
- detected: existsSync(dirname(t.dir))
3649
+ detected: existsSync4(dirname(t.dir))
2956
3650
  }));
2957
3651
  }
2958
3652
  function installTo(source, target) {
2959
- if (!existsSync(target.dir)) mkdirSync(target.dir, { recursive: true });
3653
+ if (!existsSync4(target.dir)) mkdirSync(target.dir, { recursive: true });
2960
3654
  const dest = join3(target.dir, "SKILL.md");
2961
3655
  copyFileSync(source, dest);
2962
3656
  return dest;
@@ -2999,7 +3693,7 @@ function installNonInteractive(source, targets) {
2999
3693
  }
3000
3694
  }
3001
3695
  function skillCommand() {
3002
- const cmd = new Command23("skill");
3696
+ const cmd = new Command27("skill");
3003
3697
  cmd.description("Install the agent skill file for your coding agents").option("--print", "Print the skill file content instead of installing").option("--path <path>", "Install to a specific file path").action(
3004
3698
  withErrorHandling(async (opts) => {
3005
3699
  if (opts.print) {
@@ -3009,7 +3703,7 @@ function skillCommand() {
3009
3703
  const source = skillPath();
3010
3704
  if (opts.path) {
3011
3705
  const dir = dirname(opts.path);
3012
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
3706
+ if (!existsSync4(dir)) mkdirSync(dir, { recursive: true });
3013
3707
  copyFileSync(source, opts.path);
3014
3708
  process.stdout.write(`Installed to ${opts.path}
3015
3709
  `);
@@ -3028,17 +3722,17 @@ function skillCommand() {
3028
3722
  }
3029
3723
 
3030
3724
  // src/commands/update.ts
3031
- import { Command as Command24 } from "commander";
3032
- function collectProps3(val, acc) {
3725
+ import { Command as Command28 } from "commander";
3726
+ function collectProps4(val, acc) {
3033
3727
  acc.push(val);
3034
3728
  return acc;
3035
3729
  }
3036
3730
  function updateCommand() {
3037
- const cmd = new Command24("update");
3731
+ const cmd = new Command28("update");
3038
3732
  cmd.description("update properties on a Notion page").argument("<id/url>", "Notion page ID or URL").option(
3039
3733
  "--prop <property=value>",
3040
3734
  "set a property value (repeatable)",
3041
- collectProps3,
3735
+ collectProps4,
3042
3736
  []
3043
3737
  ).option("--title <title>", "set the page title").action(
3044
3738
  withErrorHandling(async (idOrUrl, opts) => {
@@ -3092,7 +3786,7 @@ function updateCommand() {
3092
3786
  }
3093
3787
 
3094
3788
  // src/commands/users.ts
3095
- import { Command as Command25 } from "commander";
3789
+ import { Command as Command29 } from "commander";
3096
3790
  function getEmailOrWorkspace(user) {
3097
3791
  if (user.type === "person") {
3098
3792
  return user.person.email ?? "\u2014";
@@ -3104,7 +3798,7 @@ function getEmailOrWorkspace(user) {
3104
3798
  return "\u2014";
3105
3799
  }
3106
3800
  function usersCommand() {
3107
- const cmd = new Command25("users");
3801
+ const cmd = new Command29("users");
3108
3802
  cmd.description("list all users in the workspace").option("--json", "output as JSON").action(
3109
3803
  withErrorHandling(async (opts) => {
3110
3804
  if (opts.json) setOutputMode("json");
@@ -3135,7 +3829,7 @@ var __dirname = dirname2(__filename);
3135
3829
  var pkg = JSON.parse(
3136
3830
  readFileSync2(join4(__dirname, "../package.json"), "utf-8")
3137
3831
  );
3138
- var program = new Command26();
3832
+ var program = new Command30();
3139
3833
  program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
3140
3834
  program.option("--verbose", "show API requests/responses").option("--color", "force color output").option("--json", "force JSON output (overrides TTY detection)").option("--md", "force markdown output for page content");
3141
3835
  program.configureOutput({
@@ -3156,7 +3850,7 @@ program.hook("preAction", (thisCommand) => {
3156
3850
  setOutputMode("md");
3157
3851
  }
3158
3852
  });
3159
- var authCmd = new Command26("auth").description("manage Notion authentication");
3853
+ var authCmd = new Command30("auth").description("manage Notion authentication");
3160
3854
  authCmd.action(authDefaultAction(authCmd));
3161
3855
  authCmd.addCommand(loginCommand());
3162
3856
  authCmd.addCommand(logoutCommand());
@@ -3166,7 +3860,7 @@ authCmd.addCommand(profileUseCommand());
3166
3860
  authCmd.addCommand(profileRemoveCommand());
3167
3861
  program.addCommand(authCmd);
3168
3862
  program.addCommand(initCommand(), { hidden: true });
3169
- var profileCmd = new Command26("profile").description(
3863
+ var profileCmd = new Command30("profile").description(
3170
3864
  "manage authentication profiles"
3171
3865
  );
3172
3866
  profileCmd.addCommand(profileListCommand());
@@ -3181,15 +3875,19 @@ program.addCommand(commentsCommand());
3181
3875
  program.addCommand(readCommand());
3182
3876
  program.addCommand(commentAddCommand());
3183
3877
  program.addCommand(appendCommand());
3878
+ program.addCommand(attachCommand());
3184
3879
  program.addCommand(createPageCommand());
3185
3880
  program.addCommand(editPageCommand());
3186
3881
  program.addCommand(updateCommand());
3187
3882
  program.addCommand(archiveCommand());
3883
+ program.addCommand(deleteBlockCommand());
3188
3884
  program.addCommand(moveCommand());
3189
- var dbCmd = new Command26("db").description("Database operations");
3885
+ var dbCmd = new Command30("db").description("Database operations");
3190
3886
  dbCmd.addCommand(dbCreateCommand());
3191
3887
  dbCmd.addCommand(dbSchemaCommand());
3192
3888
  dbCmd.addCommand(dbQueryCommand());
3889
+ dbCmd.addCommand(dbUpdateCommand());
3890
+ dbCmd.addCommand(dbUpdateRowsCommand());
3193
3891
  program.addCommand(dbCmd);
3194
3892
  program.addCommand(completionCommand());
3195
3893
  program.addCommand(skillCommand());