@andrzejchm/notion-cli 0.4.1 → 0.5.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) {
@@ -775,13 +557,61 @@ async function addComment(client, pageId, text, options = {}) {
775
557
  ...options.asUser && { display_name: { type: "user" } }
776
558
  });
777
559
  }
778
- async function appendBlocks(client, blockId, blocks) {
779
- await client.blocks.children.append({
780
- block_id: blockId,
781
- children: blocks
560
+ async function appendMarkdown(client, pageId, markdown) {
561
+ await client.pages.updateMarkdown({
562
+ page_id: pageId,
563
+ type: "insert_content",
564
+ insert_content: { content: markdown }
565
+ });
566
+ }
567
+ function countOccurrences(text, sub) {
568
+ let count = 0;
569
+ let pos = text.indexOf(sub, 0);
570
+ while (pos !== -1) {
571
+ count++;
572
+ pos = text.indexOf(sub, pos + 1);
573
+ }
574
+ return count;
575
+ }
576
+ function buildContentRange(content) {
577
+ const START_LEN = 15;
578
+ const STEP = 10;
579
+ if (content.length <= START_LEN * 2) {
580
+ return content;
581
+ }
582
+ const start = content.slice(0, START_LEN);
583
+ for (let endLen = START_LEN; endLen < content.length - START_LEN; endLen += STEP) {
584
+ const end = content.slice(-endLen);
585
+ if (countOccurrences(content, end) === 1) {
586
+ return `${start}...${end}`;
587
+ }
588
+ }
589
+ return content;
590
+ }
591
+ async function replaceMarkdown(client, pageId, newMarkdown) {
592
+ const current = await client.pages.retrieveMarkdown({ page_id: pageId });
593
+ const currentContent = current.markdown.trim();
594
+ if (!currentContent) {
595
+ if (newMarkdown.trim()) {
596
+ await client.pages.updateMarkdown({
597
+ page_id: pageId,
598
+ type: "insert_content",
599
+ insert_content: { content: newMarkdown }
600
+ });
601
+ }
602
+ return;
603
+ }
604
+ await client.pages.updateMarkdown({
605
+ page_id: pageId,
606
+ type: "replace_content_range",
607
+ replace_content_range: {
608
+ content: newMarkdown,
609
+ content_range: buildContentRange(currentContent),
610
+ allow_deleting_content: true
611
+ }
782
612
  });
783
613
  }
784
- async function createPage(client, parentId, title, blocks) {
614
+ async function createPage(client, parentId, title, markdown) {
785
615
  const response = await client.pages.create({
786
616
  parent: { type: "page_id", page_id: parentId },
787
617
  properties: {
@@ -789,29 +619,46 @@ async function createPage(client, parentId, title, blocks) {
789
619
  title: [{ type: "text", text: { content: title, link: null } }]
790
620
  }
791
621
  },
792
- children: blocks
622
+ ...markdown.trim() ? { markdown } : {}
793
623
  });
794
624
  return response.url;
795
625
  }
796
626
 
797
627
  // src/commands/append.ts
628
+ async function readStdin() {
629
+ const chunks = [];
630
+ for await (const chunk of process.stdin) {
631
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
632
+ }
633
+ return Buffer.concat(chunks).toString("utf-8");
634
+ }
798
635
  function appendCommand() {
799
636
  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(
637
+ 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").action(
801
638
  withErrorHandling(async (idOrUrl, opts) => {
802
639
  const { token, source } = await resolveToken();
803
640
  reportTokenSource(source);
804
641
  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) {
642
+ let markdown = "";
643
+ if (opts.message) {
644
+ markdown = opts.message;
645
+ } else if (!process.stdin.isTTY) {
646
+ markdown = await readStdin();
647
+ } else {
648
+ throw new CliError(
649
+ ErrorCodes.INVALID_ARG,
650
+ "No content to append.",
651
+ "Pass markdown via -m/--message or pipe it through stdin"
652
+ );
653
+ }
654
+ if (!markdown.trim()) {
809
655
  process.stdout.write("Nothing to append.\n");
810
656
  return;
811
657
  }
812
- await appendBlocks(client, uuid, blocks);
813
- process.stdout.write(`Appended ${blocks.length} block(s).
814
- `);
658
+ const pageId = parseNotionId(idOrUrl);
659
+ const uuid = toUuid(pageId);
660
+ await appendMarkdown(client, uuid, markdown);
661
+ process.stdout.write("Appended.\n");
815
662
  })
816
663
  );
817
664
  return cmd;
@@ -825,8 +672,8 @@ function authDefaultAction(authCmd2) {
825
672
  }
826
673
 
827
674
  // src/commands/auth/login.ts
828
- import { input as input2, select } from "@inquirer/prompts";
829
- import { Command as Command3 } from "commander";
675
+ import { input } from "@inquirer/prompts";
676
+ import { Command as Command2 } from "commander";
830
677
 
831
678
  // src/oauth/oauth-flow.ts
832
679
  import { spawn } from "child_process";
@@ -1071,108 +918,10 @@ Waiting for callback (up to 120 seconds)...
1071
918
  });
1072
919
  }
1073
920
 
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
921
  // src/commands/auth/login.ts
1173
922
  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(
923
+ const cmd = new Command2("login");
924
+ cmd.description("authenticate with Notion via OAuth").option("--profile <name>", "profile name to store credentials in").option(
1176
925
  "--manual",
1177
926
  "print auth URL instead of opening browser (for headless OAuth)"
1178
927
  ).action(
@@ -1184,76 +933,57 @@ function loginCommand() {
1184
933
  "Use --manual flag to get an auth URL you can open in a browser"
1185
934
  );
1186
935
  }
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
- }
936
+ const result = await runOAuthFlow({ manual: opts.manual });
937
+ const response = await exchangeCode(result.code);
938
+ const userName = response.owner?.user?.name ?? "unknown user";
939
+ const workspaceName = response.workspace_name ?? "unknown workspace";
940
+ const config = await readGlobalConfig();
941
+ const existingProfiles = config.profiles ?? {};
942
+ let profileName = opts.profile;
943
+ if (!profileName) {
944
+ const suggested = workspaceName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "default";
945
+ profileName = await input({
946
+ message: "Profile name to save this account under:",
947
+ default: suggested
948
+ });
949
+ }
950
+ const isUpdate = Boolean(existingProfiles[profileName]);
951
+ const isFirst = Object.keys(existingProfiles).length === 0;
952
+ if (isUpdate) {
953
+ stderrWrite(dim(`Updating existing profile "${profileName}"...`));
954
+ }
955
+ await saveOAuthTokens(profileName, response);
956
+ if (isFirst) {
957
+ const updated = await readGlobalConfig();
958
+ await writeGlobalConfig({
959
+ ...updated,
960
+ active_profile: profileName
961
+ });
962
+ }
963
+ stderrWrite(
964
+ success(`\u2713 Logged in as ${userName} to workspace ${workspaceName}`)
965
+ );
966
+ stderrWrite(dim(`Saved as profile "${profileName}".`));
967
+ if (!isFirst && !isUpdate) {
1241
968
  stderrWrite(
1242
969
  dim(
1243
- "Your comments and pages will now be attributed to your Notion account."
970
+ `Run "notion auth use ${profileName}" to switch to this profile.`
1244
971
  )
1245
972
  );
1246
- } else {
1247
- await runInitFlow();
1248
973
  }
974
+ stderrWrite(
975
+ dim(
976
+ "Your comments and pages will now be attributed to your Notion account."
977
+ )
978
+ );
1249
979
  })
1250
980
  );
1251
981
  return cmd;
1252
982
  }
1253
983
 
1254
984
  // src/commands/auth/logout.ts
1255
- import { select as select2 } from "@inquirer/prompts";
1256
- import { Command as Command4 } from "commander";
985
+ import { select } from "@inquirer/prompts";
986
+ import { Command as Command3 } from "commander";
1257
987
  function profileLabel(name, profile) {
1258
988
  const parts = [];
1259
989
  if (profile.oauth_access_token)
@@ -1266,7 +996,7 @@ function profileLabel(name, profile) {
1266
996
  return `${bold(name)} ${dim(authDesc)}${workspace}`;
1267
997
  }
1268
998
  function logoutCommand() {
1269
- const cmd = new Command4("logout");
999
+ const cmd = new Command3("logout");
1270
1000
  cmd.description("remove a profile and its credentials").option(
1271
1001
  "--profile <name>",
1272
1002
  "profile name to remove (skips interactive selector)"
@@ -1288,7 +1018,7 @@ function logoutCommand() {
1288
1018
  "Use --profile <name> to specify the profile to remove"
1289
1019
  );
1290
1020
  }
1291
- profileName = await select2({
1021
+ profileName = await select({
1292
1022
  message: "Which profile do you want to log out of?",
1293
1023
  choices: profileNames.map((name) => ({
1294
1024
  // biome-ignore lint/style/noNonNullAssertion: key is from Object.keys, always present
@@ -1324,9 +1054,9 @@ function logoutCommand() {
1324
1054
  }
1325
1055
 
1326
1056
  // src/commands/auth/status.ts
1327
- import { Command as Command5 } from "commander";
1057
+ import { Command as Command4 } from "commander";
1328
1058
  function statusCommand() {
1329
- const cmd = new Command5("status");
1059
+ const cmd = new Command4("status");
1330
1060
  cmd.description("show authentication status for the active profile").option("--profile <name>", "profile name to check").action(
1331
1061
  withErrorHandling(async (opts) => {
1332
1062
  let profileName = opts.profile;
@@ -1384,9 +1114,9 @@ function statusCommand() {
1384
1114
  }
1385
1115
 
1386
1116
  // src/commands/comment-add.ts
1387
- import { Command as Command6 } from "commander";
1117
+ import { Command as Command5 } from "commander";
1388
1118
  function commentAddCommand() {
1389
- const cmd = new Command6("comment");
1119
+ const cmd = new Command5("comment");
1390
1120
  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
1121
  withErrorHandling(async (idOrUrl, opts) => {
1392
1122
  const { token, source } = await resolveToken();
@@ -1404,7 +1134,7 @@ function commentAddCommand() {
1404
1134
  }
1405
1135
 
1406
1136
  // src/commands/comments.ts
1407
- import { Command as Command7 } from "commander";
1137
+ import { Command as Command6 } from "commander";
1408
1138
 
1409
1139
  // src/output/format.ts
1410
1140
  var _mode = "auto";
@@ -1489,7 +1219,7 @@ async function paginateResults(fetcher) {
1489
1219
 
1490
1220
  // src/commands/comments.ts
1491
1221
  function commentsCommand() {
1492
- const cmd = new Command7("comments");
1222
+ const cmd = new Command6("comments");
1493
1223
  cmd.description("list comments on a Notion page").argument("<id/url>", "Notion page ID or URL").option("--json", "output as JSON").action(
1494
1224
  withErrorHandling(async (idOrUrl, opts) => {
1495
1225
  if (opts.json) setOutputMode("json");
@@ -1520,7 +1250,7 @@ function commentsCommand() {
1520
1250
  }
1521
1251
 
1522
1252
  // src/commands/completion.ts
1523
- import { Command as Command8 } from "commander";
1253
+ import { Command as Command7 } from "commander";
1524
1254
  var BASH_COMPLETION = `# notion bash completion
1525
1255
  _notion_completion() {
1526
1256
  local cur prev words cword
@@ -1622,7 +1352,7 @@ complete -c notion -n '__fish_seen_subcommand_from completion' -a zsh -d 'zsh co
1622
1352
  complete -c notion -n '__fish_seen_subcommand_from completion' -a fish -d 'fish completion script'
1623
1353
  `;
1624
1354
  function completionCommand() {
1625
- const cmd = new Command8("completion");
1355
+ const cmd = new Command7("completion");
1626
1356
  cmd.description("output shell completion script").argument("<shell>", "shell type (bash, zsh, fish)").action(
1627
1357
  withErrorHandling(async (shell) => {
1628
1358
  switch (shell) {
@@ -1648,8 +1378,8 @@ function completionCommand() {
1648
1378
  }
1649
1379
 
1650
1380
  // src/commands/create-page.ts
1651
- import { Command as Command9 } from "commander";
1652
- async function readStdin() {
1381
+ import { Command as Command8 } from "commander";
1382
+ async function readStdin2() {
1653
1383
  const chunks = [];
1654
1384
  for await (const chunk of process.stdin) {
1655
1385
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
@@ -1657,7 +1387,7 @@ async function readStdin() {
1657
1387
  return Buffer.concat(chunks).toString("utf-8");
1658
1388
  }
1659
1389
  function createPageCommand() {
1660
- const cmd = new Command9("create-page");
1390
+ const cmd = new Command8("create-page");
1661
1391
  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
1392
  "-m, --message <markdown>",
1663
1393
  "inline markdown content for the page body"
@@ -1671,11 +1401,15 @@ function createPageCommand() {
1671
1401
  if (opts.message) {
1672
1402
  markdown = opts.message;
1673
1403
  } else if (!process.stdin.isTTY) {
1674
- markdown = await readStdin();
1404
+ markdown = await readStdin2();
1675
1405
  }
1676
- const blocks = mdToBlocks(markdown);
1677
1406
  const parentUuid = toUuid(parseNotionId(opts.parent));
1678
- const url = await createPage(client, parentUuid, opts.title, blocks);
1407
+ const url = await createPage(
1408
+ client,
1409
+ parentUuid,
1410
+ opts.title,
1411
+ markdown
1412
+ );
1679
1413
  process.stdout.write(`${url}
1680
1414
  `);
1681
1415
  }
@@ -1685,7 +1419,7 @@ function createPageCommand() {
1685
1419
  }
1686
1420
 
1687
1421
  // src/commands/db/query.ts
1688
- import { Command as Command10 } from "commander";
1422
+ import { Command as Command9 } from "commander";
1689
1423
 
1690
1424
  // src/services/database.service.ts
1691
1425
  import { isFullPage } from "@notionhq/client";
@@ -1879,7 +1613,7 @@ function autoSelectColumns(schema, entries) {
1879
1613
  return selected;
1880
1614
  }
1881
1615
  function dbQueryCommand() {
1882
- return new Command10("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option(
1616
+ return new Command9("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option(
1883
1617
  "--filter <filter>",
1884
1618
  'Filter entries (repeatable): --filter "Status=Done"',
1885
1619
  collect,
@@ -1934,9 +1668,9 @@ function collect(value, previous) {
1934
1668
  }
1935
1669
 
1936
1670
  // src/commands/db/schema.ts
1937
- import { Command as Command11 } from "commander";
1671
+ import { Command as Command10 } from "commander";
1938
1672
  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(
1673
+ 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
1674
  withErrorHandling(async (id, options) => {
1941
1675
  const { token } = await resolveToken();
1942
1676
  const client = createNotionClient(token);
@@ -1959,9 +1693,156 @@ function dbSchemaCommand() {
1959
1693
  );
1960
1694
  }
1961
1695
 
1696
+ // src/commands/edit-page.ts
1697
+ import { Command as Command11 } from "commander";
1698
+ async function readStdin3() {
1699
+ const chunks = [];
1700
+ for await (const chunk of process.stdin) {
1701
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1702
+ }
1703
+ return Buffer.concat(chunks).toString("utf-8");
1704
+ }
1705
+ function editPageCommand() {
1706
+ const cmd = new Command11("edit-page");
1707
+ cmd.description(
1708
+ "replace the entire content of a Notion page with new markdown"
1709
+ ).argument("<id/url>", "Notion page ID or URL").option(
1710
+ "-m, --message <markdown>",
1711
+ "new markdown content for the page body"
1712
+ ).action(
1713
+ withErrorHandling(async (idOrUrl, opts) => {
1714
+ const { token, source } = await resolveToken();
1715
+ reportTokenSource(source);
1716
+ const client = createNotionClient(token);
1717
+ let markdown = "";
1718
+ if (opts.message) {
1719
+ markdown = opts.message;
1720
+ } else if (!process.stdin.isTTY) {
1721
+ markdown = await readStdin3();
1722
+ if (!markdown.trim()) {
1723
+ throw new CliError(
1724
+ ErrorCodes.INVALID_ARG,
1725
+ "No content provided (stdin was empty).",
1726
+ "Pass markdown via -m/--message or pipe non-empty content through stdin"
1727
+ );
1728
+ }
1729
+ } else {
1730
+ throw new CliError(
1731
+ ErrorCodes.INVALID_ARG,
1732
+ "No content provided.",
1733
+ "Pass markdown via -m/--message or pipe it through stdin"
1734
+ );
1735
+ }
1736
+ const pageId = parseNotionId(idOrUrl);
1737
+ const uuid = toUuid(pageId);
1738
+ await replaceMarkdown(client, uuid, markdown);
1739
+ process.stdout.write("Page content replaced.\n");
1740
+ })
1741
+ );
1742
+ return cmd;
1743
+ }
1744
+
1745
+ // src/commands/init.ts
1746
+ import { confirm, input as input2, password } from "@inquirer/prompts";
1747
+ import { Command as Command12 } from "commander";
1748
+ async function runInitFlow() {
1749
+ const profileName = await input2({
1750
+ message: "Profile name:",
1751
+ default: "default"
1752
+ });
1753
+ const token = await password({
1754
+ message: "Integration token (from notion.so/profile/integrations/internal):",
1755
+ mask: "*"
1756
+ });
1757
+ stderrWrite("Validating token...");
1758
+ const { workspaceName, workspaceId } = await validateToken(token);
1759
+ stderrWrite(success(`\u2713 Connected to workspace: ${bold(workspaceName)}`));
1760
+ const config = await readGlobalConfig();
1761
+ if (config.profiles?.[profileName]) {
1762
+ const replace = await confirm({
1763
+ message: `Profile "${profileName}" already exists. Replace?`,
1764
+ default: false
1765
+ });
1766
+ if (!replace) {
1767
+ stderrWrite("Aborted.");
1768
+ return;
1769
+ }
1770
+ }
1771
+ const profiles = config.profiles ?? {};
1772
+ profiles[profileName] = {
1773
+ token,
1774
+ workspace_name: workspaceName,
1775
+ workspace_id: workspaceId
1776
+ };
1777
+ await writeGlobalConfig({
1778
+ ...config,
1779
+ profiles,
1780
+ active_profile: profileName
1781
+ });
1782
+ stderrWrite(success(`Profile "${profileName}" saved and set as active.`));
1783
+ stderrWrite(dim("Checking integration access..."));
1784
+ try {
1785
+ const notion = createNotionClient(token);
1786
+ const probe = await notion.search({ page_size: 1 });
1787
+ if (probe.results.length === 0) {
1788
+ stderrWrite("");
1789
+ stderrWrite("\u26A0\uFE0F Your integration has no pages connected.");
1790
+ stderrWrite(" To grant access, open any Notion page or database:");
1791
+ stderrWrite(" 1. Click \xB7\xB7\xB7 (three dots) in the top-right corner");
1792
+ stderrWrite(' 2. Select "Connect to"');
1793
+ stderrWrite(` 3. Choose "${workspaceName}"`);
1794
+ stderrWrite(" Then re-run any notion command to confirm access.");
1795
+ } else {
1796
+ stderrWrite(
1797
+ success(
1798
+ `\u2713 Integration has access to content in ${bold(workspaceName)}.`
1799
+ )
1800
+ );
1801
+ }
1802
+ } catch {
1803
+ stderrWrite(
1804
+ dim("(Could not verify integration access \u2014 run `notion ls` to check)")
1805
+ );
1806
+ }
1807
+ stderrWrite("");
1808
+ stderrWrite(
1809
+ dim("Write commands (comment, append, create-page) require additional")
1810
+ );
1811
+ stderrWrite(dim("capabilities in your integration settings:"));
1812
+ stderrWrite(
1813
+ dim(" notion.so/profile/integrations/internal \u2192 your integration \u2192")
1814
+ );
1815
+ stderrWrite(
1816
+ dim(
1817
+ ' Capabilities: enable "Read content", "Insert content", "Read comments", "Insert comments"'
1818
+ )
1819
+ );
1820
+ stderrWrite("");
1821
+ stderrWrite(
1822
+ dim("To post comments and create pages attributed to your user account:")
1823
+ );
1824
+ stderrWrite(dim(" notion auth login"));
1825
+ }
1826
+ function initCommand() {
1827
+ const cmd = new Command12("init");
1828
+ cmd.description("authenticate with Notion and save a profile").action(
1829
+ withErrorHandling(async () => {
1830
+ if (!process.stdin.isTTY) {
1831
+ throw new CliError(
1832
+ ErrorCodes.AUTH_NO_TOKEN,
1833
+ "Cannot run interactive init in non-TTY mode.",
1834
+ "Set NOTION_API_TOKEN environment variable or create .notion.yaml"
1835
+ );
1836
+ }
1837
+ await runInitFlow();
1838
+ })
1839
+ );
1840
+ return cmd;
1841
+ }
1842
+
1962
1843
  // src/commands/ls.ts
1963
1844
  import { isFullPageOrDataSource } from "@notionhq/client";
1964
- import { Command as Command12 } from "commander";
1845
+ import { Command as Command13 } from "commander";
1965
1846
  function getTitle(item) {
1966
1847
  if (item.object === "data_source") {
1967
1848
  return item.title.map((t) => t.plain_text).join("") || "(untitled)";
@@ -1978,7 +1859,7 @@ function displayType(item) {
1978
1859
  return item.object === "data_source" ? "database" : item.object;
1979
1860
  }
1980
1861
  function lsCommand() {
1981
- const cmd = new Command12("ls");
1862
+ const cmd = new Command13("ls");
1982
1863
  cmd.description("list accessible Notion pages and databases").option(
1983
1864
  "--type <type>",
1984
1865
  "filter by object type (page or database)",
@@ -2041,10 +1922,10 @@ function lsCommand() {
2041
1922
  // src/commands/open.ts
2042
1923
  import { exec } from "child_process";
2043
1924
  import { promisify } from "util";
2044
- import { Command as Command13 } from "commander";
1925
+ import { Command as Command14 } from "commander";
2045
1926
  var execAsync = promisify(exec);
2046
1927
  function openCommand() {
2047
- const cmd = new Command13("open");
1928
+ const cmd = new Command14("open");
2048
1929
  cmd.description("open a Notion page in the default browser").argument("<id/url>", "Notion page ID or URL").action(
2049
1930
  withErrorHandling(async (idOrUrl) => {
2050
1931
  const id = parseNotionId(idOrUrl);
@@ -2060,9 +1941,9 @@ function openCommand() {
2060
1941
  }
2061
1942
 
2062
1943
  // src/commands/profile/list.ts
2063
- import { Command as Command14 } from "commander";
1944
+ import { Command as Command15 } from "commander";
2064
1945
  function profileListCommand() {
2065
- const cmd = new Command14("list");
1946
+ const cmd = new Command15("list");
2066
1947
  cmd.description("list all authentication profiles").action(
2067
1948
  withErrorHandling(async () => {
2068
1949
  const config = await readGlobalConfig();
@@ -2091,9 +1972,9 @@ function profileListCommand() {
2091
1972
  }
2092
1973
 
2093
1974
  // src/commands/profile/remove.ts
2094
- import { Command as Command15 } from "commander";
1975
+ import { Command as Command16 } from "commander";
2095
1976
  function profileRemoveCommand() {
2096
- const cmd = new Command15("remove");
1977
+ const cmd = new Command16("remove");
2097
1978
  cmd.description("remove an authentication profile").argument("<name>", "profile name to remove").action(
2098
1979
  withErrorHandling(async (name) => {
2099
1980
  const config = await readGlobalConfig();
@@ -2119,9 +2000,9 @@ function profileRemoveCommand() {
2119
2000
  }
2120
2001
 
2121
2002
  // src/commands/profile/use.ts
2122
- import { Command as Command16 } from "commander";
2003
+ import { Command as Command17 } from "commander";
2123
2004
  function profileUseCommand() {
2124
- const cmd = new Command16("use");
2005
+ const cmd = new Command17("use");
2125
2006
  cmd.description("switch the active profile").argument("<name>", "profile name to activate").action(
2126
2007
  withErrorHandling(async (name) => {
2127
2008
  const config = await readGlobalConfig();
@@ -2144,264 +2025,7 @@ function profileUseCommand() {
2144
2025
  }
2145
2026
 
2146
2027
  // 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
- }
2028
+ import { Command as Command18 } from "commander";
2405
2029
 
2406
2030
  // src/output/markdown.ts
2407
2031
  import { Chalk as Chalk2 } from "chalk";
@@ -2531,69 +2155,43 @@ function renderInline(text) {
2531
2155
  }
2532
2156
 
2533
2157
  // 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 };
2158
+ async function fetchPageMarkdown(client, pageId) {
2159
+ const [page, markdownResponse] = await Promise.all([
2160
+ client.pages.retrieve({ page_id: pageId }),
2161
+ client.pages.retrieveMarkdown({ page_id: pageId })
2162
+ ]);
2163
+ return { page, markdown: markdownResponse.markdown };
2565
2164
  }
2566
2165
 
2567
2166
  // src/commands/read.ts
2568
2167
  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)}
2168
+ return new Command18("read").description("Read a Notion page as markdown").argument("<id>", "Notion page ID or URL").action(
2169
+ withErrorHandling(async (id) => {
2170
+ const { token } = await resolveToken();
2171
+ const client = createNotionClient(token);
2172
+ const pageId = parseNotionId(id);
2173
+ const pageWithMarkdown = await fetchPageMarkdown(client, pageId);
2174
+ const mode = getOutputMode();
2175
+ if (mode === "json") {
2176
+ process.stdout.write(
2177
+ `${JSON.stringify(pageWithMarkdown, null, 2)}
2579
2178
  `
2580
- );
2179
+ );
2180
+ } else {
2181
+ const { markdown } = pageWithMarkdown;
2182
+ if (mode === "md" || !isatty()) {
2183
+ process.stdout.write(markdown);
2581
2184
  } 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
- }
2185
+ process.stdout.write(renderMarkdown(markdown));
2588
2186
  }
2589
2187
  }
2590
- )
2188
+ })
2591
2189
  );
2592
2190
  }
2593
2191
 
2594
2192
  // src/commands/search.ts
2595
2193
  import { isFullPageOrDataSource as isFullPageOrDataSource2 } from "@notionhq/client";
2596
- import { Command as Command18 } from "commander";
2194
+ import { Command as Command19 } from "commander";
2597
2195
  function getTitle2(item) {
2598
2196
  if (item.object === "data_source") {
2599
2197
  return item.title.map((t) => t.plain_text).join("") || "(untitled)";
@@ -2613,7 +2211,7 @@ function displayType2(item) {
2613
2211
  return item.object === "data_source" ? "database" : item.object;
2614
2212
  }
2615
2213
  function searchCommand() {
2616
- const cmd = new Command18("search");
2214
+ const cmd = new Command19("search");
2617
2215
  cmd.description("search Notion workspace by keyword").argument("<query>", "search keyword").option(
2618
2216
  "--type <type>",
2619
2217
  "filter by object type (page or database)",
@@ -2671,7 +2269,7 @@ function searchCommand() {
2671
2269
  }
2672
2270
 
2673
2271
  // src/commands/users.ts
2674
- import { Command as Command19 } from "commander";
2272
+ import { Command as Command20 } from "commander";
2675
2273
  function getEmailOrWorkspace(user) {
2676
2274
  if (user.type === "person") {
2677
2275
  return user.person.email ?? "\u2014";
@@ -2683,7 +2281,7 @@ function getEmailOrWorkspace(user) {
2683
2281
  return "\u2014";
2684
2282
  }
2685
2283
  function usersCommand() {
2686
- const cmd = new Command19("users");
2284
+ const cmd = new Command20("users");
2687
2285
  cmd.description("list all users in the workspace").option("--json", "output as JSON").action(
2688
2286
  withErrorHandling(async (opts) => {
2689
2287
  if (opts.json) setOutputMode("json");
@@ -2714,7 +2312,7 @@ var __dirname = dirname(__filename);
2714
2312
  var pkg = JSON.parse(
2715
2313
  readFileSync(join3(__dirname, "../package.json"), "utf-8")
2716
2314
  );
2717
- var program = new Command20();
2315
+ var program = new Command21();
2718
2316
  program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
2719
2317
  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
2318
  program.configureOutput({
@@ -2735,7 +2333,7 @@ program.hook("preAction", (thisCommand) => {
2735
2333
  setOutputMode("md");
2736
2334
  }
2737
2335
  });
2738
- var authCmd = new Command20("auth").description("manage Notion authentication");
2336
+ var authCmd = new Command21("auth").description("manage Notion authentication");
2739
2337
  authCmd.action(authDefaultAction(authCmd));
2740
2338
  authCmd.addCommand(loginCommand());
2741
2339
  authCmd.addCommand(logoutCommand());
@@ -2745,7 +2343,7 @@ authCmd.addCommand(profileUseCommand());
2745
2343
  authCmd.addCommand(profileRemoveCommand());
2746
2344
  program.addCommand(authCmd);
2747
2345
  program.addCommand(initCommand(), { hidden: true });
2748
- var profileCmd = new Command20("profile").description(
2346
+ var profileCmd = new Command21("profile").description(
2749
2347
  "manage authentication profiles"
2750
2348
  );
2751
2349
  profileCmd.addCommand(profileListCommand());
@@ -2761,7 +2359,8 @@ program.addCommand(readCommand());
2761
2359
  program.addCommand(commentAddCommand());
2762
2360
  program.addCommand(appendCommand());
2763
2361
  program.addCommand(createPageCommand());
2764
- var dbCmd = new Command20("db").description("Database operations");
2362
+ program.addCommand(editPageCommand());
2363
+ var dbCmd = new Command21("db").description("Database operations");
2765
2364
  dbCmd.addCommand(dbSchemaCommand());
2766
2365
  dbCmd.addCommand(dbQueryCommand());
2767
2366
  program.addCommand(dbCmd);