@andrzejchm/notion-cli 0.10.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/.agents/skills/using-notion-cli/SKILL.md +335 -0
- package/README.md +25 -0
- package/dist/cli.js +575 -127
- package/dist/cli.js.map +1 -1
- package/docs/README.agents.md +74 -0
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync } from "fs";
|
|
5
|
-
import { dirname, join as
|
|
4
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
-
import { Command as
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
"
|
|
740
|
-
"
|
|
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 (
|
|
744
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1553
|
+
import { Command as Command6 } from "commander";
|
|
1261
1554
|
function statusCommand() {
|
|
1262
|
-
const cmd = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,13 +1723,13 @@ function commentsCommand() {
|
|
|
1430
1723
|
}
|
|
1431
1724
|
|
|
1432
1725
|
// src/commands/completion.ts
|
|
1433
|
-
import { Command as
|
|
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
|
|
1437
1730
|
_init_completion || return
|
|
1438
1731
|
|
|
1439
|
-
local commands="init profile completion --help --version --verbose --color"
|
|
1732
|
+
local commands="init profile completion skill --help --version --verbose --color"
|
|
1440
1733
|
local profile_commands="list use remove"
|
|
1441
1734
|
|
|
1442
1735
|
case "$prev" in
|
|
@@ -1469,6 +1762,7 @@ _notion() {
|
|
|
1469
1762
|
'init:authenticate with Notion and save a profile'
|
|
1470
1763
|
'profile:manage authentication profiles'
|
|
1471
1764
|
'completion:output shell completion script'
|
|
1765
|
+
'skill:install the agent skill file'
|
|
1472
1766
|
)
|
|
1473
1767
|
|
|
1474
1768
|
local -a global_opts
|
|
@@ -1520,6 +1814,7 @@ complete -c notion -l color -d 'force color output'
|
|
|
1520
1814
|
complete -c notion -n '__fish_use_subcommand' -a init -d 'authenticate with Notion and save a profile'
|
|
1521
1815
|
complete -c notion -n '__fish_use_subcommand' -a profile -d 'manage authentication profiles'
|
|
1522
1816
|
complete -c notion -n '__fish_use_subcommand' -a completion -d 'output shell completion script'
|
|
1817
|
+
complete -c notion -n '__fish_use_subcommand' -a skill -d 'install the agent skill file'
|
|
1523
1818
|
|
|
1524
1819
|
# profile subcommands
|
|
1525
1820
|
complete -c notion -n '__fish_seen_subcommand_from profile' -a list -d 'list all authentication profiles'
|
|
@@ -1532,7 +1827,7 @@ complete -c notion -n '__fish_seen_subcommand_from completion' -a zsh -d 'zsh co
|
|
|
1532
1827
|
complete -c notion -n '__fish_seen_subcommand_from completion' -a fish -d 'fish completion script'
|
|
1533
1828
|
`;
|
|
1534
1829
|
function completionCommand() {
|
|
1535
|
-
const cmd = new
|
|
1830
|
+
const cmd = new Command9("completion");
|
|
1536
1831
|
cmd.description("output shell completion script").argument("<shell>", "shell type (bash, zsh, fish)").action(
|
|
1537
1832
|
withErrorHandling(async (shell) => {
|
|
1538
1833
|
switch (shell) {
|
|
@@ -1558,7 +1853,7 @@ function completionCommand() {
|
|
|
1558
1853
|
}
|
|
1559
1854
|
|
|
1560
1855
|
// src/commands/create-page.ts
|
|
1561
|
-
import { Command as
|
|
1856
|
+
import { Command as Command10 } from "commander";
|
|
1562
1857
|
|
|
1563
1858
|
// src/services/database.service.ts
|
|
1564
1859
|
import { isFullPage } from "@notionhq/client";
|
|
@@ -1846,6 +2141,7 @@ function displayPropertyValue(prop) {
|
|
|
1846
2141
|
}
|
|
1847
2142
|
|
|
1848
2143
|
// src/services/update.service.ts
|
|
2144
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1849
2145
|
var UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
1850
2146
|
"relation",
|
|
1851
2147
|
"formula",
|
|
@@ -1854,7 +2150,6 @@ var UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
|
1854
2150
|
"created_by",
|
|
1855
2151
|
"last_edited_time",
|
|
1856
2152
|
"last_edited_by",
|
|
1857
|
-
"files",
|
|
1858
2153
|
"unique_id",
|
|
1859
2154
|
"verification",
|
|
1860
2155
|
"button"
|
|
@@ -1864,7 +2159,7 @@ function buildPropertyUpdate(propName, propType, value) {
|
|
|
1864
2159
|
throw new CliError(
|
|
1865
2160
|
ErrorCodes.INVALID_ARG,
|
|
1866
2161
|
`Property "${propName}" has type "${propType}" which cannot be set via the CLI.`,
|
|
1867
|
-
"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"
|
|
1868
2163
|
);
|
|
1869
2164
|
}
|
|
1870
2165
|
if (value === "") {
|
|
@@ -1910,41 +2205,54 @@ function buildPropertyUpdate(propName, propType, value) {
|
|
|
1910
2205
|
const end = parts[1]?.trim();
|
|
1911
2206
|
return { date: end ? { start, end } : { start } };
|
|
1912
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
|
+
);
|
|
1913
2214
|
default:
|
|
1914
2215
|
throw new CliError(
|
|
1915
2216
|
ErrorCodes.INVALID_ARG,
|
|
1916
2217
|
`Property "${propName}" has unsupported type "${propType}".`,
|
|
1917
|
-
"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"
|
|
1918
2219
|
);
|
|
1919
2220
|
}
|
|
1920
2221
|
}
|
|
1921
|
-
function
|
|
2222
|
+
function normalizeSchema(schemaOrPage) {
|
|
1922
2223
|
const isPage = "object" in schemaOrPage && schemaOrPage.object === "page";
|
|
1923
|
-
|
|
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);
|
|
1924
2251
|
const result = {};
|
|
1925
2252
|
for (const propString of propStrings) {
|
|
1926
|
-
const
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
ErrorCodes.INVALID_ARG,
|
|
1930
|
-
`Invalid --prop value: "${propString}". Expected format: "PropertyName=Value".`,
|
|
1931
|
-
'Example: --prop "Status=Done"'
|
|
1932
|
-
);
|
|
1933
|
-
}
|
|
1934
|
-
const propName = propString.slice(0, eqIdx).trim();
|
|
1935
|
-
const value = propString.slice(eqIdx + 1);
|
|
1936
|
-
const schemaProp = schema[propName];
|
|
1937
|
-
if (!schemaProp) {
|
|
1938
|
-
const available = Object.keys(schema).join(", ");
|
|
1939
|
-
throw new CliError(
|
|
1940
|
-
ErrorCodes.INVALID_ARG,
|
|
1941
|
-
`Property "${propName}" not found on this page.`,
|
|
1942
|
-
`Available properties: ${available}`
|
|
1943
|
-
);
|
|
1944
|
-
}
|
|
1945
|
-
const propType = schemaProp.type;
|
|
1946
|
-
const payload = buildPropertyUpdate(propName, propType, value);
|
|
1947
|
-
result[propName] = payload;
|
|
2253
|
+
const [propName, value] = parsePropString(propString);
|
|
2254
|
+
const schemaProp = lookupSchemaProp(propName, schema);
|
|
2255
|
+
result[propName] = buildPropertyUpdate(propName, schemaProp.type, value);
|
|
1948
2256
|
}
|
|
1949
2257
|
return result;
|
|
1950
2258
|
}
|
|
@@ -1957,7 +2265,7 @@ async function updatePageProperties(client, pageId, properties) {
|
|
|
1957
2265
|
}
|
|
1958
2266
|
|
|
1959
2267
|
// src/commands/create-page.ts
|
|
1960
|
-
function
|
|
2268
|
+
function collectValues(val, acc) {
|
|
1961
2269
|
acc.push(val);
|
|
1962
2270
|
return acc;
|
|
1963
2271
|
}
|
|
@@ -1969,16 +2277,21 @@ async function tryGetDatabaseSchema(client, uuid) {
|
|
|
1969
2277
|
}
|
|
1970
2278
|
}
|
|
1971
2279
|
function createPageCommand() {
|
|
1972
|
-
const cmd = new
|
|
2280
|
+
const cmd = new Command10("create-page");
|
|
1973
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(
|
|
1974
2282
|
"-m, --message <markdown>",
|
|
1975
2283
|
"inline markdown content for the page body"
|
|
1976
2284
|
).option(
|
|
1977
2285
|
"--prop <property=value>",
|
|
1978
2286
|
"set a property value (repeatable, database parents only)",
|
|
1979
|
-
|
|
2287
|
+
collectValues,
|
|
1980
2288
|
[]
|
|
1981
|
-
).option("--icon <emoji-or-url>", "page icon \u2014 emoji character or image URL").option("--cover <url>", "page cover image URL").
|
|
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,
|
|
2293
|
+
[]
|
|
2294
|
+
).action(
|
|
1982
2295
|
withErrorHandling(async (opts) => {
|
|
1983
2296
|
const { token, source } = await resolveToken();
|
|
1984
2297
|
reportTokenSource(source);
|
|
@@ -1986,12 +2299,13 @@ function createPageCommand() {
|
|
|
1986
2299
|
let markdown = "";
|
|
1987
2300
|
if (opts.message) {
|
|
1988
2301
|
markdown = opts.message;
|
|
1989
|
-
} else if (!process.stdin.isTTY) {
|
|
2302
|
+
} else if (!process.stdin.isTTY && opts.file.length === 0) {
|
|
1990
2303
|
markdown = await readStdin();
|
|
1991
2304
|
}
|
|
1992
2305
|
const parentUuid = toUuid(parseNotionId(opts.parent));
|
|
1993
2306
|
const iconCover = { icon: opts.icon, cover: opts.cover };
|
|
1994
2307
|
const dbSchema = await tryGetDatabaseSchema(client, parentUuid);
|
|
2308
|
+
let createdPageUrl;
|
|
1995
2309
|
if (dbSchema) {
|
|
1996
2310
|
const titleEntry = Object.entries(dbSchema.properties).find(
|
|
1997
2311
|
([, prop]) => prop.type === "title"
|
|
@@ -2005,7 +2319,7 @@ function createPageCommand() {
|
|
|
2005
2319
|
}
|
|
2006
2320
|
const [titlePropName] = titleEntry;
|
|
2007
2321
|
const extraProperties = opts.prop.length > 0 ? buildPropertiesPayload(opts.prop, dbSchema.properties) : {};
|
|
2008
|
-
|
|
2322
|
+
createdPageUrl = await createPageInDatabase(
|
|
2009
2323
|
client,
|
|
2010
2324
|
dbSchema.databaseId,
|
|
2011
2325
|
titlePropName,
|
|
@@ -2014,8 +2328,6 @@ function createPageCommand() {
|
|
|
2014
2328
|
markdown,
|
|
2015
2329
|
iconCover
|
|
2016
2330
|
);
|
|
2017
|
-
process.stdout.write(`${url}
|
|
2018
|
-
`);
|
|
2019
2331
|
} else {
|
|
2020
2332
|
if (opts.prop.length > 0) {
|
|
2021
2333
|
throw new CliError(
|
|
@@ -2024,32 +2336,40 @@ function createPageCommand() {
|
|
|
2024
2336
|
"To set properties, use a database ID/URL as --parent"
|
|
2025
2337
|
);
|
|
2026
2338
|
}
|
|
2027
|
-
|
|
2339
|
+
createdPageUrl = await createPage(
|
|
2028
2340
|
client,
|
|
2029
2341
|
parentUuid,
|
|
2030
2342
|
opts.title,
|
|
2031
2343
|
markdown,
|
|
2032
2344
|
iconCover
|
|
2033
2345
|
);
|
|
2034
|
-
process.stdout.write(`${url}
|
|
2035
|
-
`);
|
|
2036
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
|
+
`);
|
|
2037
2357
|
})
|
|
2038
2358
|
);
|
|
2039
2359
|
return cmd;
|
|
2040
2360
|
}
|
|
2041
2361
|
|
|
2042
2362
|
// src/commands/db/create.ts
|
|
2043
|
-
import { Command as
|
|
2044
|
-
function
|
|
2363
|
+
import { Command as Command11 } from "commander";
|
|
2364
|
+
function collectProps(val, acc) {
|
|
2045
2365
|
acc.push(val);
|
|
2046
2366
|
return acc;
|
|
2047
2367
|
}
|
|
2048
2368
|
function dbCreateCommand() {
|
|
2049
|
-
return new
|
|
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(
|
|
2050
2370
|
"--prop <definition>",
|
|
2051
2371
|
'property definition (repeatable): --prop "Name:type:options"',
|
|
2052
|
-
|
|
2372
|
+
collectProps,
|
|
2053
2373
|
[]
|
|
2054
2374
|
).action(
|
|
2055
2375
|
withErrorHandling(async (opts) => {
|
|
@@ -2078,7 +2398,7 @@ function dbCreateCommand() {
|
|
|
2078
2398
|
}
|
|
2079
2399
|
|
|
2080
2400
|
// src/commands/db/query.ts
|
|
2081
|
-
import { Command as
|
|
2401
|
+
import { Command as Command12 } from "commander";
|
|
2082
2402
|
var SKIP_TYPES_IN_AUTO = /* @__PURE__ */ new Set(["relation", "rich_text", "people"]);
|
|
2083
2403
|
function autoSelectColumns(schema, entries) {
|
|
2084
2404
|
const termWidth = process.stdout.columns || 120;
|
|
@@ -2106,7 +2426,7 @@ function autoSelectColumns(schema, entries) {
|
|
|
2106
2426
|
return selected;
|
|
2107
2427
|
}
|
|
2108
2428
|
function dbQueryCommand() {
|
|
2109
|
-
return new
|
|
2429
|
+
return new Command12("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option(
|
|
2110
2430
|
"--filter <filter>",
|
|
2111
2431
|
'Filter entries (repeatable): --filter "Status=Done"',
|
|
2112
2432
|
collect,
|
|
@@ -2161,9 +2481,9 @@ function collect(value, previous) {
|
|
|
2161
2481
|
}
|
|
2162
2482
|
|
|
2163
2483
|
// src/commands/db/schema.ts
|
|
2164
|
-
import { Command as
|
|
2484
|
+
import { Command as Command13 } from "commander";
|
|
2165
2485
|
function dbSchemaCommand() {
|
|
2166
|
-
return new
|
|
2486
|
+
return new Command13("schema").description("Show database schema (property names, types, and options)").argument("<id>", "Notion database ID or URL").action(
|
|
2167
2487
|
withErrorHandling(async (id) => {
|
|
2168
2488
|
const { token } = await resolveToken();
|
|
2169
2489
|
const client = createNotionClient(token);
|
|
@@ -2187,13 +2507,13 @@ function dbSchemaCommand() {
|
|
|
2187
2507
|
}
|
|
2188
2508
|
|
|
2189
2509
|
// src/commands/edit-page.ts
|
|
2190
|
-
import { Command as
|
|
2510
|
+
import { Command as Command14 } from "commander";
|
|
2191
2511
|
function collect2(val, acc) {
|
|
2192
2512
|
acc.push(val);
|
|
2193
2513
|
return acc;
|
|
2194
2514
|
}
|
|
2195
2515
|
function editPageCommand() {
|
|
2196
|
-
const cmd = new
|
|
2516
|
+
const cmd = new Command14("edit-page");
|
|
2197
2517
|
cmd.description(
|
|
2198
2518
|
"replace a Notion page's content \u2014 full page or a targeted section"
|
|
2199
2519
|
).argument("<id/url>", "Notion page ID or URL").option(
|
|
@@ -2291,7 +2611,7 @@ function editPageCommand() {
|
|
|
2291
2611
|
|
|
2292
2612
|
// src/commands/init.ts
|
|
2293
2613
|
import { confirm, input as input2, password } from "@inquirer/prompts";
|
|
2294
|
-
import { Command as
|
|
2614
|
+
import { Command as Command15 } from "commander";
|
|
2295
2615
|
async function runInitFlow() {
|
|
2296
2616
|
const profileName = await input2({
|
|
2297
2617
|
message: "Profile name:",
|
|
@@ -2371,7 +2691,7 @@ async function runInitFlow() {
|
|
|
2371
2691
|
stderrWrite(dim(" notion auth login"));
|
|
2372
2692
|
}
|
|
2373
2693
|
function initCommand() {
|
|
2374
|
-
const cmd = new
|
|
2694
|
+
const cmd = new Command15("init");
|
|
2375
2695
|
cmd.description("authenticate with Notion and save a profile").action(
|
|
2376
2696
|
withErrorHandling(async () => {
|
|
2377
2697
|
if (!process.stdin.isTTY) {
|
|
@@ -2389,7 +2709,7 @@ function initCommand() {
|
|
|
2389
2709
|
|
|
2390
2710
|
// src/commands/ls.ts
|
|
2391
2711
|
import { isFullPageOrDataSource } from "@notionhq/client";
|
|
2392
|
-
import { Command as
|
|
2712
|
+
import { Command as Command16 } from "commander";
|
|
2393
2713
|
function getTitle(item) {
|
|
2394
2714
|
if (item.object === "data_source") {
|
|
2395
2715
|
return item.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
@@ -2406,7 +2726,7 @@ function displayType(item) {
|
|
|
2406
2726
|
return item.object === "data_source" ? "database" : item.object;
|
|
2407
2727
|
}
|
|
2408
2728
|
function lsCommand() {
|
|
2409
|
-
const cmd = new
|
|
2729
|
+
const cmd = new Command16("ls");
|
|
2410
2730
|
cmd.description("list accessible Notion pages and databases").option(
|
|
2411
2731
|
"--type <type>",
|
|
2412
2732
|
"filter by object type (page or database)",
|
|
@@ -2480,9 +2800,9 @@ function lsCommand() {
|
|
|
2480
2800
|
}
|
|
2481
2801
|
|
|
2482
2802
|
// src/commands/move.ts
|
|
2483
|
-
import { Command as
|
|
2803
|
+
import { Command as Command17 } from "commander";
|
|
2484
2804
|
function moveCommand() {
|
|
2485
|
-
const cmd = new
|
|
2805
|
+
const cmd = new Command17("move");
|
|
2486
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(
|
|
2487
2807
|
"--to-db <id/url>",
|
|
2488
2808
|
"target database parent ID or URL (resolves to data source)"
|
|
@@ -2540,10 +2860,10 @@ function moveCommand() {
|
|
|
2540
2860
|
// src/commands/open.ts
|
|
2541
2861
|
import { exec } from "child_process";
|
|
2542
2862
|
import { promisify } from "util";
|
|
2543
|
-
import { Command as
|
|
2863
|
+
import { Command as Command18 } from "commander";
|
|
2544
2864
|
var execAsync = promisify(exec);
|
|
2545
2865
|
function openCommand() {
|
|
2546
|
-
const cmd = new
|
|
2866
|
+
const cmd = new Command18("open");
|
|
2547
2867
|
cmd.description("open a Notion page in the default browser").argument("<id/url>", "Notion page ID or URL").action(
|
|
2548
2868
|
withErrorHandling(async (idOrUrl) => {
|
|
2549
2869
|
const id = parseNotionId(idOrUrl);
|
|
@@ -2559,9 +2879,9 @@ function openCommand() {
|
|
|
2559
2879
|
}
|
|
2560
2880
|
|
|
2561
2881
|
// src/commands/profile/list.ts
|
|
2562
|
-
import { Command as
|
|
2882
|
+
import { Command as Command19 } from "commander";
|
|
2563
2883
|
function profileListCommand() {
|
|
2564
|
-
const cmd = new
|
|
2884
|
+
const cmd = new Command19("list");
|
|
2565
2885
|
cmd.description("list all authentication profiles").action(
|
|
2566
2886
|
withErrorHandling(async () => {
|
|
2567
2887
|
const config = await readGlobalConfig();
|
|
@@ -2590,9 +2910,9 @@ function profileListCommand() {
|
|
|
2590
2910
|
}
|
|
2591
2911
|
|
|
2592
2912
|
// src/commands/profile/remove.ts
|
|
2593
|
-
import { Command as
|
|
2913
|
+
import { Command as Command20 } from "commander";
|
|
2594
2914
|
function profileRemoveCommand() {
|
|
2595
|
-
const cmd = new
|
|
2915
|
+
const cmd = new Command20("remove");
|
|
2596
2916
|
cmd.description("remove an authentication profile").argument("<name>", "profile name to remove").action(
|
|
2597
2917
|
withErrorHandling(async (name) => {
|
|
2598
2918
|
const config = await readGlobalConfig();
|
|
@@ -2618,9 +2938,9 @@ function profileRemoveCommand() {
|
|
|
2618
2938
|
}
|
|
2619
2939
|
|
|
2620
2940
|
// src/commands/profile/use.ts
|
|
2621
|
-
import { Command as
|
|
2941
|
+
import { Command as Command21 } from "commander";
|
|
2622
2942
|
function profileUseCommand() {
|
|
2623
|
-
const cmd = new
|
|
2943
|
+
const cmd = new Command21("use");
|
|
2624
2944
|
cmd.description("switch the active profile").argument("<name>", "profile name to activate").action(
|
|
2625
2945
|
withErrorHandling(async (name) => {
|
|
2626
2946
|
const config = await readGlobalConfig();
|
|
@@ -2643,7 +2963,7 @@ function profileUseCommand() {
|
|
|
2643
2963
|
}
|
|
2644
2964
|
|
|
2645
2965
|
// src/commands/read.ts
|
|
2646
|
-
import { Command as
|
|
2966
|
+
import { Command as Command22 } from "commander";
|
|
2647
2967
|
|
|
2648
2968
|
// src/output/markdown.ts
|
|
2649
2969
|
import { Chalk as Chalk2 } from "chalk";
|
|
@@ -2783,7 +3103,7 @@ async function fetchPageMarkdown(client, pageId) {
|
|
|
2783
3103
|
|
|
2784
3104
|
// src/commands/read.ts
|
|
2785
3105
|
function readCommand() {
|
|
2786
|
-
return new
|
|
3106
|
+
return new Command22("read").description("Read a Notion page as markdown").argument("<id>", "Notion page ID or URL").action(
|
|
2787
3107
|
withErrorHandling(async (id) => {
|
|
2788
3108
|
const { token } = await resolveToken();
|
|
2789
3109
|
const client = createNotionClient(token);
|
|
@@ -2809,7 +3129,7 @@ function readCommand() {
|
|
|
2809
3129
|
|
|
2810
3130
|
// src/commands/search.ts
|
|
2811
3131
|
import { isFullPageOrDataSource as isFullPageOrDataSource2 } from "@notionhq/client";
|
|
2812
|
-
import { Command as
|
|
3132
|
+
import { Command as Command23 } from "commander";
|
|
2813
3133
|
function getTitle2(item) {
|
|
2814
3134
|
if (item.object === "data_source") {
|
|
2815
3135
|
return item.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
@@ -2829,7 +3149,7 @@ function displayType2(item) {
|
|
|
2829
3149
|
return item.object === "data_source" ? "database" : item.object;
|
|
2830
3150
|
}
|
|
2831
3151
|
function searchCommand() {
|
|
2832
|
-
const cmd = new
|
|
3152
|
+
const cmd = new Command23("search");
|
|
2833
3153
|
cmd.description("search Notion workspace by keyword").argument("<query>", "search keyword").option(
|
|
2834
3154
|
"--type <type>",
|
|
2835
3155
|
"filter by object type (page or database)",
|
|
@@ -2899,18 +3219,144 @@ function searchCommand() {
|
|
|
2899
3219
|
return cmd;
|
|
2900
3220
|
}
|
|
2901
3221
|
|
|
3222
|
+
// src/commands/skill.ts
|
|
3223
|
+
import {
|
|
3224
|
+
copyFileSync,
|
|
3225
|
+
existsSync as existsSync4,
|
|
3226
|
+
mkdirSync,
|
|
3227
|
+
readFileSync,
|
|
3228
|
+
realpathSync
|
|
3229
|
+
} from "fs";
|
|
3230
|
+
import { homedir as homedir2 } from "os";
|
|
3231
|
+
import { dirname, join as join3 } from "path";
|
|
3232
|
+
import chalk from "chalk";
|
|
3233
|
+
import { Command as Command24 } from "commander";
|
|
3234
|
+
function skillPath() {
|
|
3235
|
+
if (!process.argv[1]) {
|
|
3236
|
+
throw new Error("Cannot determine install path. Run with: notion skill");
|
|
3237
|
+
}
|
|
3238
|
+
const entryPoint = realpathSync(process.argv[1]);
|
|
3239
|
+
const packageRoot = dirname(dirname(entryPoint));
|
|
3240
|
+
const candidates = [
|
|
3241
|
+
join3(packageRoot, ".agents", "skills", "using-notion-cli", "SKILL.md"),
|
|
3242
|
+
join3(
|
|
3243
|
+
dirname(entryPoint),
|
|
3244
|
+
"..",
|
|
3245
|
+
".agents",
|
|
3246
|
+
"skills",
|
|
3247
|
+
"using-notion-cli",
|
|
3248
|
+
"SKILL.md"
|
|
3249
|
+
)
|
|
3250
|
+
];
|
|
3251
|
+
for (const c2 of candidates) {
|
|
3252
|
+
if (existsSync4(c2)) return c2;
|
|
3253
|
+
}
|
|
3254
|
+
throw new Error(
|
|
3255
|
+
"SKILL.md not found. Reinstall with: npm install -g @andrzejchm/notion-cli"
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
function getAgentTargets() {
|
|
3259
|
+
const home = homedir2();
|
|
3260
|
+
const targets = [
|
|
3261
|
+
{
|
|
3262
|
+
name: "Claude Code",
|
|
3263
|
+
dir: join3(home, ".claude", "skills", "using-notion-cli")
|
|
3264
|
+
},
|
|
3265
|
+
{ name: "Codex", dir: join3(home, ".agents", "skills", "using-notion-cli") },
|
|
3266
|
+
{
|
|
3267
|
+
name: "OpenCode",
|
|
3268
|
+
dir: join3(home, ".config", "opencode", "skills", "using-notion-cli")
|
|
3269
|
+
}
|
|
3270
|
+
];
|
|
3271
|
+
return targets.map((t) => ({
|
|
3272
|
+
...t,
|
|
3273
|
+
detected: existsSync4(dirname(t.dir))
|
|
3274
|
+
}));
|
|
3275
|
+
}
|
|
3276
|
+
function installTo(source, target) {
|
|
3277
|
+
if (!existsSync4(target.dir)) mkdirSync(target.dir, { recursive: true });
|
|
3278
|
+
const dest = join3(target.dir, "SKILL.md");
|
|
3279
|
+
copyFileSync(source, dest);
|
|
3280
|
+
return dest;
|
|
3281
|
+
}
|
|
3282
|
+
async function installInteractive(source, targets) {
|
|
3283
|
+
const { checkbox } = await import("@inquirer/prompts");
|
|
3284
|
+
const selected = await checkbox({
|
|
3285
|
+
message: "Install skill for which agents?",
|
|
3286
|
+
choices: targets.map((t) => ({
|
|
3287
|
+
name: `${t.name}${t.detected ? chalk.dim(" (detected)") : ""}`,
|
|
3288
|
+
value: t.name,
|
|
3289
|
+
checked: t.detected
|
|
3290
|
+
}))
|
|
3291
|
+
});
|
|
3292
|
+
if (selected.length === 0) {
|
|
3293
|
+
process.stderr.write("No agents selected.\n");
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
process.stdout.write("\n");
|
|
3297
|
+
for (const name of selected) {
|
|
3298
|
+
const target = targets.find((t) => t.name === name);
|
|
3299
|
+
if (!target) continue;
|
|
3300
|
+
const dest = installTo(source, target);
|
|
3301
|
+
process.stdout.write(` Installed: ${target.name}: ${dest}
|
|
3302
|
+
`);
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
function installNonInteractive(source, targets) {
|
|
3306
|
+
const detected = targets.filter((t) => t.detected);
|
|
3307
|
+
if (detected.length === 0) {
|
|
3308
|
+
process.stderr.write(
|
|
3309
|
+
"No agents detected. Use --path to specify install location:\n notion skill --path ~/.claude/skills/using-notion-cli/SKILL.md\n"
|
|
3310
|
+
);
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
for (const target of detected) {
|
|
3314
|
+
const dest = installTo(source, target);
|
|
3315
|
+
process.stdout.write(`Installed: ${target.name}: ${dest}
|
|
3316
|
+
`);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
function skillCommand() {
|
|
3320
|
+
const cmd = new Command24("skill");
|
|
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(
|
|
3322
|
+
withErrorHandling(async (opts) => {
|
|
3323
|
+
if (opts.print) {
|
|
3324
|
+
process.stdout.write(readFileSync(skillPath(), "utf-8"));
|
|
3325
|
+
return;
|
|
3326
|
+
}
|
|
3327
|
+
const source = skillPath();
|
|
3328
|
+
if (opts.path) {
|
|
3329
|
+
const dir = dirname(opts.path);
|
|
3330
|
+
if (!existsSync4(dir)) mkdirSync(dir, { recursive: true });
|
|
3331
|
+
copyFileSync(source, opts.path);
|
|
3332
|
+
process.stdout.write(`Installed to ${opts.path}
|
|
3333
|
+
`);
|
|
3334
|
+
return;
|
|
3335
|
+
}
|
|
3336
|
+
const targets = getAgentTargets();
|
|
3337
|
+
const isTTY = process.stdout.isTTY ?? false;
|
|
3338
|
+
if (isTTY) {
|
|
3339
|
+
await installInteractive(source, targets);
|
|
3340
|
+
} else {
|
|
3341
|
+
installNonInteractive(source, targets);
|
|
3342
|
+
}
|
|
3343
|
+
})
|
|
3344
|
+
);
|
|
3345
|
+
return cmd;
|
|
3346
|
+
}
|
|
3347
|
+
|
|
2902
3348
|
// src/commands/update.ts
|
|
2903
|
-
import { Command as
|
|
2904
|
-
function
|
|
3349
|
+
import { Command as Command25 } from "commander";
|
|
3350
|
+
function collectProps2(val, acc) {
|
|
2905
3351
|
acc.push(val);
|
|
2906
3352
|
return acc;
|
|
2907
3353
|
}
|
|
2908
3354
|
function updateCommand() {
|
|
2909
|
-
const cmd = new
|
|
3355
|
+
const cmd = new Command25("update");
|
|
2910
3356
|
cmd.description("update properties on a Notion page").argument("<id/url>", "Notion page ID or URL").option(
|
|
2911
3357
|
"--prop <property=value>",
|
|
2912
3358
|
"set a property value (repeatable)",
|
|
2913
|
-
|
|
3359
|
+
collectProps2,
|
|
2914
3360
|
[]
|
|
2915
3361
|
).option("--title <title>", "set the page title").action(
|
|
2916
3362
|
withErrorHandling(async (idOrUrl, opts) => {
|
|
@@ -2964,7 +3410,7 @@ function updateCommand() {
|
|
|
2964
3410
|
}
|
|
2965
3411
|
|
|
2966
3412
|
// src/commands/users.ts
|
|
2967
|
-
import { Command as
|
|
3413
|
+
import { Command as Command26 } from "commander";
|
|
2968
3414
|
function getEmailOrWorkspace(user) {
|
|
2969
3415
|
if (user.type === "person") {
|
|
2970
3416
|
return user.person.email ?? "\u2014";
|
|
@@ -2976,7 +3422,7 @@ function getEmailOrWorkspace(user) {
|
|
|
2976
3422
|
return "\u2014";
|
|
2977
3423
|
}
|
|
2978
3424
|
function usersCommand() {
|
|
2979
|
-
const cmd = new
|
|
3425
|
+
const cmd = new Command26("users");
|
|
2980
3426
|
cmd.description("list all users in the workspace").option("--json", "output as JSON").action(
|
|
2981
3427
|
withErrorHandling(async (opts) => {
|
|
2982
3428
|
if (opts.json) setOutputMode("json");
|
|
@@ -3003,11 +3449,11 @@ function usersCommand() {
|
|
|
3003
3449
|
|
|
3004
3450
|
// src/cli.ts
|
|
3005
3451
|
var __filename = fileURLToPath(import.meta.url);
|
|
3006
|
-
var __dirname =
|
|
3452
|
+
var __dirname = dirname2(__filename);
|
|
3007
3453
|
var pkg = JSON.parse(
|
|
3008
|
-
|
|
3454
|
+
readFileSync2(join4(__dirname, "../package.json"), "utf-8")
|
|
3009
3455
|
);
|
|
3010
|
-
var program = new
|
|
3456
|
+
var program = new Command27();
|
|
3011
3457
|
program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
|
|
3012
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");
|
|
3013
3459
|
program.configureOutput({
|
|
@@ -3028,7 +3474,7 @@ program.hook("preAction", (thisCommand) => {
|
|
|
3028
3474
|
setOutputMode("md");
|
|
3029
3475
|
}
|
|
3030
3476
|
});
|
|
3031
|
-
var authCmd = new
|
|
3477
|
+
var authCmd = new Command27("auth").description("manage Notion authentication");
|
|
3032
3478
|
authCmd.action(authDefaultAction(authCmd));
|
|
3033
3479
|
authCmd.addCommand(loginCommand());
|
|
3034
3480
|
authCmd.addCommand(logoutCommand());
|
|
@@ -3038,7 +3484,7 @@ authCmd.addCommand(profileUseCommand());
|
|
|
3038
3484
|
authCmd.addCommand(profileRemoveCommand());
|
|
3039
3485
|
program.addCommand(authCmd);
|
|
3040
3486
|
program.addCommand(initCommand(), { hidden: true });
|
|
3041
|
-
var profileCmd = new
|
|
3487
|
+
var profileCmd = new Command27("profile").description(
|
|
3042
3488
|
"manage authentication profiles"
|
|
3043
3489
|
);
|
|
3044
3490
|
profileCmd.addCommand(profileListCommand());
|
|
@@ -3053,16 +3499,18 @@ program.addCommand(commentsCommand());
|
|
|
3053
3499
|
program.addCommand(readCommand());
|
|
3054
3500
|
program.addCommand(commentAddCommand());
|
|
3055
3501
|
program.addCommand(appendCommand());
|
|
3502
|
+
program.addCommand(attachCommand());
|
|
3056
3503
|
program.addCommand(createPageCommand());
|
|
3057
3504
|
program.addCommand(editPageCommand());
|
|
3058
3505
|
program.addCommand(updateCommand());
|
|
3059
3506
|
program.addCommand(archiveCommand());
|
|
3060
3507
|
program.addCommand(moveCommand());
|
|
3061
|
-
var dbCmd = new
|
|
3508
|
+
var dbCmd = new Command27("db").description("Database operations");
|
|
3062
3509
|
dbCmd.addCommand(dbCreateCommand());
|
|
3063
3510
|
dbCmd.addCommand(dbSchemaCommand());
|
|
3064
3511
|
dbCmd.addCommand(dbQueryCommand());
|
|
3065
3512
|
program.addCommand(dbCmd);
|
|
3066
3513
|
program.addCommand(completionCommand());
|
|
3514
|
+
program.addCommand(skillCommand());
|
|
3067
3515
|
await program.parseAsync();
|
|
3068
3516
|
//# sourceMappingURL=cli.js.map
|