@andrzejchm/notion-cli 0.11.0 → 0.12.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 Command27 } from "commander";
8
8
 
9
9
  // src/commands/append.ts
10
10
  import { Command } from "commander";
@@ -542,7 +542,196 @@ function reportTokenSource(source) {
542
542
  stderrWrite(dim(`Using token from ${source}`));
543
543
  }
544
544
 
545
+ // src/services/upload.service.ts
546
+ import { createReadStream, existsSync, statSync } from "fs";
547
+ import { basename, extname } from "path";
548
+ var PART_SIZE = 20 * 1024 * 1024;
549
+ var MIME_MAP = {
550
+ ".png": "image/png",
551
+ ".jpg": "image/jpeg",
552
+ ".jpeg": "image/jpeg",
553
+ ".gif": "image/gif",
554
+ ".webp": "image/webp",
555
+ ".svg": "image/svg+xml",
556
+ ".bmp": "image/bmp",
557
+ ".ico": "image/x-icon",
558
+ ".tiff": "image/tiff",
559
+ ".tif": "image/tiff",
560
+ ".heic": "image/heic",
561
+ ".avif": "image/avif",
562
+ ".pdf": "application/pdf",
563
+ ".mp3": "audio/mpeg",
564
+ ".wav": "audio/wav",
565
+ ".ogg": "audio/ogg",
566
+ ".m4a": "audio/mp4",
567
+ ".flac": "audio/flac",
568
+ ".aac": "audio/aac",
569
+ ".mp4": "video/mp4",
570
+ ".mov": "video/quicktime",
571
+ ".avi": "video/x-msvideo",
572
+ ".webm": "video/webm",
573
+ ".mkv": "video/x-matroska",
574
+ ".csv": "text/csv",
575
+ ".json": "application/json",
576
+ ".txt": "text/plain",
577
+ ".md": "text/markdown",
578
+ ".html": "text/html",
579
+ ".xml": "application/xml",
580
+ ".zip": "application/zip",
581
+ ".gz": "application/gzip",
582
+ ".doc": "application/msword",
583
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
584
+ ".xls": "application/vnd.ms-excel",
585
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
586
+ ".ppt": "application/vnd.ms-powerpoint",
587
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
588
+ };
589
+ function detectMimeType(filePath) {
590
+ const ext = extname(filePath).toLowerCase();
591
+ return MIME_MAP[ext] ?? "application/octet-stream";
592
+ }
593
+ function resolveBlockType(contentType) {
594
+ const base = contentType.split(";")[0].trim().toLowerCase();
595
+ if (base.startsWith("image/")) return "image";
596
+ if (base.startsWith("audio/")) return "audio";
597
+ if (base.startsWith("video/")) return "video";
598
+ if (base === "application/pdf") return "pdf";
599
+ return "file";
600
+ }
601
+ async function readStreamChunk(filePath, start, end) {
602
+ return new Promise((resolve, reject) => {
603
+ const chunks = [];
604
+ const stream = createReadStream(filePath, { start, end });
605
+ stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
606
+ stream.on("end", () => resolve(Buffer.concat(chunks)));
607
+ stream.on("error", reject);
608
+ });
609
+ }
610
+ async function uploadSinglePart(client, filePath, filename, contentType, fileSize) {
611
+ const upload = await client.fileUploads.create({
612
+ mode: "single_part",
613
+ filename,
614
+ content_type: contentType
615
+ });
616
+ const data = await readStreamChunk(filePath, 0, fileSize - 1);
617
+ const blob = new Blob(
618
+ [
619
+ data.buffer.slice(
620
+ data.byteOffset,
621
+ data.byteOffset + data.byteLength
622
+ )
623
+ ],
624
+ { type: contentType }
625
+ );
626
+ await client.fileUploads.send({
627
+ file_upload_id: upload.id,
628
+ file: { data: blob, filename }
629
+ });
630
+ return upload.id;
631
+ }
632
+ async function uploadMultiPart(client, filePath, filename, contentType, fileSize) {
633
+ const numberOfParts = Math.ceil(fileSize / PART_SIZE);
634
+ const upload = await client.fileUploads.create({
635
+ mode: "multi_part",
636
+ filename,
637
+ content_type: contentType,
638
+ number_of_parts: numberOfParts
639
+ });
640
+ for (let part = 0; part < numberOfParts; part++) {
641
+ const start = part * PART_SIZE;
642
+ const end = Math.min(start + PART_SIZE, fileSize) - 1;
643
+ const data = await readStreamChunk(filePath, start, end);
644
+ const blob = new Blob(
645
+ [
646
+ data.buffer.slice(
647
+ data.byteOffset,
648
+ data.byteOffset + data.byteLength
649
+ )
650
+ ],
651
+ { type: contentType }
652
+ );
653
+ await client.fileUploads.send({
654
+ file_upload_id: upload.id,
655
+ file: { data: blob, filename },
656
+ part_number: String(part + 1)
657
+ });
658
+ }
659
+ await client.fileUploads.complete({ file_upload_id: upload.id });
660
+ return upload.id;
661
+ }
662
+ async function uploadFile(client, filePath) {
663
+ const filename = basename(filePath);
664
+ const contentType = detectMimeType(filePath);
665
+ const { size: fileSize } = statSync(filePath);
666
+ if (fileSize === 0) {
667
+ throw new CliError(
668
+ ErrorCodes.INVALID_ARG,
669
+ `Cannot upload empty file: ${filename}`,
670
+ "Provide a file with content"
671
+ );
672
+ }
673
+ const fileUploadId = fileSize <= PART_SIZE ? await uploadSinglePart(
674
+ client,
675
+ filePath,
676
+ filename,
677
+ contentType,
678
+ fileSize
679
+ ) : await uploadMultiPart(
680
+ client,
681
+ filePath,
682
+ filename,
683
+ contentType,
684
+ fileSize
685
+ );
686
+ return { fileUploadId, filename, contentType };
687
+ }
688
+ function buildFileBlock(fileUploadId, blockType, caption) {
689
+ const captionRichText = caption ? [{ type: "text", text: { content: caption } }] : [];
690
+ const fileContent = {
691
+ file_upload: { id: fileUploadId },
692
+ type: "file_upload",
693
+ caption: captionRichText
694
+ };
695
+ switch (blockType) {
696
+ case "image":
697
+ return { type: "image", image: fileContent };
698
+ case "audio":
699
+ return { type: "audio", audio: fileContent };
700
+ case "video":
701
+ return { type: "video", video: fileContent };
702
+ case "pdf":
703
+ return { type: "pdf", pdf: fileContent };
704
+ case "file":
705
+ return { type: "file", file: fileContent };
706
+ default:
707
+ throw new CliError(
708
+ ErrorCodes.INVALID_ARG,
709
+ `Unknown block type: ${blockType}`,
710
+ "Valid types are: image, file, pdf, audio, video"
711
+ );
712
+ }
713
+ }
714
+ async function uploadFilesAsBlocks(files, opts, client) {
715
+ for (const filePath of files) {
716
+ if (!existsSync(filePath)) {
717
+ throw new CliError(
718
+ ErrorCodes.INVALID_ARG,
719
+ `File not found: ${filePath}`,
720
+ "Provide a valid file path"
721
+ );
722
+ }
723
+ }
724
+ return Promise.all(
725
+ files.map(async (filePath) => {
726
+ const result = await uploadFile(client, filePath);
727
+ const blockType = opts.type ?? resolveBlockType(result.contentType);
728
+ return buildFileBlock(result.fileUploadId, blockType, opts.caption);
729
+ })
730
+ );
731
+ }
732
+
545
733
  // src/services/write.service.ts
734
+ import { existsSync as existsSync2 } from "fs";
546
735
  async function addComment(client, target, text, options = {}) {
547
736
  const richText = [
548
737
  {
@@ -661,22 +850,44 @@ async function replaceMarkdown(client, pageId, newMarkdown, options) {
661
850
  }
662
851
  });
663
852
  }
664
- function buildIconCover(options) {
853
+ async function buildIconCover(client, options) {
665
854
  const result = {};
666
855
  if (options?.icon) {
667
856
  const isUrl = /^https?:\/\//i.test(options.icon);
668
857
  if (isUrl) {
669
858
  result.icon = { type: "external", external: { url: options.icon } };
859
+ } else if (existsSync2(options.icon)) {
860
+ const uploaded = await uploadFile(client, options.icon);
861
+ result.icon = {
862
+ type: "file_upload",
863
+ file_upload: { id: uploaded.fileUploadId }
864
+ };
670
865
  } else {
671
866
  result.icon = { type: "emoji", emoji: options.icon };
672
867
  }
673
868
  }
674
869
  if (options?.cover) {
675
- result.cover = { type: "external", external: { url: options.cover } };
870
+ const isUrl = /^https?:\/\//i.test(options.cover);
871
+ if (isUrl) {
872
+ result.cover = { type: "external", external: { url: options.cover } };
873
+ } else if (existsSync2(options.cover)) {
874
+ const uploaded = await uploadFile(client, options.cover);
875
+ result.cover = {
876
+ type: "file_upload",
877
+ file_upload: { id: uploaded.fileUploadId }
878
+ };
879
+ } else {
880
+ throw new CliError(
881
+ ErrorCodes.INVALID_ARG,
882
+ `Cover not found: "${options.cover}" is not a valid URL or existing file path.`,
883
+ "Provide an http(s):// URL or a valid local file path for --cover"
884
+ );
885
+ }
676
886
  }
677
887
  return result;
678
888
  }
679
889
  async function createPage(client, parentId, title, markdown, options) {
890
+ const iconCover = await buildIconCover(client, options);
680
891
  const response = await client.pages.create({
681
892
  parent: { type: "page_id", page_id: parentId },
682
893
  properties: {
@@ -685,7 +896,7 @@ async function createPage(client, parentId, title, markdown, options) {
685
896
  }
686
897
  },
687
898
  ...markdown.trim() ? { markdown } : {},
688
- ...buildIconCover(options)
899
+ ...iconCover
689
900
  });
690
901
  const url = "url" in response ? response.url : response.id;
691
902
  return url;
@@ -697,11 +908,12 @@ async function createPageInDatabase(client, databaseId, titlePropName, title, ex
697
908
  title: [{ type: "text", text: { content: title, link: null } }]
698
909
  }
699
910
  };
911
+ const iconCover = await buildIconCover(client, options);
700
912
  const response = await client.pages.create({
701
913
  parent: { type: "database_id", database_id: databaseId },
702
914
  properties,
703
915
  ...markdown.trim() ? { markdown } : {},
704
- ...buildIconCover(options)
916
+ ...iconCover
705
917
  });
706
918
  const url = "url" in response ? response.url : response.id;
707
919
  return url;
@@ -717,54 +929,92 @@ async function readStdin() {
717
929
  }
718
930
 
719
931
  // src/commands/append.ts
932
+ function collectFiles(val, acc) {
933
+ acc.push(val);
934
+ return acc;
935
+ }
936
+ async function resolveMarkdown(message, hasFiles) {
937
+ if (message) return message;
938
+ if (!process.stdin.isTTY && !hasFiles) return readStdin();
939
+ if (!hasFiles) {
940
+ throw new CliError(
941
+ ErrorCodes.INVALID_ARG,
942
+ "No content to append.",
943
+ "Pass markdown via -m/--message, pipe it through stdin, or use --file to attach files"
944
+ );
945
+ }
946
+ return "";
947
+ }
948
+ async function appendMarkdownWithErrorHandling(client, uuid, markdown, after) {
949
+ try {
950
+ await appendMarkdown(client, uuid, markdown, after ? { after } : void 0);
951
+ } catch (error2) {
952
+ if (after && isNotionValidationError(error2)) {
953
+ throw new CliError(
954
+ ErrorCodes.INVALID_ARG,
955
+ `Selector not found: "${after}". ${error2.message}`,
956
+ SELECTOR_HINT,
957
+ error2
958
+ );
959
+ }
960
+ throw error2;
961
+ }
962
+ }
963
+ async function appendFileBlocks(client, uuid, filePaths) {
964
+ const blocks = await uploadFilesAsBlocks(filePaths, {}, client);
965
+ await client.blocks.children.append({
966
+ block_id: uuid,
967
+ children: blocks
968
+ });
969
+ }
720
970
  function appendCommand() {
721
971
  const cmd = new Command("append");
722
972
  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
973
  "--after <selector>",
724
974
  'insert after matched content \u2014 ellipsis selector, e.g. "## Section...end of section"'
975
+ ).option(
976
+ "--file <path>",
977
+ "attach a local file to the page (repeatable)",
978
+ collectFiles,
979
+ []
725
980
  ).action(
726
981
  withErrorHandling(
727
982
  async (idOrUrl, opts) => {
728
983
  const { token, source } = await resolveToken();
729
984
  reportTokenSource(source);
730
985
  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 {
986
+ const hasFiles = opts.file.length > 0;
987
+ const markdown = await resolveMarkdown(opts.message, hasFiles);
988
+ const uuid = toUuid(parseNotionId(idOrUrl));
989
+ if (opts.after && !markdown.trim()) {
737
990
  throw new CliError(
738
991
  ErrorCodes.INVALID_ARG,
739
- "No content to append.",
740
- "Pass markdown via -m/--message or pipe it through stdin"
992
+ "--after requires markdown content (-m or stdin)",
993
+ "Provide markdown via -m/--message or pipe it through stdin"
741
994
  );
742
995
  }
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(
996
+ if (markdown.trim()) {
997
+ await appendMarkdownWithErrorHandling(
751
998
  client,
752
999
  uuid,
753
1000
  markdown,
754
- opts.after ? { after: opts.after } : void 0
1001
+ opts.after
755
1002
  );
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
1003
  }
767
- process.stdout.write("Appended.\n");
1004
+ if (hasFiles) {
1005
+ await appendFileBlocks(client, uuid, opts.file);
1006
+ }
1007
+ if (hasFiles && markdown.trim()) {
1008
+ process.stdout.write(
1009
+ `Appended and attached ${opts.file.length} file(s).
1010
+ `
1011
+ );
1012
+ } else if (hasFiles) {
1013
+ process.stdout.write(`Attached ${opts.file.length} file(s).
1014
+ `);
1015
+ } else {
1016
+ process.stdout.write("Appended.\n");
1017
+ }
768
1018
  }
769
1019
  )
770
1020
  );
@@ -867,6 +1117,49 @@ function archiveCommand() {
867
1117
  return cmd;
868
1118
  }
869
1119
 
1120
+ // src/commands/attach.ts
1121
+ import { Command as Command3, Option } from "commander";
1122
+ function attachCommand() {
1123
+ const cmd = new Command3("attach");
1124
+ 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(
1125
+ new Option(
1126
+ "--type <type>",
1127
+ "override auto-detected block type (image|file|pdf|audio|video)"
1128
+ ).choices(["image", "file", "pdf", "audio", "video"])
1129
+ ).action(
1130
+ withErrorHandling(
1131
+ async (idOrUrl, firstFile, extraFiles, opts) => {
1132
+ const { token, source } = await resolveToken();
1133
+ reportTokenSource(source);
1134
+ const client = createNotionClient(token);
1135
+ const pageId = toUuid(parseNotionId(idOrUrl));
1136
+ const allFiles = [firstFile, ...extraFiles];
1137
+ const blocks = await uploadFilesAsBlocks(
1138
+ allFiles,
1139
+ { caption: opts.caption, type: opts.type },
1140
+ client
1141
+ );
1142
+ const response = await client.blocks.children.append({
1143
+ block_id: pageId,
1144
+ children: blocks
1145
+ });
1146
+ const mode = getOutputMode();
1147
+ if (mode === "json") {
1148
+ process.stdout.write(`${formatJSON(response)}
1149
+ `);
1150
+ } else {
1151
+ const pageUrl = `https://www.notion.so/${parseNotionId(idOrUrl)}`;
1152
+ process.stdout.write(
1153
+ `Attached ${allFiles.length} file(s) to ${pageUrl}
1154
+ `
1155
+ );
1156
+ }
1157
+ }
1158
+ )
1159
+ );
1160
+ return cmd;
1161
+ }
1162
+
870
1163
  // src/commands/auth/index.ts
871
1164
  function authDefaultAction(authCmd2) {
872
1165
  return async () => {
@@ -876,7 +1169,7 @@ function authDefaultAction(authCmd2) {
876
1169
 
877
1170
  // src/commands/auth/login.ts
878
1171
  import { input } from "@inquirer/prompts";
879
- import { Command as Command3 } from "commander";
1172
+ import { Command as Command4 } from "commander";
880
1173
 
881
1174
  // src/oauth/oauth-flow.ts
882
1175
  import { spawn } from "child_process";
@@ -1123,7 +1416,7 @@ Waiting for callback (up to 120 seconds)...
1123
1416
 
1124
1417
  // src/commands/auth/login.ts
1125
1418
  function loginCommand() {
1126
- const cmd = new Command3("login");
1419
+ const cmd = new Command4("login");
1127
1420
  cmd.description("authenticate with Notion via OAuth").option("--profile <name>", "profile name to store credentials in").option(
1128
1421
  "--manual",
1129
1422
  "print auth URL instead of opening browser (for headless OAuth)"
@@ -1186,7 +1479,7 @@ function loginCommand() {
1186
1479
 
1187
1480
  // src/commands/auth/logout.ts
1188
1481
  import { select } from "@inquirer/prompts";
1189
- import { Command as Command4 } from "commander";
1482
+ import { Command as Command5 } from "commander";
1190
1483
  function profileLabel(name, profile) {
1191
1484
  const parts = [];
1192
1485
  if (profile.oauth_access_token)
@@ -1199,7 +1492,7 @@ function profileLabel(name, profile) {
1199
1492
  return `${bold(name)} ${dim(authDesc)}${workspace}`;
1200
1493
  }
1201
1494
  function logoutCommand() {
1202
- const cmd = new Command4("logout");
1495
+ const cmd = new Command5("logout");
1203
1496
  cmd.description("remove a profile and its credentials").option(
1204
1497
  "--profile <name>",
1205
1498
  "profile name to remove (skips interactive selector)"
@@ -1257,9 +1550,9 @@ function logoutCommand() {
1257
1550
  }
1258
1551
 
1259
1552
  // src/commands/auth/status.ts
1260
- import { Command as Command5 } from "commander";
1553
+ import { Command as Command6 } from "commander";
1261
1554
  function statusCommand() {
1262
- const cmd = new Command5("status");
1555
+ const cmd = new Command6("status");
1263
1556
  cmd.description("show authentication status for the active profile").option("--profile <name>", "profile name to check").action(
1264
1557
  withErrorHandling(async (opts) => {
1265
1558
  let profileName = opts.profile;
@@ -1317,7 +1610,7 @@ function statusCommand() {
1317
1610
  }
1318
1611
 
1319
1612
  // src/commands/comment-add.ts
1320
- import { Command as Command6 } from "commander";
1613
+ import { Command as Command7 } from "commander";
1321
1614
  function resolveTarget(idOrUrl, opts) {
1322
1615
  const targetCount = [idOrUrl, opts.replyTo, opts.block].filter(
1323
1616
  Boolean
@@ -1345,7 +1638,7 @@ function resolveTarget(idOrUrl, opts) {
1345
1638
  );
1346
1639
  }
1347
1640
  function commentAddCommand() {
1348
- const cmd = new Command6("comment");
1641
+ const cmd = new Command7("comment");
1349
1642
  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
1643
  "--reply-to <discussion-id>",
1351
1644
  "reply to an existing discussion thread"
@@ -1367,7 +1660,7 @@ function commentAddCommand() {
1367
1660
  }
1368
1661
 
1369
1662
  // src/commands/comments.ts
1370
- import { Command as Command7 } from "commander";
1663
+ import { Command as Command8 } from "commander";
1371
1664
 
1372
1665
  // src/output/paginate.ts
1373
1666
  async function paginateResults(fetcher) {
@@ -1393,7 +1686,7 @@ function formatParent(parent) {
1393
1686
  }
1394
1687
  }
1395
1688
  function commentsCommand() {
1396
- const cmd = new Command7("comments");
1689
+ const cmd = new Command8("comments");
1397
1690
  cmd.description("list comments on a Notion page").argument("<id/url>", "Notion page ID or URL").option("--json", "output as JSON").action(
1398
1691
  withErrorHandling(async (idOrUrl, opts) => {
1399
1692
  if (opts.json) setOutputMode("json");
@@ -1430,7 +1723,7 @@ function commentsCommand() {
1430
1723
  }
1431
1724
 
1432
1725
  // src/commands/completion.ts
1433
- import { Command as Command8 } from "commander";
1726
+ import { Command as Command9 } from "commander";
1434
1727
  var BASH_COMPLETION = `# notion bash completion
1435
1728
  _notion_completion() {
1436
1729
  local cur prev words cword
@@ -1534,7 +1827,7 @@ complete -c notion -n '__fish_seen_subcommand_from completion' -a zsh -d 'zsh co
1534
1827
  complete -c notion -n '__fish_seen_subcommand_from completion' -a fish -d 'fish completion script'
1535
1828
  `;
1536
1829
  function completionCommand() {
1537
- const cmd = new Command8("completion");
1830
+ const cmd = new Command9("completion");
1538
1831
  cmd.description("output shell completion script").argument("<shell>", "shell type (bash, zsh, fish)").action(
1539
1832
  withErrorHandling(async (shell) => {
1540
1833
  switch (shell) {
@@ -1560,7 +1853,7 @@ function completionCommand() {
1560
1853
  }
1561
1854
 
1562
1855
  // src/commands/create-page.ts
1563
- import { Command as Command9 } from "commander";
1856
+ import { Command as Command10 } from "commander";
1564
1857
 
1565
1858
  // src/services/database.service.ts
1566
1859
  import { isFullPage } from "@notionhq/client";
@@ -1848,6 +2141,7 @@ function displayPropertyValue(prop) {
1848
2141
  }
1849
2142
 
1850
2143
  // src/services/update.service.ts
2144
+ import { existsSync as existsSync3 } from "fs";
1851
2145
  var UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
1852
2146
  "relation",
1853
2147
  "formula",
@@ -1856,7 +2150,6 @@ var UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
1856
2150
  "created_by",
1857
2151
  "last_edited_time",
1858
2152
  "last_edited_by",
1859
- "files",
1860
2153
  "unique_id",
1861
2154
  "verification",
1862
2155
  "button"
@@ -1866,7 +2159,7 @@ function buildPropertyUpdate(propName, propType, value) {
1866
2159
  throw new CliError(
1867
2160
  ErrorCodes.INVALID_ARG,
1868
2161
  `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"
2162
+ "Supported types: title, rich_text, select, status, multi_select, number, checkbox, url, email, phone_number, date, files"
1870
2163
  );
1871
2164
  }
1872
2165
  if (value === "") {
@@ -1912,41 +2205,54 @@ function buildPropertyUpdate(propName, propType, value) {
1912
2205
  const end = parts[1]?.trim();
1913
2206
  return { date: end ? { start, end } : { start } };
1914
2207
  }
2208
+ case "files":
2209
+ throw new CliError(
2210
+ ErrorCodes.INVALID_ARG,
2211
+ `Property "${propName}" has type "files" which requires async upload \u2014 use buildPropertiesPayloadAsync instead.`,
2212
+ "Use buildPropertiesPayloadAsync with a Notion client to handle file uploads"
2213
+ );
1915
2214
  default:
1916
2215
  throw new CliError(
1917
2216
  ErrorCodes.INVALID_ARG,
1918
2217
  `Property "${propName}" has unsupported type "${propType}".`,
1919
- "Supported types: title, rich_text, select, status, multi_select, number, checkbox, url, email, phone_number, date"
2218
+ "Supported types: title, rich_text, select, status, multi_select, number, checkbox, url, email, phone_number, date, files"
1920
2219
  );
1921
2220
  }
1922
2221
  }
1923
- function buildPropertiesPayload(propStrings, schemaOrPage) {
2222
+ function normalizeSchema(schemaOrPage) {
1924
2223
  const isPage = "object" in schemaOrPage && schemaOrPage.object === "page";
1925
- const schema = isPage ? schemaOrPage.properties : schemaOrPage;
2224
+ return isPage ? schemaOrPage.properties : schemaOrPage;
2225
+ }
2226
+ function parsePropString(propString) {
2227
+ const eqIdx = propString.indexOf("=");
2228
+ if (eqIdx === -1) {
2229
+ throw new CliError(
2230
+ ErrorCodes.INVALID_ARG,
2231
+ `Invalid --prop value: "${propString}". Expected format: "PropertyName=Value".`,
2232
+ 'Example: --prop "Status=Done"'
2233
+ );
2234
+ }
2235
+ return [propString.slice(0, eqIdx).trim(), propString.slice(eqIdx + 1)];
2236
+ }
2237
+ function lookupSchemaProp(propName, schema) {
2238
+ const schemaProp = schema[propName];
2239
+ if (!schemaProp) {
2240
+ const available = Object.keys(schema).join(", ");
2241
+ throw new CliError(
2242
+ ErrorCodes.INVALID_ARG,
2243
+ `Property "${propName}" not found on this page.`,
2244
+ `Available properties: ${available}`
2245
+ );
2246
+ }
2247
+ return schemaProp;
2248
+ }
2249
+ function buildPropertiesPayload(propStrings, schemaOrPage) {
2250
+ const schema = normalizeSchema(schemaOrPage);
1926
2251
  const result = {};
1927
2252
  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;
2253
+ const [propName, value] = parsePropString(propString);
2254
+ const schemaProp = lookupSchemaProp(propName, schema);
2255
+ result[propName] = buildPropertyUpdate(propName, schemaProp.type, value);
1950
2256
  }
1951
2257
  return result;
1952
2258
  }
@@ -1959,7 +2265,7 @@ async function updatePageProperties(client, pageId, properties) {
1959
2265
  }
1960
2266
 
1961
2267
  // src/commands/create-page.ts
1962
- function collectProps(val, acc) {
2268
+ function collectValues(val, acc) {
1963
2269
  acc.push(val);
1964
2270
  return acc;
1965
2271
  }
@@ -1971,16 +2277,21 @@ async function tryGetDatabaseSchema(client, uuid) {
1971
2277
  }
1972
2278
  }
1973
2279
  function createPageCommand() {
1974
- const cmd = new Command9("create-page");
2280
+ const cmd = new Command10("create-page");
1975
2281
  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
2282
  "-m, --message <markdown>",
1977
2283
  "inline markdown content for the page body"
1978
2284
  ).option(
1979
2285
  "--prop <property=value>",
1980
2286
  "set a property value (repeatable, database parents only)",
1981
- collectProps,
2287
+ collectValues,
2288
+ []
2289
+ ).option("--icon <emoji-or-url>", "page icon \u2014 emoji character or image URL").option("--cover <url>", "page cover image URL").option(
2290
+ "--file <path>",
2291
+ "attach a local file to the page after creation (repeatable)",
2292
+ collectValues,
1982
2293
  []
1983
- ).option("--icon <emoji-or-url>", "page icon \u2014 emoji character or image URL").option("--cover <url>", "page cover image URL").action(
2294
+ ).action(
1984
2295
  withErrorHandling(async (opts) => {
1985
2296
  const { token, source } = await resolveToken();
1986
2297
  reportTokenSource(source);
@@ -1988,12 +2299,13 @@ function createPageCommand() {
1988
2299
  let markdown = "";
1989
2300
  if (opts.message) {
1990
2301
  markdown = opts.message;
1991
- } else if (!process.stdin.isTTY) {
2302
+ } else if (!process.stdin.isTTY && opts.file.length === 0) {
1992
2303
  markdown = await readStdin();
1993
2304
  }
1994
2305
  const parentUuid = toUuid(parseNotionId(opts.parent));
1995
2306
  const iconCover = { icon: opts.icon, cover: opts.cover };
1996
2307
  const dbSchema = await tryGetDatabaseSchema(client, parentUuid);
2308
+ let createdPageUrl;
1997
2309
  if (dbSchema) {
1998
2310
  const titleEntry = Object.entries(dbSchema.properties).find(
1999
2311
  ([, prop]) => prop.type === "title"
@@ -2007,7 +2319,7 @@ function createPageCommand() {
2007
2319
  }
2008
2320
  const [titlePropName] = titleEntry;
2009
2321
  const extraProperties = opts.prop.length > 0 ? buildPropertiesPayload(opts.prop, dbSchema.properties) : {};
2010
- const url = await createPageInDatabase(
2322
+ createdPageUrl = await createPageInDatabase(
2011
2323
  client,
2012
2324
  dbSchema.databaseId,
2013
2325
  titlePropName,
@@ -2016,8 +2328,6 @@ function createPageCommand() {
2016
2328
  markdown,
2017
2329
  iconCover
2018
2330
  );
2019
- process.stdout.write(`${url}
2020
- `);
2021
2331
  } else {
2022
2332
  if (opts.prop.length > 0) {
2023
2333
  throw new CliError(
@@ -2026,32 +2336,40 @@ function createPageCommand() {
2026
2336
  "To set properties, use a database ID/URL as --parent"
2027
2337
  );
2028
2338
  }
2029
- const url = await createPage(
2339
+ createdPageUrl = await createPage(
2030
2340
  client,
2031
2341
  parentUuid,
2032
2342
  opts.title,
2033
2343
  markdown,
2034
2344
  iconCover
2035
2345
  );
2036
- process.stdout.write(`${url}
2037
- `);
2038
2346
  }
2347
+ if (opts.file.length > 0) {
2348
+ const createdPageId = toUuid(parseNotionId(createdPageUrl));
2349
+ const blocks = await uploadFilesAsBlocks(opts.file, {}, client);
2350
+ await client.blocks.children.append({
2351
+ block_id: createdPageId,
2352
+ children: blocks
2353
+ });
2354
+ }
2355
+ process.stdout.write(`${createdPageUrl}
2356
+ `);
2039
2357
  })
2040
2358
  );
2041
2359
  return cmd;
2042
2360
  }
2043
2361
 
2044
2362
  // src/commands/db/create.ts
2045
- import { Command as Command10 } from "commander";
2046
- function collectProps2(val, acc) {
2363
+ import { Command as Command11 } from "commander";
2364
+ function collectProps(val, acc) {
2047
2365
  acc.push(val);
2048
2366
  return acc;
2049
2367
  }
2050
2368
  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(
2369
+ 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
2370
  "--prop <definition>",
2053
2371
  'property definition (repeatable): --prop "Name:type:options"',
2054
- collectProps2,
2372
+ collectProps,
2055
2373
  []
2056
2374
  ).action(
2057
2375
  withErrorHandling(async (opts) => {
@@ -2080,7 +2398,7 @@ function dbCreateCommand() {
2080
2398
  }
2081
2399
 
2082
2400
  // src/commands/db/query.ts
2083
- import { Command as Command11 } from "commander";
2401
+ import { Command as Command12 } from "commander";
2084
2402
  var SKIP_TYPES_IN_AUTO = /* @__PURE__ */ new Set(["relation", "rich_text", "people"]);
2085
2403
  function autoSelectColumns(schema, entries) {
2086
2404
  const termWidth = process.stdout.columns || 120;
@@ -2108,7 +2426,7 @@ function autoSelectColumns(schema, entries) {
2108
2426
  return selected;
2109
2427
  }
2110
2428
  function dbQueryCommand() {
2111
- return new Command11("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option(
2429
+ return new Command12("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option(
2112
2430
  "--filter <filter>",
2113
2431
  'Filter entries (repeatable): --filter "Status=Done"',
2114
2432
  collect,
@@ -2163,9 +2481,9 @@ function collect(value, previous) {
2163
2481
  }
2164
2482
 
2165
2483
  // src/commands/db/schema.ts
2166
- import { Command as Command12 } from "commander";
2484
+ import { Command as Command13 } from "commander";
2167
2485
  function dbSchemaCommand() {
2168
- return new Command12("schema").description("Show database schema (property names, types, and options)").argument("<id>", "Notion database ID or URL").action(
2486
+ return new Command13("schema").description("Show database schema (property names, types, and options)").argument("<id>", "Notion database ID or URL").action(
2169
2487
  withErrorHandling(async (id) => {
2170
2488
  const { token } = await resolveToken();
2171
2489
  const client = createNotionClient(token);
@@ -2189,13 +2507,13 @@ function dbSchemaCommand() {
2189
2507
  }
2190
2508
 
2191
2509
  // src/commands/edit-page.ts
2192
- import { Command as Command13 } from "commander";
2510
+ import { Command as Command14 } from "commander";
2193
2511
  function collect2(val, acc) {
2194
2512
  acc.push(val);
2195
2513
  return acc;
2196
2514
  }
2197
2515
  function editPageCommand() {
2198
- const cmd = new Command13("edit-page");
2516
+ const cmd = new Command14("edit-page");
2199
2517
  cmd.description(
2200
2518
  "replace a Notion page's content \u2014 full page or a targeted section"
2201
2519
  ).argument("<id/url>", "Notion page ID or URL").option(
@@ -2293,7 +2611,7 @@ function editPageCommand() {
2293
2611
 
2294
2612
  // src/commands/init.ts
2295
2613
  import { confirm, input as input2, password } from "@inquirer/prompts";
2296
- import { Command as Command14 } from "commander";
2614
+ import { Command as Command15 } from "commander";
2297
2615
  async function runInitFlow() {
2298
2616
  const profileName = await input2({
2299
2617
  message: "Profile name:",
@@ -2373,7 +2691,7 @@ async function runInitFlow() {
2373
2691
  stderrWrite(dim(" notion auth login"));
2374
2692
  }
2375
2693
  function initCommand() {
2376
- const cmd = new Command14("init");
2694
+ const cmd = new Command15("init");
2377
2695
  cmd.description("authenticate with Notion and save a profile").action(
2378
2696
  withErrorHandling(async () => {
2379
2697
  if (!process.stdin.isTTY) {
@@ -2391,7 +2709,7 @@ function initCommand() {
2391
2709
 
2392
2710
  // src/commands/ls.ts
2393
2711
  import { isFullPageOrDataSource } from "@notionhq/client";
2394
- import { Command as Command15 } from "commander";
2712
+ import { Command as Command16 } from "commander";
2395
2713
  function getTitle(item) {
2396
2714
  if (item.object === "data_source") {
2397
2715
  return item.title.map((t) => t.plain_text).join("") || "(untitled)";
@@ -2408,7 +2726,7 @@ function displayType(item) {
2408
2726
  return item.object === "data_source" ? "database" : item.object;
2409
2727
  }
2410
2728
  function lsCommand() {
2411
- const cmd = new Command15("ls");
2729
+ const cmd = new Command16("ls");
2412
2730
  cmd.description("list accessible Notion pages and databases").option(
2413
2731
  "--type <type>",
2414
2732
  "filter by object type (page or database)",
@@ -2482,9 +2800,9 @@ function lsCommand() {
2482
2800
  }
2483
2801
 
2484
2802
  // src/commands/move.ts
2485
- import { Command as Command16 } from "commander";
2803
+ import { Command as Command17 } from "commander";
2486
2804
  function moveCommand() {
2487
- const cmd = new Command16("move");
2805
+ const cmd = new Command17("move");
2488
2806
  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
2807
  "--to-db <id/url>",
2490
2808
  "target database parent ID or URL (resolves to data source)"
@@ -2542,10 +2860,10 @@ function moveCommand() {
2542
2860
  // src/commands/open.ts
2543
2861
  import { exec } from "child_process";
2544
2862
  import { promisify } from "util";
2545
- import { Command as Command17 } from "commander";
2863
+ import { Command as Command18 } from "commander";
2546
2864
  var execAsync = promisify(exec);
2547
2865
  function openCommand() {
2548
- const cmd = new Command17("open");
2866
+ const cmd = new Command18("open");
2549
2867
  cmd.description("open a Notion page in the default browser").argument("<id/url>", "Notion page ID or URL").action(
2550
2868
  withErrorHandling(async (idOrUrl) => {
2551
2869
  const id = parseNotionId(idOrUrl);
@@ -2561,9 +2879,9 @@ function openCommand() {
2561
2879
  }
2562
2880
 
2563
2881
  // src/commands/profile/list.ts
2564
- import { Command as Command18 } from "commander";
2882
+ import { Command as Command19 } from "commander";
2565
2883
  function profileListCommand() {
2566
- const cmd = new Command18("list");
2884
+ const cmd = new Command19("list");
2567
2885
  cmd.description("list all authentication profiles").action(
2568
2886
  withErrorHandling(async () => {
2569
2887
  const config = await readGlobalConfig();
@@ -2592,9 +2910,9 @@ function profileListCommand() {
2592
2910
  }
2593
2911
 
2594
2912
  // src/commands/profile/remove.ts
2595
- import { Command as Command19 } from "commander";
2913
+ import { Command as Command20 } from "commander";
2596
2914
  function profileRemoveCommand() {
2597
- const cmd = new Command19("remove");
2915
+ const cmd = new Command20("remove");
2598
2916
  cmd.description("remove an authentication profile").argument("<name>", "profile name to remove").action(
2599
2917
  withErrorHandling(async (name) => {
2600
2918
  const config = await readGlobalConfig();
@@ -2620,9 +2938,9 @@ function profileRemoveCommand() {
2620
2938
  }
2621
2939
 
2622
2940
  // src/commands/profile/use.ts
2623
- import { Command as Command20 } from "commander";
2941
+ import { Command as Command21 } from "commander";
2624
2942
  function profileUseCommand() {
2625
- const cmd = new Command20("use");
2943
+ const cmd = new Command21("use");
2626
2944
  cmd.description("switch the active profile").argument("<name>", "profile name to activate").action(
2627
2945
  withErrorHandling(async (name) => {
2628
2946
  const config = await readGlobalConfig();
@@ -2645,7 +2963,7 @@ function profileUseCommand() {
2645
2963
  }
2646
2964
 
2647
2965
  // src/commands/read.ts
2648
- import { Command as Command21 } from "commander";
2966
+ import { Command as Command22 } from "commander";
2649
2967
 
2650
2968
  // src/output/markdown.ts
2651
2969
  import { Chalk as Chalk2 } from "chalk";
@@ -2785,7 +3103,7 @@ async function fetchPageMarkdown(client, pageId) {
2785
3103
 
2786
3104
  // src/commands/read.ts
2787
3105
  function readCommand() {
2788
- return new Command21("read").description("Read a Notion page as markdown").argument("<id>", "Notion page ID or URL").action(
3106
+ return new Command22("read").description("Read a Notion page as markdown").argument("<id>", "Notion page ID or URL").action(
2789
3107
  withErrorHandling(async (id) => {
2790
3108
  const { token } = await resolveToken();
2791
3109
  const client = createNotionClient(token);
@@ -2811,7 +3129,7 @@ function readCommand() {
2811
3129
 
2812
3130
  // src/commands/search.ts
2813
3131
  import { isFullPageOrDataSource as isFullPageOrDataSource2 } from "@notionhq/client";
2814
- import { Command as Command22 } from "commander";
3132
+ import { Command as Command23 } from "commander";
2815
3133
  function getTitle2(item) {
2816
3134
  if (item.object === "data_source") {
2817
3135
  return item.title.map((t) => t.plain_text).join("") || "(untitled)";
@@ -2831,7 +3149,7 @@ function displayType2(item) {
2831
3149
  return item.object === "data_source" ? "database" : item.object;
2832
3150
  }
2833
3151
  function searchCommand() {
2834
- const cmd = new Command22("search");
3152
+ const cmd = new Command23("search");
2835
3153
  cmd.description("search Notion workspace by keyword").argument("<query>", "search keyword").option(
2836
3154
  "--type <type>",
2837
3155
  "filter by object type (page or database)",
@@ -2904,7 +3222,7 @@ function searchCommand() {
2904
3222
  // src/commands/skill.ts
2905
3223
  import {
2906
3224
  copyFileSync,
2907
- existsSync,
3225
+ existsSync as existsSync4,
2908
3226
  mkdirSync,
2909
3227
  readFileSync,
2910
3228
  realpathSync
@@ -2912,7 +3230,7 @@ import {
2912
3230
  import { homedir as homedir2 } from "os";
2913
3231
  import { dirname, join as join3 } from "path";
2914
3232
  import chalk from "chalk";
2915
- import { Command as Command23 } from "commander";
3233
+ import { Command as Command24 } from "commander";
2916
3234
  function skillPath() {
2917
3235
  if (!process.argv[1]) {
2918
3236
  throw new Error("Cannot determine install path. Run with: notion skill");
@@ -2931,7 +3249,7 @@ function skillPath() {
2931
3249
  )
2932
3250
  ];
2933
3251
  for (const c2 of candidates) {
2934
- if (existsSync(c2)) return c2;
3252
+ if (existsSync4(c2)) return c2;
2935
3253
  }
2936
3254
  throw new Error(
2937
3255
  "SKILL.md not found. Reinstall with: npm install -g @andrzejchm/notion-cli"
@@ -2952,11 +3270,11 @@ function getAgentTargets() {
2952
3270
  ];
2953
3271
  return targets.map((t) => ({
2954
3272
  ...t,
2955
- detected: existsSync(dirname(t.dir))
3273
+ detected: existsSync4(dirname(t.dir))
2956
3274
  }));
2957
3275
  }
2958
3276
  function installTo(source, target) {
2959
- if (!existsSync(target.dir)) mkdirSync(target.dir, { recursive: true });
3277
+ if (!existsSync4(target.dir)) mkdirSync(target.dir, { recursive: true });
2960
3278
  const dest = join3(target.dir, "SKILL.md");
2961
3279
  copyFileSync(source, dest);
2962
3280
  return dest;
@@ -2999,7 +3317,7 @@ function installNonInteractive(source, targets) {
2999
3317
  }
3000
3318
  }
3001
3319
  function skillCommand() {
3002
- const cmd = new Command23("skill");
3320
+ const cmd = new Command24("skill");
3003
3321
  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
3322
  withErrorHandling(async (opts) => {
3005
3323
  if (opts.print) {
@@ -3009,7 +3327,7 @@ function skillCommand() {
3009
3327
  const source = skillPath();
3010
3328
  if (opts.path) {
3011
3329
  const dir = dirname(opts.path);
3012
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
3330
+ if (!existsSync4(dir)) mkdirSync(dir, { recursive: true });
3013
3331
  copyFileSync(source, opts.path);
3014
3332
  process.stdout.write(`Installed to ${opts.path}
3015
3333
  `);
@@ -3028,17 +3346,17 @@ function skillCommand() {
3028
3346
  }
3029
3347
 
3030
3348
  // src/commands/update.ts
3031
- import { Command as Command24 } from "commander";
3032
- function collectProps3(val, acc) {
3349
+ import { Command as Command25 } from "commander";
3350
+ function collectProps2(val, acc) {
3033
3351
  acc.push(val);
3034
3352
  return acc;
3035
3353
  }
3036
3354
  function updateCommand() {
3037
- const cmd = new Command24("update");
3355
+ const cmd = new Command25("update");
3038
3356
  cmd.description("update properties on a Notion page").argument("<id/url>", "Notion page ID or URL").option(
3039
3357
  "--prop <property=value>",
3040
3358
  "set a property value (repeatable)",
3041
- collectProps3,
3359
+ collectProps2,
3042
3360
  []
3043
3361
  ).option("--title <title>", "set the page title").action(
3044
3362
  withErrorHandling(async (idOrUrl, opts) => {
@@ -3092,7 +3410,7 @@ function updateCommand() {
3092
3410
  }
3093
3411
 
3094
3412
  // src/commands/users.ts
3095
- import { Command as Command25 } from "commander";
3413
+ import { Command as Command26 } from "commander";
3096
3414
  function getEmailOrWorkspace(user) {
3097
3415
  if (user.type === "person") {
3098
3416
  return user.person.email ?? "\u2014";
@@ -3104,7 +3422,7 @@ function getEmailOrWorkspace(user) {
3104
3422
  return "\u2014";
3105
3423
  }
3106
3424
  function usersCommand() {
3107
- const cmd = new Command25("users");
3425
+ const cmd = new Command26("users");
3108
3426
  cmd.description("list all users in the workspace").option("--json", "output as JSON").action(
3109
3427
  withErrorHandling(async (opts) => {
3110
3428
  if (opts.json) setOutputMode("json");
@@ -3135,7 +3453,7 @@ var __dirname = dirname2(__filename);
3135
3453
  var pkg = JSON.parse(
3136
3454
  readFileSync2(join4(__dirname, "../package.json"), "utf-8")
3137
3455
  );
3138
- var program = new Command26();
3456
+ var program = new Command27();
3139
3457
  program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
3140
3458
  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
3459
  program.configureOutput({
@@ -3156,7 +3474,7 @@ program.hook("preAction", (thisCommand) => {
3156
3474
  setOutputMode("md");
3157
3475
  }
3158
3476
  });
3159
- var authCmd = new Command26("auth").description("manage Notion authentication");
3477
+ var authCmd = new Command27("auth").description("manage Notion authentication");
3160
3478
  authCmd.action(authDefaultAction(authCmd));
3161
3479
  authCmd.addCommand(loginCommand());
3162
3480
  authCmd.addCommand(logoutCommand());
@@ -3166,7 +3484,7 @@ authCmd.addCommand(profileUseCommand());
3166
3484
  authCmd.addCommand(profileRemoveCommand());
3167
3485
  program.addCommand(authCmd);
3168
3486
  program.addCommand(initCommand(), { hidden: true });
3169
- var profileCmd = new Command26("profile").description(
3487
+ var profileCmd = new Command27("profile").description(
3170
3488
  "manage authentication profiles"
3171
3489
  );
3172
3490
  profileCmd.addCommand(profileListCommand());
@@ -3181,12 +3499,13 @@ program.addCommand(commentsCommand());
3181
3499
  program.addCommand(readCommand());
3182
3500
  program.addCommand(commentAddCommand());
3183
3501
  program.addCommand(appendCommand());
3502
+ program.addCommand(attachCommand());
3184
3503
  program.addCommand(createPageCommand());
3185
3504
  program.addCommand(editPageCommand());
3186
3505
  program.addCommand(updateCommand());
3187
3506
  program.addCommand(archiveCommand());
3188
3507
  program.addCommand(moveCommand());
3189
- var dbCmd = new Command26("db").description("Database operations");
3508
+ var dbCmd = new Command27("db").description("Database operations");
3190
3509
  dbCmd.addCommand(dbCreateCommand());
3191
3510
  dbCmd.addCommand(dbSchemaCommand());
3192
3511
  dbCmd.addCommand(dbQueryCommand());