@andrzejchm/notion-cli 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4,229 +4,11 @@
4
4
  import { readFileSync } from "fs";
5
5
  import { dirname, join as join3 } from "path";
6
6
  import { fileURLToPath } from "url";
7
- import { Command as Command20 } from "commander";
7
+ import { Command as Command21 } from "commander";
8
8
 
9
9
  // src/commands/append.ts
10
10
  import { Command } from "commander";
11
11
 
12
- // src/blocks/md-to-blocks.ts
13
- var INLINE_RE = /(\*\*[^*]+\*\*|_[^_]+_|\*[^*]+\*|`[^`]+`|\[[^\]]+\]\([^)]+\)|[^*_`[]+)/g;
14
- function parseInlineMarkdown(text) {
15
- const result = [];
16
- let match;
17
- INLINE_RE.lastIndex = 0;
18
- while ((match = INLINE_RE.exec(text)) !== null) {
19
- const segment = match[0];
20
- if (segment.startsWith("**") && segment.endsWith("**")) {
21
- const content = segment.slice(2, -2);
22
- result.push({
23
- type: "text",
24
- text: { content, link: null },
25
- annotations: {
26
- bold: true,
27
- italic: false,
28
- strikethrough: false,
29
- underline: false,
30
- code: false,
31
- color: "default"
32
- }
33
- });
34
- continue;
35
- }
36
- if (segment.startsWith("_") && segment.endsWith("_") || segment.startsWith("*") && segment.endsWith("*")) {
37
- const content = segment.slice(1, -1);
38
- result.push({
39
- type: "text",
40
- text: { content, link: null },
41
- annotations: {
42
- bold: false,
43
- italic: true,
44
- strikethrough: false,
45
- underline: false,
46
- code: false,
47
- color: "default"
48
- }
49
- });
50
- continue;
51
- }
52
- if (segment.startsWith("`") && segment.endsWith("`")) {
53
- const content = segment.slice(1, -1);
54
- result.push({
55
- type: "text",
56
- text: { content, link: null },
57
- annotations: {
58
- bold: false,
59
- italic: false,
60
- strikethrough: false,
61
- underline: false,
62
- code: true,
63
- color: "default"
64
- }
65
- });
66
- continue;
67
- }
68
- const linkMatch = segment.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
69
- if (linkMatch) {
70
- const [, label, url] = linkMatch;
71
- result.push({
72
- type: "text",
73
- text: { content: label, link: { url } },
74
- annotations: {
75
- bold: false,
76
- italic: false,
77
- strikethrough: false,
78
- underline: false,
79
- code: false,
80
- color: "default"
81
- }
82
- });
83
- continue;
84
- }
85
- result.push({
86
- type: "text",
87
- text: { content: segment, link: null },
88
- annotations: {
89
- bold: false,
90
- italic: false,
91
- strikethrough: false,
92
- underline: false,
93
- code: false,
94
- color: "default"
95
- }
96
- });
97
- }
98
- return result;
99
- }
100
- function makeRichText(text) {
101
- return parseInlineMarkdown(text);
102
- }
103
- function mdToBlocks(md) {
104
- if (!md) return [];
105
- const lines = md.split("\n");
106
- const blocks = [];
107
- let inFence = false;
108
- let fenceLang = "";
109
- const fenceLines = [];
110
- for (const line of lines) {
111
- if (!inFence && line.startsWith("```")) {
112
- inFence = true;
113
- fenceLang = line.slice(3).trim() || "plain text";
114
- fenceLines.length = 0;
115
- continue;
116
- }
117
- if (inFence) {
118
- if (line.startsWith("```")) {
119
- const content = fenceLines.join("\n");
120
- blocks.push({
121
- type: "code",
122
- code: {
123
- rich_text: [
124
- {
125
- type: "text",
126
- text: { content, link: null },
127
- annotations: {
128
- bold: false,
129
- italic: false,
130
- strikethrough: false,
131
- underline: false,
132
- code: false,
133
- color: "default"
134
- }
135
- }
136
- ],
137
- // biome-ignore lint/suspicious/noExplicitAny: Notion SDK language type is too narrow
138
- language: fenceLang
139
- }
140
- });
141
- inFence = false;
142
- fenceLang = "";
143
- fenceLines.length = 0;
144
- } else {
145
- fenceLines.push(line);
146
- }
147
- continue;
148
- }
149
- if (line.trim() === "") continue;
150
- const h1 = line.match(/^# (.+)$/);
151
- if (h1) {
152
- blocks.push({
153
- type: "heading_1",
154
- heading_1: { rich_text: makeRichText(h1[1]) }
155
- });
156
- continue;
157
- }
158
- const h2 = line.match(/^## (.+)$/);
159
- if (h2) {
160
- blocks.push({
161
- type: "heading_2",
162
- heading_2: { rich_text: makeRichText(h2[1]) }
163
- });
164
- continue;
165
- }
166
- const h3 = line.match(/^### (.+)$/);
167
- if (h3) {
168
- blocks.push({
169
- type: "heading_3",
170
- heading_3: { rich_text: makeRichText(h3[1]) }
171
- });
172
- continue;
173
- }
174
- const bullet = line.match(/^[-*] (.+)$/);
175
- if (bullet) {
176
- blocks.push({
177
- type: "bulleted_list_item",
178
- bulleted_list_item: { rich_text: makeRichText(bullet[1]) }
179
- });
180
- continue;
181
- }
182
- const numbered = line.match(/^\d+\. (.+)$/);
183
- if (numbered) {
184
- blocks.push({
185
- type: "numbered_list_item",
186
- numbered_list_item: { rich_text: makeRichText(numbered[1]) }
187
- });
188
- continue;
189
- }
190
- const quote = line.match(/^> (.+)$/);
191
- if (quote) {
192
- blocks.push({
193
- type: "quote",
194
- quote: { rich_text: makeRichText(quote[1]) }
195
- });
196
- continue;
197
- }
198
- blocks.push({
199
- type: "paragraph",
200
- paragraph: { rich_text: makeRichText(line) }
201
- });
202
- }
203
- if (inFence && fenceLines.length > 0) {
204
- const content = fenceLines.join("\n");
205
- blocks.push({
206
- type: "code",
207
- code: {
208
- rich_text: [
209
- {
210
- type: "text",
211
- text: { content, link: null },
212
- annotations: {
213
- bold: false,
214
- italic: false,
215
- strikethrough: false,
216
- underline: false,
217
- code: false,
218
- color: "default"
219
- }
220
- }
221
- ],
222
- // biome-ignore lint/suspicious/noExplicitAny: Notion SDK language type is too narrow
223
- language: fenceLang
224
- }
225
- });
226
- }
227
- return blocks;
228
- }
229
-
230
12
  // src/errors/cli-error.ts
231
13
  var CliError = class extends Error {
232
14
  constructor(code, message, suggestion, cause) {
@@ -662,6 +444,12 @@ function withErrorHandling(fn) {
662
444
  });
663
445
  }
664
446
 
447
+ // src/errors/notion-errors.ts
448
+ var SELECTOR_HINT = 'Use an ellipsis selector matching page content, e.g. "## Section...end of section". Run `notion read <id>` to see the page content.';
449
+ function isNotionValidationError(error2) {
450
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "validation_error";
451
+ }
452
+
665
453
  // src/notion/client.ts
666
454
  import { APIErrorCode, Client, isNotionClientError } from "@notionhq/client";
667
455
  async function validateToken(token) {
@@ -775,13 +563,79 @@ async function addComment(client, pageId, text, options = {}) {
775
563
  ...options.asUser && { display_name: { type: "user" } }
776
564
  });
777
565
  }
778
- async function appendBlocks(client, blockId, blocks) {
779
- await client.blocks.children.append({
780
- block_id: blockId,
781
- children: blocks
566
+ async function appendMarkdown(client, pageId, markdown, options) {
567
+ await client.pages.updateMarkdown({
568
+ page_id: pageId,
569
+ type: "insert_content",
570
+ insert_content: {
571
+ content: markdown,
572
+ ...options?.after != null && { after: options.after }
573
+ }
574
+ });
575
+ }
576
+ function countOccurrences(text, sub) {
577
+ if (!sub) return 0;
578
+ let count = 0;
579
+ let pos = text.indexOf(sub, 0);
580
+ while (pos !== -1) {
581
+ count++;
582
+ pos = text.indexOf(sub, pos + sub.length);
583
+ }
584
+ return count;
585
+ }
586
+ function buildContentRange(content) {
587
+ const START_LEN = 15;
588
+ const STEP = 10;
589
+ if (content.length <= START_LEN * 2) {
590
+ return content;
591
+ }
592
+ const start = content.slice(0, START_LEN);
593
+ for (let endLen = START_LEN; endLen < content.length - START_LEN; endLen += STEP) {
594
+ const end = content.slice(-endLen);
595
+ if (countOccurrences(content, end) === 1) {
596
+ return `${start}...${end}`;
597
+ }
598
+ }
599
+ return content;
600
+ }
601
+ async function replaceMarkdown(client, pageId, newMarkdown, options) {
602
+ const current = await client.pages.retrieveMarkdown({ page_id: pageId });
603
+ const currentContent = current.markdown.trim();
604
+ if (current.truncated && !options?.range) {
605
+ throw new CliError(
606
+ ErrorCodes.API_ERROR,
607
+ "Page content is too large for full-page replace (markdown was truncated by the API).",
608
+ "Use --range to replace a specific section instead."
609
+ );
610
+ }
611
+ if (!currentContent) {
612
+ if (options?.range) {
613
+ process.stderr.write(
614
+ "Warning: page is empty, --range ignored, content inserted as-is.\n"
615
+ );
616
+ }
617
+ if (newMarkdown.trim()) {
618
+ await client.pages.updateMarkdown({
619
+ page_id: pageId,
620
+ type: "insert_content",
621
+ insert_content: { content: newMarkdown }
622
+ });
623
+ }
624
+ return;
625
+ }
626
+ const contentRange = options?.range ?? buildContentRange(currentContent);
627
+ const allowDeletingContent = options?.allowDeletingContent ?? options?.range == null;
628
+ await client.pages.updateMarkdown({
629
+ page_id: pageId,
630
+ type: "replace_content_range",
631
+ replace_content_range: {
632
+ content: newMarkdown,
633
+ content_range: contentRange,
634
+ allow_deleting_content: allowDeletingContent
635
+ }
782
636
  });
783
637
  }
784
- async function createPage(client, parentId, title, blocks) {
638
+ async function createPage(client, parentId, title, markdown) {
785
639
  const response = await client.pages.create({
786
640
  parent: { type: "page_id", page_id: parentId },
787
641
  properties: {
@@ -789,30 +643,72 @@ async function createPage(client, parentId, title, blocks) {
789
643
  title: [{ type: "text", text: { content: title, link: null } }]
790
644
  }
791
645
  },
792
- children: blocks
646
+ ...markdown.trim() ? { markdown } : {}
793
647
  });
794
- return response.url;
648
+ const url = "url" in response ? response.url : response.id;
649
+ return url;
650
+ }
651
+
652
+ // src/utils/stdin.ts
653
+ async function readStdin() {
654
+ const chunks = [];
655
+ for await (const chunk of process.stdin) {
656
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
657
+ }
658
+ return Buffer.concat(chunks).toString("utf-8");
795
659
  }
796
660
 
797
661
  // src/commands/append.ts
798
662
  function appendCommand() {
799
663
  const cmd = new Command("append");
800
- cmd.description("append markdown content to a Notion page").argument("<id/url>", "Notion page ID or URL").requiredOption("-m, --message <markdown>", "markdown content to append").action(
801
- withErrorHandling(async (idOrUrl, opts) => {
802
- const { token, source } = await resolveToken();
803
- reportTokenSource(source);
804
- const client = createNotionClient(token);
805
- const pageId = parseNotionId(idOrUrl);
806
- const uuid = toUuid(pageId);
807
- const blocks = mdToBlocks(opts.message);
808
- if (blocks.length === 0) {
809
- process.stdout.write("Nothing to append.\n");
810
- return;
664
+ 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(
665
+ "--after <selector>",
666
+ 'insert after matched content \u2014 ellipsis selector, e.g. "## Section...end of section"'
667
+ ).action(
668
+ withErrorHandling(
669
+ async (idOrUrl, opts) => {
670
+ const { token, source } = await resolveToken();
671
+ reportTokenSource(source);
672
+ const client = createNotionClient(token);
673
+ let markdown = "";
674
+ if (opts.message) {
675
+ markdown = opts.message;
676
+ } else if (!process.stdin.isTTY) {
677
+ markdown = await readStdin();
678
+ } else {
679
+ throw new CliError(
680
+ ErrorCodes.INVALID_ARG,
681
+ "No content to append.",
682
+ "Pass markdown via -m/--message or pipe it through stdin"
683
+ );
684
+ }
685
+ if (!markdown.trim()) {
686
+ process.stdout.write("Nothing to append.\n");
687
+ return;
688
+ }
689
+ const pageId = parseNotionId(idOrUrl);
690
+ const uuid = toUuid(pageId);
691
+ try {
692
+ await appendMarkdown(
693
+ client,
694
+ uuid,
695
+ markdown,
696
+ opts.after ? { after: opts.after } : void 0
697
+ );
698
+ } catch (error2) {
699
+ if (opts.after && isNotionValidationError(error2)) {
700
+ throw new CliError(
701
+ ErrorCodes.INVALID_ARG,
702
+ `Selector not found: "${opts.after}". ${error2.message}`,
703
+ SELECTOR_HINT,
704
+ error2
705
+ );
706
+ }
707
+ throw error2;
708
+ }
709
+ process.stdout.write("Appended.\n");
811
710
  }
812
- await appendBlocks(client, uuid, blocks);
813
- process.stdout.write(`Appended ${blocks.length} block(s).
814
- `);
815
- })
711
+ )
816
712
  );
817
713
  return cmd;
818
714
  }
@@ -825,8 +721,8 @@ function authDefaultAction(authCmd2) {
825
721
  }
826
722
 
827
723
  // src/commands/auth/login.ts
828
- import { input as input2, select } from "@inquirer/prompts";
829
- import { Command as Command3 } from "commander";
724
+ import { input } from "@inquirer/prompts";
725
+ import { Command as Command2 } from "commander";
830
726
 
831
727
  // src/oauth/oauth-flow.ts
832
728
  import { spawn } from "child_process";
@@ -1071,108 +967,10 @@ Waiting for callback (up to 120 seconds)...
1071
967
  });
1072
968
  }
1073
969
 
1074
- // src/commands/init.ts
1075
- import { confirm, input, password } from "@inquirer/prompts";
1076
- import { Command as Command2 } from "commander";
1077
- async function runInitFlow() {
1078
- const profileName = await input({
1079
- message: "Profile name:",
1080
- default: "default"
1081
- });
1082
- const token = await password({
1083
- message: "Integration token (from notion.so/profile/integrations/internal):",
1084
- mask: "*"
1085
- });
1086
- stderrWrite("Validating token...");
1087
- const { workspaceName, workspaceId } = await validateToken(token);
1088
- stderrWrite(success(`\u2713 Connected to workspace: ${bold(workspaceName)}`));
1089
- const config = await readGlobalConfig();
1090
- if (config.profiles?.[profileName]) {
1091
- const replace = await confirm({
1092
- message: `Profile "${profileName}" already exists. Replace?`,
1093
- default: false
1094
- });
1095
- if (!replace) {
1096
- stderrWrite("Aborted.");
1097
- return;
1098
- }
1099
- }
1100
- const profiles = config.profiles ?? {};
1101
- profiles[profileName] = {
1102
- token,
1103
- workspace_name: workspaceName,
1104
- workspace_id: workspaceId
1105
- };
1106
- await writeGlobalConfig({
1107
- ...config,
1108
- profiles,
1109
- active_profile: profileName
1110
- });
1111
- stderrWrite(success(`Profile "${profileName}" saved and set as active.`));
1112
- stderrWrite(dim("Checking integration access..."));
1113
- try {
1114
- const notion = createNotionClient(token);
1115
- const probe = await notion.search({ page_size: 1 });
1116
- if (probe.results.length === 0) {
1117
- stderrWrite("");
1118
- stderrWrite("\u26A0\uFE0F Your integration has no pages connected.");
1119
- stderrWrite(" To grant access, open any Notion page or database:");
1120
- stderrWrite(" 1. Click \xB7\xB7\xB7 (three dots) in the top-right corner");
1121
- stderrWrite(' 2. Select "Connect to"');
1122
- stderrWrite(` 3. Choose "${workspaceName}"`);
1123
- stderrWrite(" Then re-run any notion command to confirm access.");
1124
- } else {
1125
- stderrWrite(
1126
- success(
1127
- `\u2713 Integration has access to content in ${bold(workspaceName)}.`
1128
- )
1129
- );
1130
- }
1131
- } catch {
1132
- stderrWrite(
1133
- dim("(Could not verify integration access \u2014 run `notion ls` to check)")
1134
- );
1135
- }
1136
- stderrWrite("");
1137
- stderrWrite(
1138
- dim("Write commands (comment, append, create-page) require additional")
1139
- );
1140
- stderrWrite(dim("capabilities in your integration settings:"));
1141
- stderrWrite(
1142
- dim(" notion.so/profile/integrations/internal \u2192 your integration \u2192")
1143
- );
1144
- stderrWrite(
1145
- dim(
1146
- ' Capabilities: enable "Read content", "Insert content", "Read comments", "Insert comments"'
1147
- )
1148
- );
1149
- stderrWrite("");
1150
- stderrWrite(
1151
- dim("To post comments and create pages attributed to your user account:")
1152
- );
1153
- stderrWrite(dim(" notion auth login"));
1154
- }
1155
- function initCommand() {
1156
- const cmd = new Command2("init");
1157
- cmd.description("authenticate with Notion and save a profile").action(
1158
- withErrorHandling(async () => {
1159
- if (!process.stdin.isTTY) {
1160
- throw new CliError(
1161
- ErrorCodes.AUTH_NO_TOKEN,
1162
- "Cannot run interactive init in non-TTY mode.",
1163
- "Set NOTION_API_TOKEN environment variable or create .notion.yaml"
1164
- );
1165
- }
1166
- await runInitFlow();
1167
- })
1168
- );
1169
- return cmd;
1170
- }
1171
-
1172
970
  // src/commands/auth/login.ts
1173
971
  function loginCommand() {
1174
- const cmd = new Command3("login");
1175
- cmd.description("authenticate with Notion \u2014 choose OAuth or integration token").option("--profile <name>", "profile name to store credentials in").option(
972
+ const cmd = new Command2("login");
973
+ cmd.description("authenticate with Notion via OAuth").option("--profile <name>", "profile name to store credentials in").option(
1176
974
  "--manual",
1177
975
  "print auth URL instead of opening browser (for headless OAuth)"
1178
976
  ).action(
@@ -1184,76 +982,57 @@ function loginCommand() {
1184
982
  "Use --manual flag to get an auth URL you can open in a browser"
1185
983
  );
1186
984
  }
1187
- const method = await select({
1188
- message: "How do you want to authenticate with Notion?",
1189
- choices: [
1190
- {
1191
- name: "OAuth user login (browser required)",
1192
- value: "oauth",
1193
- description: "Opens Notion in your browser. Comments and pages are attributed to your account. Tokens auto-refresh."
1194
- },
1195
- {
1196
- name: "Internal integration token (CI/headless friendly)",
1197
- value: "token",
1198
- description: "Paste a token from notion.so/profile/integrations. No browser needed. Write ops attributed to integration bot."
1199
- }
1200
- ]
1201
- });
1202
- if (method === "oauth") {
1203
- const result = await runOAuthFlow({ manual: opts.manual });
1204
- const response = await exchangeCode(result.code);
1205
- const userName = response.owner?.user?.name ?? "unknown user";
1206
- const workspaceName = response.workspace_name ?? "unknown workspace";
1207
- const config = await readGlobalConfig();
1208
- const existingProfiles = config.profiles ?? {};
1209
- let profileName = opts.profile;
1210
- if (!profileName) {
1211
- const suggested = workspaceName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "default";
1212
- profileName = await input2({
1213
- message: "Profile name to save this account under:",
1214
- default: suggested
1215
- });
1216
- }
1217
- const isUpdate = Boolean(existingProfiles[profileName]);
1218
- const isFirst = Object.keys(existingProfiles).length === 0;
1219
- if (isUpdate) {
1220
- stderrWrite(dim(`Updating existing profile "${profileName}"...`));
1221
- }
1222
- await saveOAuthTokens(profileName, response);
1223
- if (isFirst) {
1224
- const updated = await readGlobalConfig();
1225
- await writeGlobalConfig({
1226
- ...updated,
1227
- active_profile: profileName
1228
- });
1229
- }
1230
- stderrWrite(
1231
- success(`\u2713 Logged in as ${userName} to workspace ${workspaceName}`)
1232
- );
1233
- stderrWrite(dim(`Saved as profile "${profileName}".`));
1234
- if (!isFirst && !isUpdate) {
1235
- stderrWrite(
1236
- dim(
1237
- `Run "notion auth use ${profileName}" to switch to this profile.`
1238
- )
1239
- );
1240
- }
1241
- stderrWrite(
1242
- dim(
1243
- "Your comments and pages will now be attributed to your Notion account."
1244
- )
985
+ const result = await runOAuthFlow({ manual: opts.manual });
986
+ const response = await exchangeCode(result.code);
987
+ const userName = response.owner?.user?.name ?? "unknown user";
988
+ const workspaceName = response.workspace_name ?? "unknown workspace";
989
+ const config = await readGlobalConfig();
990
+ const existingProfiles = config.profiles ?? {};
991
+ let profileName = opts.profile;
992
+ if (!profileName) {
993
+ const suggested = workspaceName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "default";
994
+ profileName = await input({
995
+ message: "Profile name to save this account under:",
996
+ default: suggested
997
+ });
998
+ }
999
+ const isUpdate = Boolean(existingProfiles[profileName]);
1000
+ const isFirst = Object.keys(existingProfiles).length === 0;
1001
+ if (isUpdate) {
1002
+ stderrWrite(dim(`Updating existing profile "${profileName}"...`));
1003
+ }
1004
+ await saveOAuthTokens(profileName, response);
1005
+ if (isFirst) {
1006
+ const updated = await readGlobalConfig();
1007
+ await writeGlobalConfig({
1008
+ ...updated,
1009
+ active_profile: profileName
1010
+ });
1011
+ }
1012
+ stderrWrite(
1013
+ success(`\u2713 Logged in as ${userName} to workspace ${workspaceName}`)
1014
+ );
1015
+ stderrWrite(dim(`Saved as profile "${profileName}".`));
1016
+ if (!isFirst && !isUpdate) {
1017
+ stderrWrite(
1018
+ dim(
1019
+ `Run "notion auth use ${profileName}" to switch to this profile.`
1020
+ )
1245
1021
  );
1246
- } else {
1247
- await runInitFlow();
1248
1022
  }
1023
+ stderrWrite(
1024
+ dim(
1025
+ "Your comments and pages will now be attributed to your Notion account."
1026
+ )
1027
+ );
1249
1028
  })
1250
1029
  );
1251
1030
  return cmd;
1252
1031
  }
1253
1032
 
1254
1033
  // src/commands/auth/logout.ts
1255
- import { select as select2 } from "@inquirer/prompts";
1256
- import { Command as Command4 } from "commander";
1034
+ import { select } from "@inquirer/prompts";
1035
+ import { Command as Command3 } from "commander";
1257
1036
  function profileLabel(name, profile) {
1258
1037
  const parts = [];
1259
1038
  if (profile.oauth_access_token)
@@ -1266,7 +1045,7 @@ function profileLabel(name, profile) {
1266
1045
  return `${bold(name)} ${dim(authDesc)}${workspace}`;
1267
1046
  }
1268
1047
  function logoutCommand() {
1269
- const cmd = new Command4("logout");
1048
+ const cmd = new Command3("logout");
1270
1049
  cmd.description("remove a profile and its credentials").option(
1271
1050
  "--profile <name>",
1272
1051
  "profile name to remove (skips interactive selector)"
@@ -1288,7 +1067,7 @@ function logoutCommand() {
1288
1067
  "Use --profile <name> to specify the profile to remove"
1289
1068
  );
1290
1069
  }
1291
- profileName = await select2({
1070
+ profileName = await select({
1292
1071
  message: "Which profile do you want to log out of?",
1293
1072
  choices: profileNames.map((name) => ({
1294
1073
  // biome-ignore lint/style/noNonNullAssertion: key is from Object.keys, always present
@@ -1324,9 +1103,9 @@ function logoutCommand() {
1324
1103
  }
1325
1104
 
1326
1105
  // src/commands/auth/status.ts
1327
- import { Command as Command5 } from "commander";
1106
+ import { Command as Command4 } from "commander";
1328
1107
  function statusCommand() {
1329
- const cmd = new Command5("status");
1108
+ const cmd = new Command4("status");
1330
1109
  cmd.description("show authentication status for the active profile").option("--profile <name>", "profile name to check").action(
1331
1110
  withErrorHandling(async (opts) => {
1332
1111
  let profileName = opts.profile;
@@ -1384,9 +1163,9 @@ function statusCommand() {
1384
1163
  }
1385
1164
 
1386
1165
  // src/commands/comment-add.ts
1387
- import { Command as Command6 } from "commander";
1166
+ import { Command as Command5 } from "commander";
1388
1167
  function commentAddCommand() {
1389
- const cmd = new Command6("comment");
1168
+ const cmd = new Command5("comment");
1390
1169
  cmd.description("add a comment to a Notion page").argument("<id/url>", "Notion page ID or URL").requiredOption("-m, --message <text>", "comment text to post").action(
1391
1170
  withErrorHandling(async (idOrUrl, opts) => {
1392
1171
  const { token, source } = await resolveToken();
@@ -1404,7 +1183,7 @@ function commentAddCommand() {
1404
1183
  }
1405
1184
 
1406
1185
  // src/commands/comments.ts
1407
- import { Command as Command7 } from "commander";
1186
+ import { Command as Command6 } from "commander";
1408
1187
 
1409
1188
  // src/output/format.ts
1410
1189
  var _mode = "auto";
@@ -1489,7 +1268,7 @@ async function paginateResults(fetcher) {
1489
1268
 
1490
1269
  // src/commands/comments.ts
1491
1270
  function commentsCommand() {
1492
- const cmd = new Command7("comments");
1271
+ const cmd = new Command6("comments");
1493
1272
  cmd.description("list comments on a Notion page").argument("<id/url>", "Notion page ID or URL").option("--json", "output as JSON").action(
1494
1273
  withErrorHandling(async (idOrUrl, opts) => {
1495
1274
  if (opts.json) setOutputMode("json");
@@ -1520,7 +1299,7 @@ function commentsCommand() {
1520
1299
  }
1521
1300
 
1522
1301
  // src/commands/completion.ts
1523
- import { Command as Command8 } from "commander";
1302
+ import { Command as Command7 } from "commander";
1524
1303
  var BASH_COMPLETION = `# notion bash completion
1525
1304
  _notion_completion() {
1526
1305
  local cur prev words cword
@@ -1622,7 +1401,7 @@ complete -c notion -n '__fish_seen_subcommand_from completion' -a zsh -d 'zsh co
1622
1401
  complete -c notion -n '__fish_seen_subcommand_from completion' -a fish -d 'fish completion script'
1623
1402
  `;
1624
1403
  function completionCommand() {
1625
- const cmd = new Command8("completion");
1404
+ const cmd = new Command7("completion");
1626
1405
  cmd.description("output shell completion script").argument("<shell>", "shell type (bash, zsh, fish)").action(
1627
1406
  withErrorHandling(async (shell) => {
1628
1407
  switch (shell) {
@@ -1648,16 +1427,9 @@ function completionCommand() {
1648
1427
  }
1649
1428
 
1650
1429
  // src/commands/create-page.ts
1651
- import { Command as Command9 } from "commander";
1652
- async function readStdin() {
1653
- const chunks = [];
1654
- for await (const chunk of process.stdin) {
1655
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1656
- }
1657
- return Buffer.concat(chunks).toString("utf-8");
1658
- }
1430
+ import { Command as Command8 } from "commander";
1659
1431
  function createPageCommand() {
1660
- const cmd = new Command9("create-page");
1432
+ const cmd = new Command8("create-page");
1661
1433
  cmd.description("create a new Notion page under a parent page").requiredOption("--parent <id/url>", "parent page ID or URL").requiredOption("--title <title>", "page title").option(
1662
1434
  "-m, --message <markdown>",
1663
1435
  "inline markdown content for the page body"
@@ -1673,9 +1445,13 @@ function createPageCommand() {
1673
1445
  } else if (!process.stdin.isTTY) {
1674
1446
  markdown = await readStdin();
1675
1447
  }
1676
- const blocks = mdToBlocks(markdown);
1677
1448
  const parentUuid = toUuid(parseNotionId(opts.parent));
1678
- const url = await createPage(client, parentUuid, opts.title, blocks);
1449
+ const url = await createPage(
1450
+ client,
1451
+ parentUuid,
1452
+ opts.title,
1453
+ markdown
1454
+ );
1679
1455
  process.stdout.write(`${url}
1680
1456
  `);
1681
1457
  }
@@ -1685,7 +1461,7 @@ function createPageCommand() {
1685
1461
  }
1686
1462
 
1687
1463
  // src/commands/db/query.ts
1688
- import { Command as Command10 } from "commander";
1464
+ import { Command as Command9 } from "commander";
1689
1465
 
1690
1466
  // src/services/database.service.ts
1691
1467
  import { isFullPage } from "@notionhq/client";
@@ -1879,7 +1655,7 @@ function autoSelectColumns(schema, entries) {
1879
1655
  return selected;
1880
1656
  }
1881
1657
  function dbQueryCommand() {
1882
- return new Command10("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option(
1658
+ return new Command9("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option(
1883
1659
  "--filter <filter>",
1884
1660
  'Filter entries (repeatable): --filter "Status=Done"',
1885
1661
  collect,
@@ -1934,9 +1710,9 @@ function collect(value, previous) {
1934
1710
  }
1935
1711
 
1936
1712
  // src/commands/db/schema.ts
1937
- import { Command as Command11 } from "commander";
1713
+ import { Command as Command10 } from "commander";
1938
1714
  function dbSchemaCommand() {
1939
- return new Command11("schema").description("Show database schema (property names, types, and options)").argument("<id>", "Notion database ID or URL").option("--json", "Output raw JSON").action(
1715
+ return new Command10("schema").description("Show database schema (property names, types, and options)").argument("<id>", "Notion database ID or URL").option("--json", "Output raw JSON").action(
1940
1716
  withErrorHandling(async (id, options) => {
1941
1717
  const { token } = await resolveToken();
1942
1718
  const client = createNotionClient(token);
@@ -1959,9 +1735,179 @@ function dbSchemaCommand() {
1959
1735
  );
1960
1736
  }
1961
1737
 
1738
+ // src/commands/edit-page.ts
1739
+ import { Command as Command11 } from "commander";
1740
+ function editPageCommand() {
1741
+ const cmd = new Command11("edit-page");
1742
+ cmd.description(
1743
+ "replace a Notion page's content \u2014 full page or a targeted section"
1744
+ ).argument("<id/url>", "Notion page ID or URL").option(
1745
+ "-m, --message <markdown>",
1746
+ "new markdown content for the page body"
1747
+ ).option(
1748
+ "--range <selector>",
1749
+ 'ellipsis selector to replace only a section, e.g. "## My Section...last line"'
1750
+ ).option(
1751
+ "--allow-deleting-content",
1752
+ "allow deletion when using --range (always true for full-page replace)"
1753
+ ).action(
1754
+ withErrorHandling(async (idOrUrl, opts) => {
1755
+ const { token, source } = await resolveToken();
1756
+ reportTokenSource(source);
1757
+ const client = createNotionClient(token);
1758
+ if (opts.allowDeletingContent && !opts.range) {
1759
+ process.stderr.write(
1760
+ "Warning: --allow-deleting-content has no effect without --range (full-page replace always allows deletion).\n"
1761
+ );
1762
+ }
1763
+ let markdown = "";
1764
+ if (opts.message) {
1765
+ markdown = opts.message;
1766
+ } else if (!process.stdin.isTTY) {
1767
+ markdown = await readStdin();
1768
+ if (!markdown.trim()) {
1769
+ throw new CliError(
1770
+ ErrorCodes.INVALID_ARG,
1771
+ "No content provided (stdin was empty).",
1772
+ "Pass markdown via -m/--message or pipe non-empty content through stdin"
1773
+ );
1774
+ }
1775
+ } else {
1776
+ throw new CliError(
1777
+ ErrorCodes.INVALID_ARG,
1778
+ "No content provided.",
1779
+ "Pass markdown via -m/--message or pipe it through stdin"
1780
+ );
1781
+ }
1782
+ const pageId = parseNotionId(idOrUrl);
1783
+ const uuid = toUuid(pageId);
1784
+ try {
1785
+ if (opts.range) {
1786
+ await replaceMarkdown(client, uuid, markdown, {
1787
+ range: opts.range,
1788
+ allowDeletingContent: opts.allowDeletingContent ?? false
1789
+ });
1790
+ } else {
1791
+ await replaceMarkdown(client, uuid, markdown);
1792
+ }
1793
+ } catch (error2) {
1794
+ if (opts.range && isNotionValidationError(error2)) {
1795
+ throw new CliError(
1796
+ ErrorCodes.INVALID_ARG,
1797
+ `Selector not found: "${opts.range}". ${error2.message}`,
1798
+ SELECTOR_HINT,
1799
+ error2
1800
+ );
1801
+ }
1802
+ throw error2;
1803
+ }
1804
+ process.stdout.write("Page content replaced.\n");
1805
+ })
1806
+ );
1807
+ return cmd;
1808
+ }
1809
+
1810
+ // src/commands/init.ts
1811
+ import { confirm, input as input2, password } from "@inquirer/prompts";
1812
+ import { Command as Command12 } from "commander";
1813
+ async function runInitFlow() {
1814
+ const profileName = await input2({
1815
+ message: "Profile name:",
1816
+ default: "default"
1817
+ });
1818
+ const token = await password({
1819
+ message: "Integration token (from notion.so/profile/integrations/internal):",
1820
+ mask: "*"
1821
+ });
1822
+ stderrWrite("Validating token...");
1823
+ const { workspaceName, workspaceId } = await validateToken(token);
1824
+ stderrWrite(success(`\u2713 Connected to workspace: ${bold(workspaceName)}`));
1825
+ const config = await readGlobalConfig();
1826
+ if (config.profiles?.[profileName]) {
1827
+ const replace = await confirm({
1828
+ message: `Profile "${profileName}" already exists. Replace?`,
1829
+ default: false
1830
+ });
1831
+ if (!replace) {
1832
+ stderrWrite("Aborted.");
1833
+ return;
1834
+ }
1835
+ }
1836
+ const profiles = config.profiles ?? {};
1837
+ profiles[profileName] = {
1838
+ token,
1839
+ workspace_name: workspaceName,
1840
+ workspace_id: workspaceId
1841
+ };
1842
+ await writeGlobalConfig({
1843
+ ...config,
1844
+ profiles,
1845
+ active_profile: profileName
1846
+ });
1847
+ stderrWrite(success(`Profile "${profileName}" saved and set as active.`));
1848
+ stderrWrite(dim("Checking integration access..."));
1849
+ try {
1850
+ const notion = createNotionClient(token);
1851
+ const probe = await notion.search({ page_size: 1 });
1852
+ if (probe.results.length === 0) {
1853
+ stderrWrite("");
1854
+ stderrWrite("\u26A0\uFE0F Your integration has no pages connected.");
1855
+ stderrWrite(" To grant access, open any Notion page or database:");
1856
+ stderrWrite(" 1. Click \xB7\xB7\xB7 (three dots) in the top-right corner");
1857
+ stderrWrite(' 2. Select "Connect to"');
1858
+ stderrWrite(` 3. Choose "${workspaceName}"`);
1859
+ stderrWrite(" Then re-run any notion command to confirm access.");
1860
+ } else {
1861
+ stderrWrite(
1862
+ success(
1863
+ `\u2713 Integration has access to content in ${bold(workspaceName)}.`
1864
+ )
1865
+ );
1866
+ }
1867
+ } catch {
1868
+ stderrWrite(
1869
+ dim("(Could not verify integration access \u2014 run `notion ls` to check)")
1870
+ );
1871
+ }
1872
+ stderrWrite("");
1873
+ stderrWrite(
1874
+ dim("Write commands (comment, append, create-page) require additional")
1875
+ );
1876
+ stderrWrite(dim("capabilities in your integration settings:"));
1877
+ stderrWrite(
1878
+ dim(" notion.so/profile/integrations/internal \u2192 your integration \u2192")
1879
+ );
1880
+ stderrWrite(
1881
+ dim(
1882
+ ' Capabilities: enable "Read content", "Insert content", "Read comments", "Insert comments"'
1883
+ )
1884
+ );
1885
+ stderrWrite("");
1886
+ stderrWrite(
1887
+ dim("To post comments and create pages attributed to your user account:")
1888
+ );
1889
+ stderrWrite(dim(" notion auth login"));
1890
+ }
1891
+ function initCommand() {
1892
+ const cmd = new Command12("init");
1893
+ cmd.description("authenticate with Notion and save a profile").action(
1894
+ withErrorHandling(async () => {
1895
+ if (!process.stdin.isTTY) {
1896
+ throw new CliError(
1897
+ ErrorCodes.AUTH_NO_TOKEN,
1898
+ "Cannot run interactive init in non-TTY mode.",
1899
+ "Set NOTION_API_TOKEN environment variable or create .notion.yaml"
1900
+ );
1901
+ }
1902
+ await runInitFlow();
1903
+ })
1904
+ );
1905
+ return cmd;
1906
+ }
1907
+
1962
1908
  // src/commands/ls.ts
1963
1909
  import { isFullPageOrDataSource } from "@notionhq/client";
1964
- import { Command as Command12 } from "commander";
1910
+ import { Command as Command13 } from "commander";
1965
1911
  function getTitle(item) {
1966
1912
  if (item.object === "data_source") {
1967
1913
  return item.title.map((t) => t.plain_text).join("") || "(untitled)";
@@ -1978,7 +1924,7 @@ function displayType(item) {
1978
1924
  return item.object === "data_source" ? "database" : item.object;
1979
1925
  }
1980
1926
  function lsCommand() {
1981
- const cmd = new Command12("ls");
1927
+ const cmd = new Command13("ls");
1982
1928
  cmd.description("list accessible Notion pages and databases").option(
1983
1929
  "--type <type>",
1984
1930
  "filter by object type (page or database)",
@@ -2041,10 +1987,10 @@ function lsCommand() {
2041
1987
  // src/commands/open.ts
2042
1988
  import { exec } from "child_process";
2043
1989
  import { promisify } from "util";
2044
- import { Command as Command13 } from "commander";
1990
+ import { Command as Command14 } from "commander";
2045
1991
  var execAsync = promisify(exec);
2046
1992
  function openCommand() {
2047
- const cmd = new Command13("open");
1993
+ const cmd = new Command14("open");
2048
1994
  cmd.description("open a Notion page in the default browser").argument("<id/url>", "Notion page ID or URL").action(
2049
1995
  withErrorHandling(async (idOrUrl) => {
2050
1996
  const id = parseNotionId(idOrUrl);
@@ -2060,9 +2006,9 @@ function openCommand() {
2060
2006
  }
2061
2007
 
2062
2008
  // src/commands/profile/list.ts
2063
- import { Command as Command14 } from "commander";
2009
+ import { Command as Command15 } from "commander";
2064
2010
  function profileListCommand() {
2065
- const cmd = new Command14("list");
2011
+ const cmd = new Command15("list");
2066
2012
  cmd.description("list all authentication profiles").action(
2067
2013
  withErrorHandling(async () => {
2068
2014
  const config = await readGlobalConfig();
@@ -2091,9 +2037,9 @@ function profileListCommand() {
2091
2037
  }
2092
2038
 
2093
2039
  // src/commands/profile/remove.ts
2094
- import { Command as Command15 } from "commander";
2040
+ import { Command as Command16 } from "commander";
2095
2041
  function profileRemoveCommand() {
2096
- const cmd = new Command15("remove");
2042
+ const cmd = new Command16("remove");
2097
2043
  cmd.description("remove an authentication profile").argument("<name>", "profile name to remove").action(
2098
2044
  withErrorHandling(async (name) => {
2099
2045
  const config = await readGlobalConfig();
@@ -2119,9 +2065,9 @@ function profileRemoveCommand() {
2119
2065
  }
2120
2066
 
2121
2067
  // src/commands/profile/use.ts
2122
- import { Command as Command16 } from "commander";
2068
+ import { Command as Command17 } from "commander";
2123
2069
  function profileUseCommand() {
2124
- const cmd = new Command16("use");
2070
+ const cmd = new Command17("use");
2125
2071
  cmd.description("switch the active profile").argument("<name>", "profile name to activate").action(
2126
2072
  withErrorHandling(async (name) => {
2127
2073
  const config = await readGlobalConfig();
@@ -2144,264 +2090,7 @@ function profileUseCommand() {
2144
2090
  }
2145
2091
 
2146
2092
  // src/commands/read.ts
2147
- import { Command as Command17 } from "commander";
2148
-
2149
- // src/blocks/rich-text.ts
2150
- function richTextToMd(richText) {
2151
- return richText.map(segmentToMd).join("");
2152
- }
2153
- function segmentToMd(segment) {
2154
- if (segment.type === "equation") {
2155
- return `$${segment.equation.expression}$`;
2156
- }
2157
- if (segment.type === "mention") {
2158
- const text = segment.plain_text;
2159
- return segment.href ? `[${text}](${segment.href})` : text;
2160
- }
2161
- const annotated = applyAnnotations(segment.text.content, segment.annotations);
2162
- return segment.text.link ? `[${annotated}](${segment.text.link.url})` : annotated;
2163
- }
2164
- function applyAnnotations(text, annotations) {
2165
- let result = text;
2166
- if (annotations.code) result = `\`${result}\``;
2167
- if (annotations.strikethrough) result = `~~${result}~~`;
2168
- if (annotations.italic) result = `_${result}_`;
2169
- if (annotations.bold) result = `**${result}**`;
2170
- return result;
2171
- }
2172
-
2173
- // src/blocks/converters.ts
2174
- function indentChildren(childrenMd) {
2175
- return `${childrenMd.split("\n").filter(Boolean).map((line) => ` ${line}`).join("\n")}
2176
- `;
2177
- }
2178
- var converters = {
2179
- paragraph(block) {
2180
- const b = block;
2181
- return `${richTextToMd(b.paragraph.rich_text)}
2182
- `;
2183
- },
2184
- heading_1(block) {
2185
- const b = block;
2186
- return `# ${richTextToMd(b.heading_1.rich_text)}
2187
- `;
2188
- },
2189
- heading_2(block) {
2190
- const b = block;
2191
- return `## ${richTextToMd(b.heading_2.rich_text)}
2192
- `;
2193
- },
2194
- heading_3(block) {
2195
- const b = block;
2196
- return `### ${richTextToMd(b.heading_3.rich_text)}
2197
- `;
2198
- },
2199
- bulleted_list_item(block, ctx) {
2200
- const b = block;
2201
- const text = richTextToMd(b.bulleted_list_item.rich_text);
2202
- const header = `- ${text}
2203
- `;
2204
- if (ctx?.childrenMd) {
2205
- return header + indentChildren(ctx.childrenMd);
2206
- }
2207
- return header;
2208
- },
2209
- numbered_list_item(block, ctx) {
2210
- const b = block;
2211
- const num = ctx?.listNumber ?? 1;
2212
- return `${num}. ${richTextToMd(b.numbered_list_item.rich_text)}
2213
- `;
2214
- },
2215
- to_do(block) {
2216
- const b = block;
2217
- const checkbox = b.to_do.checked ? "[x]" : "[ ]";
2218
- return `- ${checkbox} ${richTextToMd(b.to_do.rich_text)}
2219
- `;
2220
- },
2221
- code(block) {
2222
- const b = block;
2223
- const lang = b.code.language === "plain text" ? "" : b.code.language;
2224
- const content = richTextToMd(b.code.rich_text);
2225
- return `\`\`\`${lang}
2226
- ${content}
2227
- \`\`\`
2228
- `;
2229
- },
2230
- quote(block) {
2231
- const b = block;
2232
- return `> ${richTextToMd(b.quote.rich_text)}
2233
- `;
2234
- },
2235
- divider() {
2236
- return "---\n";
2237
- },
2238
- callout(block) {
2239
- const b = block;
2240
- const text = richTextToMd(b.callout.rich_text);
2241
- const icon = b.callout.icon;
2242
- if (icon?.type === "emoji") {
2243
- return `> ${icon.emoji} ${text}
2244
- `;
2245
- }
2246
- return `> ${text}
2247
- `;
2248
- },
2249
- toggle(block, ctx) {
2250
- const b = block;
2251
- const header = `**${richTextToMd(b.toggle.rich_text)}**
2252
- `;
2253
- if (ctx?.childrenMd) {
2254
- return header + ctx.childrenMd;
2255
- }
2256
- return header;
2257
- },
2258
- image(block) {
2259
- const b = block;
2260
- const caption = richTextToMd(b.image.caption);
2261
- if (b.image.type === "file") {
2262
- const url2 = b.image.file.url;
2263
- const expiry = b.image.file.expiry_time;
2264
- return `![${caption}](${url2}) <!-- expires: ${expiry} -->
2265
- `;
2266
- }
2267
- const url = b.image.external.url;
2268
- return `![${caption}](${url})
2269
- `;
2270
- },
2271
- bookmark(block) {
2272
- const b = block;
2273
- const caption = richTextToMd(b.bookmark.caption);
2274
- const text = caption || b.bookmark.url;
2275
- return `[${text}](${b.bookmark.url})
2276
- `;
2277
- },
2278
- child_page(block) {
2279
- const b = block;
2280
- return `### ${b.child_page.title}
2281
- `;
2282
- },
2283
- child_database(block) {
2284
- const b = block;
2285
- return `### ${b.child_database.title}
2286
- `;
2287
- },
2288
- link_preview(block) {
2289
- const b = block;
2290
- return `[${b.link_preview.url}](${b.link_preview.url})
2291
- `;
2292
- }
2293
- };
2294
- function blockToMd(block, ctx) {
2295
- const converter = converters[block.type];
2296
- if (converter) {
2297
- return converter(block, ctx);
2298
- }
2299
- return `<!-- unsupported block: ${block.type} -->
2300
- `;
2301
- }
2302
-
2303
- // src/blocks/properties.ts
2304
- function formatFormula(f) {
2305
- if (f.type === "string") return f.string ?? "";
2306
- if (f.type === "number") return f.number !== null ? String(f.number) : "";
2307
- if (f.type === "boolean") return String(f.boolean);
2308
- if (f.type === "date") return f.date?.start ?? "";
2309
- return "";
2310
- }
2311
- function formatRollup(r) {
2312
- if (r.type === "number") return r.number !== null ? String(r.number) : "";
2313
- if (r.type === "date") return r.date?.start ?? "";
2314
- if (r.type === "array") return `[${r.array.length} items]`;
2315
- return "";
2316
- }
2317
- function formatUser(p) {
2318
- return "name" in p && p.name ? p.name : p.id;
2319
- }
2320
- function formatPropertyValue(_name, prop) {
2321
- switch (prop.type) {
2322
- case "title":
2323
- return prop.title.map((rt) => rt.plain_text).join("");
2324
- case "rich_text":
2325
- return prop.rich_text.map((rt) => rt.plain_text).join("");
2326
- case "number":
2327
- return prop.number !== null ? String(prop.number) : "";
2328
- case "select":
2329
- return prop.select?.name ?? "";
2330
- case "status":
2331
- return prop.status?.name ?? "";
2332
- case "multi_select":
2333
- return prop.multi_select.map((s) => s.name).join(", ");
2334
- case "date":
2335
- if (!prop.date) return "";
2336
- return prop.date.end ? `${prop.date.start} \u2192 ${prop.date.end}` : prop.date.start;
2337
- case "checkbox":
2338
- return prop.checkbox ? "true" : "false";
2339
- case "url":
2340
- return prop.url ?? "";
2341
- case "email":
2342
- return prop.email ?? "";
2343
- case "phone_number":
2344
- return prop.phone_number ?? "";
2345
- case "people":
2346
- return prop.people.map(formatUser).join(", ");
2347
- case "relation":
2348
- return prop.relation.map((r) => r.id).join(", ");
2349
- case "formula":
2350
- return formatFormula(prop.formula);
2351
- case "rollup":
2352
- return formatRollup(prop.rollup);
2353
- case "created_time":
2354
- return prop.created_time;
2355
- case "last_edited_time":
2356
- return prop.last_edited_time;
2357
- case "created_by":
2358
- return "name" in prop.created_by ? prop.created_by.name ?? prop.created_by.id : prop.created_by.id;
2359
- case "last_edited_by":
2360
- return "name" in prop.last_edited_by ? prop.last_edited_by.name ?? prop.last_edited_by.id : prop.last_edited_by.id;
2361
- case "files":
2362
- return prop.files.map((f) => f.type === "external" ? f.external.url : f.name).join(", ");
2363
- case "unique_id":
2364
- return prop.unique_id.prefix ? `${prop.unique_id.prefix}-${prop.unique_id.number}` : String(prop.unique_id.number ?? "");
2365
- default:
2366
- return "";
2367
- }
2368
- }
2369
-
2370
- // src/blocks/render.ts
2371
- function buildPropertiesHeader(page) {
2372
- const lines = ["---"];
2373
- for (const [name, prop] of Object.entries(page.properties)) {
2374
- const value = formatPropertyValue(name, prop);
2375
- if (value) {
2376
- lines.push(`${name}: ${value}`);
2377
- }
2378
- }
2379
- lines.push("---", "");
2380
- return lines.join("\n");
2381
- }
2382
- function renderBlockTree(blocks) {
2383
- const parts = [];
2384
- let listCounter = 0;
2385
- for (const node of blocks) {
2386
- if (node.block.type === "numbered_list_item") {
2387
- listCounter++;
2388
- } else {
2389
- listCounter = 0;
2390
- }
2391
- const childrenMd = node.children.length > 0 ? renderBlockTree(node.children) : "";
2392
- const md = blockToMd(node.block, {
2393
- listNumber: node.block.type === "numbered_list_item" ? listCounter : void 0,
2394
- childrenMd: childrenMd || void 0
2395
- });
2396
- parts.push(md);
2397
- }
2398
- return parts.join("");
2399
- }
2400
- function renderPageMarkdown({ page, blocks }) {
2401
- const header = buildPropertiesHeader(page);
2402
- const content = renderBlockTree(blocks);
2403
- return header + content;
2404
- }
2093
+ import { Command as Command18 } from "commander";
2405
2094
 
2406
2095
  // src/output/markdown.ts
2407
2096
  import { Chalk as Chalk2 } from "chalk";
@@ -2531,69 +2220,43 @@ function renderInline(text) {
2531
2220
  }
2532
2221
 
2533
2222
  // src/services/page.service.ts
2534
- import {
2535
- collectPaginatedAPI,
2536
- isFullBlock
2537
- } from "@notionhq/client";
2538
- var MAX_CONCURRENT_REQUESTS = 3;
2539
- async function fetchBlockTree(client, blockId, depth, maxDepth) {
2540
- if (depth >= maxDepth) return [];
2541
- const rawBlocks = await collectPaginatedAPI(client.blocks.children.list, {
2542
- block_id: blockId
2543
- });
2544
- const blocks = rawBlocks.filter(isFullBlock);
2545
- const SKIP_RECURSE = /* @__PURE__ */ new Set(["child_page", "child_database"]);
2546
- const nodes = [];
2547
- for (let i = 0; i < blocks.length; i += MAX_CONCURRENT_REQUESTS) {
2548
- const batch = blocks.slice(i, i + MAX_CONCURRENT_REQUESTS);
2549
- const batchNodes = await Promise.all(
2550
- batch.map(async (block) => {
2551
- const children = block.has_children && !SKIP_RECURSE.has(block.type) ? await fetchBlockTree(client, block.id, depth + 1, maxDepth) : [];
2552
- return { block, children };
2553
- })
2554
- );
2555
- nodes.push(...batchNodes);
2556
- }
2557
- return nodes;
2558
- }
2559
- async function fetchPageWithBlocks(client, pageId) {
2560
- const page = await client.pages.retrieve({
2561
- page_id: pageId
2562
- });
2563
- const blocks = await fetchBlockTree(client, pageId, 0, 10);
2564
- return { page, blocks };
2223
+ async function fetchPageMarkdown(client, pageId) {
2224
+ const [page, markdownResponse] = await Promise.all([
2225
+ client.pages.retrieve({ page_id: pageId }),
2226
+ client.pages.retrieveMarkdown({ page_id: pageId })
2227
+ ]);
2228
+ return { page, markdown: markdownResponse.markdown };
2565
2229
  }
2566
2230
 
2567
2231
  // src/commands/read.ts
2568
2232
  function readCommand() {
2569
- return new Command17("read").description("Read a Notion page as markdown").argument("<id>", "Notion page ID or URL").option("--json", "Output raw JSON instead of markdown").option("--md", "Output raw markdown (no terminal styling)").action(
2570
- withErrorHandling(
2571
- async (id, options) => {
2572
- const { token } = await resolveToken();
2573
- const client = createNotionClient(token);
2574
- const pageId = parseNotionId(id);
2575
- const pageWithBlocks = await fetchPageWithBlocks(client, pageId);
2576
- if (options.json) {
2577
- process.stdout.write(
2578
- `${JSON.stringify(pageWithBlocks, null, 2)}
2233
+ return new Command18("read").description("Read a Notion page as markdown").argument("<id>", "Notion page ID or URL").action(
2234
+ withErrorHandling(async (id) => {
2235
+ const { token } = await resolveToken();
2236
+ const client = createNotionClient(token);
2237
+ const pageId = parseNotionId(id);
2238
+ const pageWithMarkdown = await fetchPageMarkdown(client, pageId);
2239
+ const mode = getOutputMode();
2240
+ if (mode === "json") {
2241
+ process.stdout.write(
2242
+ `${JSON.stringify(pageWithMarkdown, null, 2)}
2579
2243
  `
2580
- );
2244
+ );
2245
+ } else {
2246
+ const { markdown } = pageWithMarkdown;
2247
+ if (mode === "md" || !isatty()) {
2248
+ process.stdout.write(markdown);
2581
2249
  } else {
2582
- const markdown = renderPageMarkdown(pageWithBlocks);
2583
- if (options.md || !isatty()) {
2584
- process.stdout.write(markdown);
2585
- } else {
2586
- process.stdout.write(renderMarkdown(markdown));
2587
- }
2250
+ process.stdout.write(renderMarkdown(markdown));
2588
2251
  }
2589
2252
  }
2590
- )
2253
+ })
2591
2254
  );
2592
2255
  }
2593
2256
 
2594
2257
  // src/commands/search.ts
2595
2258
  import { isFullPageOrDataSource as isFullPageOrDataSource2 } from "@notionhq/client";
2596
- import { Command as Command18 } from "commander";
2259
+ import { Command as Command19 } from "commander";
2597
2260
  function getTitle2(item) {
2598
2261
  if (item.object === "data_source") {
2599
2262
  return item.title.map((t) => t.plain_text).join("") || "(untitled)";
@@ -2613,7 +2276,7 @@ function displayType2(item) {
2613
2276
  return item.object === "data_source" ? "database" : item.object;
2614
2277
  }
2615
2278
  function searchCommand() {
2616
- const cmd = new Command18("search");
2279
+ const cmd = new Command19("search");
2617
2280
  cmd.description("search Notion workspace by keyword").argument("<query>", "search keyword").option(
2618
2281
  "--type <type>",
2619
2282
  "filter by object type (page or database)",
@@ -2671,7 +2334,7 @@ function searchCommand() {
2671
2334
  }
2672
2335
 
2673
2336
  // src/commands/users.ts
2674
- import { Command as Command19 } from "commander";
2337
+ import { Command as Command20 } from "commander";
2675
2338
  function getEmailOrWorkspace(user) {
2676
2339
  if (user.type === "person") {
2677
2340
  return user.person.email ?? "\u2014";
@@ -2683,7 +2346,7 @@ function getEmailOrWorkspace(user) {
2683
2346
  return "\u2014";
2684
2347
  }
2685
2348
  function usersCommand() {
2686
- const cmd = new Command19("users");
2349
+ const cmd = new Command20("users");
2687
2350
  cmd.description("list all users in the workspace").option("--json", "output as JSON").action(
2688
2351
  withErrorHandling(async (opts) => {
2689
2352
  if (opts.json) setOutputMode("json");
@@ -2714,7 +2377,7 @@ var __dirname = dirname(__filename);
2714
2377
  var pkg = JSON.parse(
2715
2378
  readFileSync(join3(__dirname, "../package.json"), "utf-8")
2716
2379
  );
2717
- var program = new Command20();
2380
+ var program = new Command21();
2718
2381
  program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
2719
2382
  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");
2720
2383
  program.configureOutput({
@@ -2735,7 +2398,7 @@ program.hook("preAction", (thisCommand) => {
2735
2398
  setOutputMode("md");
2736
2399
  }
2737
2400
  });
2738
- var authCmd = new Command20("auth").description("manage Notion authentication");
2401
+ var authCmd = new Command21("auth").description("manage Notion authentication");
2739
2402
  authCmd.action(authDefaultAction(authCmd));
2740
2403
  authCmd.addCommand(loginCommand());
2741
2404
  authCmd.addCommand(logoutCommand());
@@ -2745,7 +2408,7 @@ authCmd.addCommand(profileUseCommand());
2745
2408
  authCmd.addCommand(profileRemoveCommand());
2746
2409
  program.addCommand(authCmd);
2747
2410
  program.addCommand(initCommand(), { hidden: true });
2748
- var profileCmd = new Command20("profile").description(
2411
+ var profileCmd = new Command21("profile").description(
2749
2412
  "manage authentication profiles"
2750
2413
  );
2751
2414
  profileCmd.addCommand(profileListCommand());
@@ -2761,7 +2424,8 @@ program.addCommand(readCommand());
2761
2424
  program.addCommand(commentAddCommand());
2762
2425
  program.addCommand(appendCommand());
2763
2426
  program.addCommand(createPageCommand());
2764
- var dbCmd = new Command20("db").description("Database operations");
2427
+ program.addCommand(editPageCommand());
2428
+ var dbCmd = new Command21("db").description("Database operations");
2765
2429
  dbCmd.addCommand(dbSchemaCommand());
2766
2430
  dbCmd.addCommand(dbQueryCommand());
2767
2431
  program.addCommand(dbCmd);