@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/.agents/skills/using-notion-cli/SKILL.md +135 -10
- package/README.md +51 -1
- package/dist/cli.js +844 -146
- package/dist/cli.js.map +1 -1
- package/docs/FEATURE-PARITY.md +29 -20
- package/docs/README.agents.md +177 -1
- package/package.json +2 -2
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
|
|
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 {
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
"
|
|
740
|
-
"
|
|
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 (
|
|
744
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
1150
|
+
process.stdout.write(`${formatJSON(result)}
|
|
861
1151
|
`);
|
|
862
1152
|
} else {
|
|
863
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1595
|
+
import { Command as Command6 } from "commander";
|
|
1261
1596
|
function statusCommand() {
|
|
1262
|
-
const cmd = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1569
|
-
const
|
|
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 :
|
|
1589
|
-
return { id:
|
|
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
|
|
2340
|
+
function normalizeSchema(schemaOrPage) {
|
|
1924
2341
|
const isPage = "object" in schemaOrPage && schemaOrPage.object === "page";
|
|
1925
|
-
|
|
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
|
|
1929
|
-
|
|
1930
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2046
|
-
function
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2602
|
+
import { Command as Command13 } from "commander";
|
|
2167
2603
|
function dbSchemaCommand() {
|
|
2168
|
-
return new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
3121
|
+
import { Command as Command20 } from "commander";
|
|
2486
3122
|
function moveCommand() {
|
|
2487
|
-
const cmd = new
|
|
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
|
|
3181
|
+
import { Command as Command21 } from "commander";
|
|
2546
3182
|
var execAsync = promisify(exec);
|
|
2547
3183
|
function openCommand() {
|
|
2548
|
-
const cmd = new
|
|
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
|
|
3200
|
+
import { Command as Command22 } from "commander";
|
|
2565
3201
|
function profileListCommand() {
|
|
2566
|
-
const cmd = new
|
|
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
|
|
3231
|
+
import { Command as Command23 } from "commander";
|
|
2596
3232
|
function profileRemoveCommand() {
|
|
2597
|
-
const cmd = new
|
|
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
|
|
3259
|
+
import { Command as Command24 } from "commander";
|
|
2624
3260
|
function profileUseCommand() {
|
|
2625
|
-
const cmd = new
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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:
|
|
3649
|
+
detected: existsSync4(dirname(t.dir))
|
|
2956
3650
|
}));
|
|
2957
3651
|
}
|
|
2958
3652
|
function installTo(source, target) {
|
|
2959
|
-
if (!
|
|
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
|
|
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 (!
|
|
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
|
|
3032
|
-
function
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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());
|