@andrzejchm/notion-cli 0.1.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 ADDED
@@ -0,0 +1,1605 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command as Command14 } from "commander";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname, join as join3 } from "path";
7
+ import { readFileSync } from "fs";
8
+
9
+ // src/output/color.ts
10
+ import { Chalk } from "chalk";
11
+ var _colorForced = false;
12
+ function setColorForced(forced) {
13
+ _colorForced = forced;
14
+ }
15
+ function isColorEnabled() {
16
+ if (process.env.NO_COLOR) return false;
17
+ if (_colorForced) return true;
18
+ return Boolean(process.stderr.isTTY);
19
+ }
20
+ function createChalk() {
21
+ const level = isColorEnabled() ? void 0 : 0;
22
+ return new Chalk({ level });
23
+ }
24
+ function success(msg) {
25
+ return createChalk().green(msg);
26
+ }
27
+ function dim(msg) {
28
+ return createChalk().dim(msg);
29
+ }
30
+ function bold(msg) {
31
+ return createChalk().bold(msg);
32
+ }
33
+
34
+ // src/output/format.ts
35
+ import { spawnSync } from "child_process";
36
+ var _mode = "auto";
37
+ function setOutputMode(mode) {
38
+ _mode = mode;
39
+ }
40
+ function getOutputMode() {
41
+ return _mode;
42
+ }
43
+ function isatty() {
44
+ return Boolean(process.stdout.isTTY);
45
+ }
46
+ function isHumanMode() {
47
+ if (_mode === "json") return false;
48
+ if (_mode === "md") return false;
49
+ return isatty();
50
+ }
51
+ function formatJSON(data) {
52
+ return JSON.stringify(data, null, 2);
53
+ }
54
+ var COLUMN_CAPS = {
55
+ TITLE: 50,
56
+ ID: 36
57
+ };
58
+ var DEFAULT_MAX_COL_WIDTH = 40;
59
+ function getColumnCap(header) {
60
+ return COLUMN_CAPS[header.toUpperCase()] ?? DEFAULT_MAX_COL_WIDTH;
61
+ }
62
+ function truncate(str, maxLen) {
63
+ if (str.length <= maxLen) return str;
64
+ return str.slice(0, maxLen - 1) + "\u2026";
65
+ }
66
+ function formatTable(rows, headers) {
67
+ const colWidths = headers.map((header, colIdx) => {
68
+ const cap = getColumnCap(header);
69
+ const headerLen = header.length;
70
+ const maxRowLen = rows.reduce((max, row) => {
71
+ const cell = row[colIdx] ?? "";
72
+ return Math.max(max, cell.length);
73
+ }, 0);
74
+ return Math.min(Math.max(headerLen, maxRowLen), cap);
75
+ });
76
+ const sep = "\u2500";
77
+ const colSep = " ";
78
+ const headerRow = headers.map((h, i) => h.padEnd(colWidths[i])).join(colSep);
79
+ const separatorRow = colWidths.map((w) => sep.repeat(w)).join(colSep);
80
+ const dataRows = rows.map(
81
+ (row) => headers.map((_, i) => {
82
+ const cell = row[i] ?? "";
83
+ return truncate(cell, colWidths[i]).padEnd(colWidths[i]);
84
+ }).join(colSep)
85
+ );
86
+ return [headerRow, separatorRow, ...dataRows].join("\n");
87
+ }
88
+ function printOutput(data, tableHeaders, tableRows) {
89
+ const mode = getOutputMode();
90
+ const tty = isatty();
91
+ if (mode === "json" || !tty && mode === "auto") {
92
+ process.stdout.write(formatJSON(data) + "\n");
93
+ } else if (isHumanMode() && tableHeaders && tableRows) {
94
+ printWithPager(formatTable(tableRows, tableHeaders) + "\n");
95
+ }
96
+ }
97
+ function printWithPager(text) {
98
+ if (!isatty()) {
99
+ process.stdout.write(text);
100
+ return;
101
+ }
102
+ const result = spawnSync("less", ["-IR"], {
103
+ input: text,
104
+ stdio: ["pipe", "inherit", "inherit"]
105
+ });
106
+ if (result.error) {
107
+ process.stdout.write(text);
108
+ }
109
+ }
110
+
111
+ // src/commands/init.ts
112
+ import { Command } from "commander";
113
+ import { input, password, confirm } from "@inquirer/prompts";
114
+
115
+ // src/errors/cli-error.ts
116
+ var CliError = class extends Error {
117
+ constructor(code, message, suggestion) {
118
+ super(message);
119
+ this.code = code;
120
+ this.suggestion = suggestion;
121
+ this.name = "CliError";
122
+ }
123
+ format() {
124
+ let output = `[${this.code}] ${this.message}`;
125
+ if (this.suggestion) {
126
+ output += `
127
+ \u2192 ${this.suggestion}`;
128
+ }
129
+ return output;
130
+ }
131
+ };
132
+
133
+ // src/errors/codes.ts
134
+ var ErrorCodes = {
135
+ // Auth errors
136
+ AUTH_NO_TOKEN: "AUTH_NO_TOKEN",
137
+ AUTH_INVALID: "AUTH_INVALID",
138
+ AUTH_EXPIRED: "AUTH_EXPIRED",
139
+ AUTH_PROFILE_NOT_FOUND: "AUTH_PROFILE_NOT_FOUND",
140
+ // Config errors
141
+ CONFIG_READ_ERROR: "CONFIG_READ_ERROR",
142
+ CONFIG_WRITE_ERROR: "CONFIG_WRITE_ERROR",
143
+ CONFIG_INVALID: "CONFIG_INVALID",
144
+ // Input errors
145
+ INVALID_ID: "INVALID_ID",
146
+ INVALID_URL: "INVALID_URL",
147
+ INVALID_ARG: "INVALID_ARG",
148
+ // API errors
149
+ API_ERROR: "API_ERROR",
150
+ API_RATE_LIMITED: "API_RATE_LIMITED",
151
+ API_NOT_FOUND: "API_NOT_FOUND",
152
+ // General
153
+ UNKNOWN: "UNKNOWN"
154
+ };
155
+
156
+ // src/config/config.ts
157
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
158
+ import { parse, stringify } from "yaml";
159
+
160
+ // src/config/paths.ts
161
+ import { homedir } from "os";
162
+ import { join } from "path";
163
+ function getConfigDir() {
164
+ const xdgConfigHome = process.env["XDG_CONFIG_HOME"];
165
+ const base = xdgConfigHome ? xdgConfigHome : join(homedir(), ".config");
166
+ return join(base, "notion-cli");
167
+ }
168
+ function getConfigPath() {
169
+ return join(getConfigDir(), "config.yaml");
170
+ }
171
+
172
+ // src/config/config.ts
173
+ async function readGlobalConfig() {
174
+ const configPath = getConfigPath();
175
+ let raw;
176
+ try {
177
+ raw = await readFile(configPath, "utf-8");
178
+ } catch (err) {
179
+ if (err.code === "ENOENT") {
180
+ return {};
181
+ }
182
+ throw new CliError(
183
+ ErrorCodes.CONFIG_READ_ERROR,
184
+ `Failed to read config file: ${configPath}`,
185
+ 'Check file permissions or run "notion init" to create a new config'
186
+ );
187
+ }
188
+ try {
189
+ const parsed = parse(raw);
190
+ return parsed ?? {};
191
+ } catch {
192
+ throw new CliError(
193
+ ErrorCodes.CONFIG_READ_ERROR,
194
+ `Failed to parse config file: ${configPath}`,
195
+ 'The config file may be corrupted. Delete it and run "notion init" to start fresh'
196
+ );
197
+ }
198
+ }
199
+ async function writeGlobalConfig(config) {
200
+ const configDir = getConfigDir();
201
+ const configPath = getConfigPath();
202
+ const tmpPath = `${configPath}.tmp`;
203
+ try {
204
+ await mkdir(configDir, { recursive: true, mode: 448 });
205
+ } catch {
206
+ throw new CliError(
207
+ ErrorCodes.CONFIG_WRITE_ERROR,
208
+ `Failed to create config directory: ${configDir}`,
209
+ "Check that you have write permissions to your home directory"
210
+ );
211
+ }
212
+ const content = stringify(config);
213
+ try {
214
+ await writeFile(tmpPath, content, { mode: 384 });
215
+ await rename(tmpPath, configPath);
216
+ } catch {
217
+ throw new CliError(
218
+ ErrorCodes.CONFIG_WRITE_ERROR,
219
+ `Failed to write config file: ${configPath}`,
220
+ "Check file permissions in the config directory"
221
+ );
222
+ }
223
+ }
224
+
225
+ // src/notion/client.ts
226
+ import { Client, APIErrorCode, isNotionClientError } from "@notionhq/client";
227
+ async function validateToken(token) {
228
+ const notion = new Client({ auth: token });
229
+ try {
230
+ const me = await notion.users.me({});
231
+ const bot = me;
232
+ const workspaceName = bot.bot?.workspace_name ?? "Unknown Workspace";
233
+ const workspaceId = bot.bot?.workspace_id ?? "";
234
+ return { workspaceName, workspaceId };
235
+ } catch (error2) {
236
+ if (isNotionClientError(error2) && error2.code === APIErrorCode.Unauthorized) {
237
+ throw new CliError(
238
+ ErrorCodes.AUTH_INVALID,
239
+ "Invalid integration token.",
240
+ "Check your token at notion.so/profile/integrations/internal"
241
+ );
242
+ }
243
+ throw error2;
244
+ }
245
+ }
246
+ function createNotionClient(token) {
247
+ return new Client({ auth: token, timeoutMs: 12e4 });
248
+ }
249
+
250
+ // src/output/stderr.ts
251
+ function stderrWrite(msg) {
252
+ process.stderr.write(msg + "\n");
253
+ }
254
+ function reportTokenSource(source) {
255
+ stderrWrite(dim(`Using token from ${source}`));
256
+ }
257
+
258
+ // src/errors/error-handler.ts
259
+ function mapNotionErrorCode(code) {
260
+ switch (code) {
261
+ case "unauthorized":
262
+ return ErrorCodes.AUTH_INVALID;
263
+ case "rate_limited":
264
+ return ErrorCodes.API_RATE_LIMITED;
265
+ case "object_not_found":
266
+ return ErrorCodes.API_NOT_FOUND;
267
+ default:
268
+ return ErrorCodes.API_ERROR;
269
+ }
270
+ }
271
+ function withErrorHandling(fn) {
272
+ return (async (...args) => {
273
+ try {
274
+ await fn(...args);
275
+ } catch (error2) {
276
+ if (error2 instanceof CliError) {
277
+ process.stderr.write(error2.format() + "\n");
278
+ process.exit(1);
279
+ }
280
+ const { isNotionClientError: isNotionClientError2 } = await import("@notionhq/client");
281
+ if (isNotionClientError2(error2)) {
282
+ const code = mapNotionErrorCode(error2.code);
283
+ const mappedError = new CliError(
284
+ code,
285
+ error2.message,
286
+ code === ErrorCodes.AUTH_INVALID ? 'Run "notion init" to reconfigure your integration token' : void 0
287
+ );
288
+ process.stderr.write(mappedError.format() + "\n");
289
+ process.exit(1);
290
+ }
291
+ const message = error2 instanceof Error ? error2.message : String(error2);
292
+ process.stderr.write(`[${ErrorCodes.UNKNOWN}] ${message}
293
+ `);
294
+ process.exit(1);
295
+ }
296
+ });
297
+ }
298
+
299
+ // src/commands/init.ts
300
+ function initCommand() {
301
+ const cmd = new Command("init");
302
+ cmd.description("authenticate with Notion and save a profile").action(withErrorHandling(async () => {
303
+ if (!process.stdin.isTTY) {
304
+ throw new CliError(
305
+ ErrorCodes.AUTH_NO_TOKEN,
306
+ "Cannot run interactive init in non-TTY mode.",
307
+ "Set NOTION_API_TOKEN environment variable or create .notion.yaml"
308
+ );
309
+ }
310
+ const profileName = await input({
311
+ message: "Profile name:",
312
+ default: "default"
313
+ });
314
+ const token = await password({
315
+ message: "Integration token (from notion.so/profile/integrations/internal):",
316
+ mask: "*"
317
+ });
318
+ stderrWrite("Validating token...");
319
+ const { workspaceName, workspaceId } = await validateToken(token);
320
+ stderrWrite(success(`\u2713 Connected to workspace: ${bold(workspaceName)}`));
321
+ const config = await readGlobalConfig();
322
+ if (config.profiles?.[profileName]) {
323
+ const replace = await confirm({
324
+ message: `Profile "${profileName}" already exists. Replace?`,
325
+ default: false
326
+ });
327
+ if (!replace) {
328
+ stderrWrite("Aborted.");
329
+ return;
330
+ }
331
+ }
332
+ const profiles = config.profiles ?? {};
333
+ profiles[profileName] = {
334
+ token,
335
+ workspace_name: workspaceName,
336
+ workspace_id: workspaceId
337
+ };
338
+ await writeGlobalConfig({
339
+ ...config,
340
+ profiles,
341
+ active_profile: profileName
342
+ });
343
+ stderrWrite(success(`Profile "${profileName}" saved and set as active.`));
344
+ stderrWrite(dim("Checking integration access..."));
345
+ try {
346
+ const notion = createNotionClient(token);
347
+ const probe = await notion.search({ page_size: 1 });
348
+ if (probe.results.length === 0) {
349
+ stderrWrite("");
350
+ stderrWrite("\u26A0\uFE0F Your integration has no pages connected.");
351
+ stderrWrite(" To grant access, open any Notion page or database:");
352
+ stderrWrite(" 1. Click \xB7\xB7\xB7 (three dots) in the top-right corner");
353
+ stderrWrite(' 2. Select "Connect to"');
354
+ stderrWrite(` 3. Choose "${workspaceName}"`);
355
+ stderrWrite(" Then re-run any notion command to confirm access.");
356
+ } else {
357
+ stderrWrite(success(`\u2713 Integration has access to content in ${bold(workspaceName)}.`));
358
+ }
359
+ } catch {
360
+ stderrWrite(dim("(Could not verify integration access \u2014 run `notion ls` to check)"));
361
+ }
362
+ }));
363
+ return cmd;
364
+ }
365
+
366
+ // src/commands/profile/list.ts
367
+ import { Command as Command2 } from "commander";
368
+ function profileListCommand() {
369
+ const cmd = new Command2("list");
370
+ cmd.description("list all authentication profiles").action(withErrorHandling(async () => {
371
+ const config = await readGlobalConfig();
372
+ const profiles = config.profiles ?? {};
373
+ const profileNames = Object.keys(profiles);
374
+ if (profileNames.length === 0) {
375
+ process.stdout.write("No profiles configured. Run `notion init` to get started.\n");
376
+ return;
377
+ }
378
+ for (const name of profileNames) {
379
+ const profile = profiles[name];
380
+ const isActive = config.active_profile === name;
381
+ const marker = isActive ? bold("* ") : " ";
382
+ const activeLabel = isActive ? " (active)" : "";
383
+ const workspaceInfo = profile.workspace_name ? dim(` \u2014 ${profile.workspace_name}`) : "";
384
+ process.stdout.write(`${marker}${name}${activeLabel}${workspaceInfo}
385
+ `);
386
+ }
387
+ }));
388
+ return cmd;
389
+ }
390
+
391
+ // src/commands/profile/use.ts
392
+ import { Command as Command3 } from "commander";
393
+ function profileUseCommand() {
394
+ const cmd = new Command3("use");
395
+ cmd.description("switch the active profile").argument("<name>", "profile name to activate").action(withErrorHandling(async (name) => {
396
+ const config = await readGlobalConfig();
397
+ const profiles = config.profiles ?? {};
398
+ if (!profiles[name]) {
399
+ throw new CliError(
400
+ ErrorCodes.AUTH_PROFILE_NOT_FOUND,
401
+ `Profile "${name}" not found.`,
402
+ `Run "notion profile list" to see available profiles`
403
+ );
404
+ }
405
+ await writeGlobalConfig({
406
+ ...config,
407
+ active_profile: name
408
+ });
409
+ stderrWrite(success(`Switched to profile "${name}".`));
410
+ }));
411
+ return cmd;
412
+ }
413
+
414
+ // src/commands/profile/remove.ts
415
+ import { Command as Command4 } from "commander";
416
+ function profileRemoveCommand() {
417
+ const cmd = new Command4("remove");
418
+ cmd.description("remove an authentication profile").argument("<name>", "profile name to remove").action(withErrorHandling(async (name) => {
419
+ const config = await readGlobalConfig();
420
+ const profiles = { ...config.profiles ?? {} };
421
+ if (!profiles[name]) {
422
+ throw new CliError(
423
+ ErrorCodes.AUTH_PROFILE_NOT_FOUND,
424
+ `Profile "${name}" not found.`,
425
+ `Run "notion profile list" to see available profiles`
426
+ );
427
+ }
428
+ delete profiles[name];
429
+ const newActiveProfile = config.active_profile === name ? void 0 : config.active_profile;
430
+ await writeGlobalConfig({
431
+ ...config,
432
+ profiles,
433
+ active_profile: newActiveProfile
434
+ });
435
+ stderrWrite(success(`Profile "${name}" removed.`));
436
+ }));
437
+ return cmd;
438
+ }
439
+
440
+ // src/commands/completion.ts
441
+ import { Command as Command5 } from "commander";
442
+ var BASH_COMPLETION = `# notion bash completion
443
+ _notion_completion() {
444
+ local cur prev words cword
445
+ _init_completion || return
446
+
447
+ local commands="init profile completion --help --version --verbose --color"
448
+ local profile_commands="list use remove"
449
+
450
+ case "$prev" in
451
+ notion)
452
+ COMPREPLY=($(compgen -W "$commands" -- "$cur"))
453
+ return 0
454
+ ;;
455
+ profile)
456
+ COMPREPLY=($(compgen -W "$profile_commands" -- "$cur"))
457
+ return 0
458
+ ;;
459
+ completion)
460
+ COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
461
+ return 0
462
+ ;;
463
+ esac
464
+
465
+ COMPREPLY=($(compgen -W "$commands" -- "$cur"))
466
+ }
467
+
468
+ complete -F _notion_completion notion
469
+ `;
470
+ var ZSH_COMPLETION = `#compdef notion
471
+ # notion zsh completion
472
+
473
+ _notion() {
474
+ local -a commands
475
+
476
+ commands=(
477
+ 'init:authenticate with Notion and save a profile'
478
+ 'profile:manage authentication profiles'
479
+ 'completion:output shell completion script'
480
+ )
481
+
482
+ local -a global_opts
483
+ global_opts=(
484
+ '--help[display help]'
485
+ '--version[output version]'
486
+ '--verbose[show API requests/responses]'
487
+ '--color[force color output]'
488
+ )
489
+
490
+ if (( CURRENT == 2 )); then
491
+ _describe 'command' commands
492
+ _arguments $global_opts
493
+ return
494
+ fi
495
+
496
+ case $words[2] in
497
+ profile)
498
+ local -a profile_cmds
499
+ profile_cmds=(
500
+ 'list:list all authentication profiles'
501
+ 'use:switch the active profile'
502
+ 'remove:remove an authentication profile'
503
+ )
504
+ _describe 'profile command' profile_cmds
505
+ ;;
506
+ completion)
507
+ local -a shells
508
+ shells=('bash' 'zsh' 'fish')
509
+ _describe 'shell' shells
510
+ ;;
511
+ esac
512
+ }
513
+
514
+ _notion "$@"
515
+ `;
516
+ var FISH_COMPLETION = `# notion fish completion
517
+
518
+ # Disable file completion by default
519
+ complete -c notion -f
520
+
521
+ # Global options
522
+ complete -c notion -l help -d 'display help'
523
+ complete -c notion -l version -d 'output version'
524
+ complete -c notion -l verbose -d 'show API requests/responses'
525
+ complete -c notion -l color -d 'force color output'
526
+
527
+ # Top-level commands
528
+ complete -c notion -n '__fish_use_subcommand' -a init -d 'authenticate with Notion and save a profile'
529
+ complete -c notion -n '__fish_use_subcommand' -a profile -d 'manage authentication profiles'
530
+ complete -c notion -n '__fish_use_subcommand' -a completion -d 'output shell completion script'
531
+
532
+ # profile subcommands
533
+ complete -c notion -n '__fish_seen_subcommand_from profile' -a list -d 'list all authentication profiles'
534
+ complete -c notion -n '__fish_seen_subcommand_from profile' -a use -d 'switch the active profile'
535
+ complete -c notion -n '__fish_seen_subcommand_from profile' -a remove -d 'remove an authentication profile'
536
+
537
+ # completion shells
538
+ complete -c notion -n '__fish_seen_subcommand_from completion' -a bash -d 'bash completion script'
539
+ complete -c notion -n '__fish_seen_subcommand_from completion' -a zsh -d 'zsh completion script'
540
+ complete -c notion -n '__fish_seen_subcommand_from completion' -a fish -d 'fish completion script'
541
+ `;
542
+ function completionCommand() {
543
+ const cmd = new Command5("completion");
544
+ cmd.description("output shell completion script").argument("<shell>", "shell type (bash, zsh, fish)").action(withErrorHandling(async (shell) => {
545
+ switch (shell) {
546
+ case "bash":
547
+ process.stdout.write(BASH_COMPLETION);
548
+ break;
549
+ case "zsh":
550
+ process.stdout.write(ZSH_COMPLETION);
551
+ break;
552
+ case "fish":
553
+ process.stdout.write(FISH_COMPLETION);
554
+ break;
555
+ default:
556
+ throw new CliError(
557
+ ErrorCodes.UNKNOWN,
558
+ `Unknown shell: "${shell}".`,
559
+ "Supported shells: bash, zsh, fish"
560
+ );
561
+ }
562
+ }));
563
+ return cmd;
564
+ }
565
+
566
+ // src/commands/search.ts
567
+ import { Command as Command6 } from "commander";
568
+ import { isFullPageOrDataSource } from "@notionhq/client";
569
+
570
+ // src/config/local-config.ts
571
+ import { readFile as readFile2 } from "fs/promises";
572
+ import { join as join2 } from "path";
573
+ import { parse as parse2 } from "yaml";
574
+ async function readLocalConfig() {
575
+ const localConfigPath = join2(process.cwd(), ".notion.yaml");
576
+ let raw;
577
+ try {
578
+ raw = await readFile2(localConfigPath, "utf-8");
579
+ } catch (err) {
580
+ if (err.code === "ENOENT") {
581
+ return null;
582
+ }
583
+ throw new CliError(
584
+ ErrorCodes.CONFIG_READ_ERROR,
585
+ `Failed to read local config: ${localConfigPath}`,
586
+ "Check file permissions"
587
+ );
588
+ }
589
+ let parsed;
590
+ try {
591
+ parsed = parse2(raw) ?? {};
592
+ } catch {
593
+ throw new CliError(
594
+ ErrorCodes.CONFIG_INVALID,
595
+ `Failed to parse .notion.yaml`,
596
+ "Check that the file contains valid YAML"
597
+ );
598
+ }
599
+ if (parsed.profile !== void 0 && parsed.token !== void 0) {
600
+ throw new CliError(
601
+ ErrorCodes.CONFIG_INVALID,
602
+ '.notion.yaml cannot specify both "profile" and "token"',
603
+ 'Use either "profile: <name>" to reference a saved profile, or "token: <value>" for a direct token'
604
+ );
605
+ }
606
+ return parsed;
607
+ }
608
+
609
+ // src/config/token.ts
610
+ async function resolveToken() {
611
+ const envToken = process.env["NOTION_API_TOKEN"];
612
+ if (envToken) {
613
+ return { token: envToken, source: "NOTION_API_TOKEN" };
614
+ }
615
+ const localConfig = await readLocalConfig();
616
+ if (localConfig !== null) {
617
+ if (localConfig.token) {
618
+ return { token: localConfig.token, source: ".notion.yaml" };
619
+ }
620
+ if (localConfig.profile) {
621
+ const globalConfig2 = await readGlobalConfig();
622
+ const profileToken = globalConfig2.profiles?.[localConfig.profile]?.token;
623
+ if (profileToken) {
624
+ return { token: profileToken, source: `profile: ${localConfig.profile}` };
625
+ }
626
+ }
627
+ }
628
+ const globalConfig = await readGlobalConfig();
629
+ if (globalConfig.active_profile) {
630
+ const profileToken = globalConfig.profiles?.[globalConfig.active_profile]?.token;
631
+ if (profileToken) {
632
+ return { token: profileToken, source: `profile: ${globalConfig.active_profile}` };
633
+ }
634
+ }
635
+ throw new CliError(
636
+ ErrorCodes.AUTH_NO_TOKEN,
637
+ "No authentication token found.",
638
+ 'Run "notion init" to set up a profile'
639
+ );
640
+ }
641
+
642
+ // src/commands/search.ts
643
+ function getTitle(item) {
644
+ if (item.object === "data_source") {
645
+ return item.title.map((t) => t.plain_text).join("") || "(untitled)";
646
+ }
647
+ const titleProp = Object.values(item.properties).find((p) => p.type === "title");
648
+ if (titleProp?.type === "title") {
649
+ return titleProp.title.map((t) => t.plain_text).join("") || "(untitled)";
650
+ }
651
+ return "(untitled)";
652
+ }
653
+ function toSdkFilterValue(type) {
654
+ return type === "database" ? "data_source" : "page";
655
+ }
656
+ function displayType(item) {
657
+ return item.object === "data_source" ? "database" : item.object;
658
+ }
659
+ function searchCommand() {
660
+ const cmd = new Command6("search");
661
+ cmd.description("search Notion workspace by keyword").argument("<query>", "search keyword").option("--type <type>", "filter by object type (page or database)", (val) => {
662
+ if (val !== "page" && val !== "database") {
663
+ throw new Error('--type must be "page" or "database"');
664
+ }
665
+ return val;
666
+ }).option("--cursor <cursor>", "start from this pagination cursor (from a previous --next hint)").option("--json", "force JSON output").action(
667
+ withErrorHandling(async (query, opts) => {
668
+ if (opts.json) {
669
+ setOutputMode("json");
670
+ }
671
+ const { token, source } = await resolveToken();
672
+ reportTokenSource(source);
673
+ const notion = createNotionClient(token);
674
+ const response = await notion.search({
675
+ query,
676
+ filter: opts.type ? { property: "object", value: toSdkFilterValue(opts.type) } : void 0,
677
+ start_cursor: opts.cursor,
678
+ page_size: 20
679
+ });
680
+ const fullResults = response.results.filter((r) => isFullPageOrDataSource(r));
681
+ if (fullResults.length === 0) {
682
+ process.stdout.write(`No results found for "${query}"
683
+ `);
684
+ return;
685
+ }
686
+ const headers = ["TYPE", "TITLE", "ID", "MODIFIED"];
687
+ const rows = fullResults.map((item) => [
688
+ displayType(item),
689
+ getTitle(item),
690
+ item.id,
691
+ item.last_edited_time.split("T")[0]
692
+ ]);
693
+ printOutput(fullResults, headers, rows);
694
+ if (response.has_more && response.next_cursor) {
695
+ process.stderr.write(`
696
+ --next: notion search "${query}" --cursor ${response.next_cursor}
697
+ `);
698
+ }
699
+ })
700
+ );
701
+ return cmd;
702
+ }
703
+
704
+ // src/commands/ls.ts
705
+ import { Command as Command7 } from "commander";
706
+ import { isFullPageOrDataSource as isFullPageOrDataSource2 } from "@notionhq/client";
707
+ function getTitle2(item) {
708
+ if (item.object === "data_source") {
709
+ return item.title.map((t) => t.plain_text).join("") || "(untitled)";
710
+ }
711
+ const titleProp = Object.values(item.properties).find((p) => p.type === "title");
712
+ if (titleProp?.type === "title") {
713
+ return titleProp.title.map((t) => t.plain_text).join("") || "(untitled)";
714
+ }
715
+ return "(untitled)";
716
+ }
717
+ function displayType2(item) {
718
+ return item.object === "data_source" ? "database" : item.object;
719
+ }
720
+ function lsCommand() {
721
+ const cmd = new Command7("ls");
722
+ cmd.description("list accessible Notion pages and databases").option("--type <type>", "filter by object type (page or database)", (val) => {
723
+ if (val !== "page" && val !== "database") {
724
+ throw new Error('--type must be "page" or "database"');
725
+ }
726
+ return val;
727
+ }).option("--cursor <cursor>", "start from this pagination cursor (from a previous --next hint)").option("--json", "force JSON output").action(
728
+ withErrorHandling(async (opts) => {
729
+ if (opts.json) {
730
+ setOutputMode("json");
731
+ }
732
+ const { token, source } = await resolveToken();
733
+ reportTokenSource(source);
734
+ const notion = createNotionClient(token);
735
+ const response = await notion.search({
736
+ start_cursor: opts.cursor,
737
+ page_size: 20
738
+ });
739
+ let items = response.results.filter((r) => isFullPageOrDataSource2(r));
740
+ if (opts.type) {
741
+ const filterType = opts.type;
742
+ items = items.filter(
743
+ (r) => filterType === "database" ? r.object === "data_source" : r.object === filterType
744
+ );
745
+ }
746
+ if (items.length === 0) {
747
+ process.stdout.write("No accessible content found\n");
748
+ return;
749
+ }
750
+ const headers = ["TYPE", "TITLE", "ID", "MODIFIED"];
751
+ const rows = items.map((item) => [
752
+ displayType2(item),
753
+ getTitle2(item),
754
+ item.id,
755
+ item.last_edited_time.split("T")[0]
756
+ ]);
757
+ printOutput(items, headers, rows);
758
+ if (response.has_more && response.next_cursor) {
759
+ process.stderr.write(`
760
+ --next: notion ls --cursor ${response.next_cursor}
761
+ `);
762
+ }
763
+ })
764
+ );
765
+ return cmd;
766
+ }
767
+
768
+ // src/commands/open.ts
769
+ import { exec } from "child_process";
770
+ import { promisify } from "util";
771
+ import { Command as Command8 } from "commander";
772
+
773
+ // src/notion/url-parser.ts
774
+ var NOTION_ID_REGEX = /^[0-9a-f]{32}$/i;
775
+ var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
776
+ var NOTION_URL_REGEX = /https?:\/\/(?:[a-zA-Z0-9-]+\.)?notion\.(?:so|site)\/.*?([0-9a-f]{32})(?:[?#]|$)/i;
777
+ function throwInvalidId(input2) {
778
+ throw new CliError(
779
+ ErrorCodes.INVALID_ID,
780
+ `Cannot parse Notion ID from: ${input2}`,
781
+ "Provide a valid Notion URL or page/database ID"
782
+ );
783
+ }
784
+ function parseNotionId(input2) {
785
+ if (!input2) throwInvalidId(input2);
786
+ if (NOTION_ID_REGEX.test(input2)) {
787
+ return input2.toLowerCase();
788
+ }
789
+ if (UUID_REGEX.test(input2)) {
790
+ return input2.replace(/-/g, "").toLowerCase();
791
+ }
792
+ const urlMatch = NOTION_URL_REGEX.exec(input2);
793
+ if (urlMatch) {
794
+ return urlMatch[1].toLowerCase();
795
+ }
796
+ throwInvalidId(input2);
797
+ }
798
+ function toUuid(id) {
799
+ return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
800
+ }
801
+
802
+ // src/commands/open.ts
803
+ var execAsync = promisify(exec);
804
+ function openCommand() {
805
+ const cmd = new Command8("open");
806
+ cmd.description("open a Notion page in the default browser").argument("<id/url>", "Notion page ID or URL").action(withErrorHandling(async (idOrUrl) => {
807
+ const id = parseNotionId(idOrUrl);
808
+ const url = `https://www.notion.so/${id}`;
809
+ const platform = process.platform;
810
+ const opener = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
811
+ await execAsync(`${opener} "${url}"`);
812
+ process.stdout.write(`Opening ${url}
813
+ `);
814
+ }));
815
+ return cmd;
816
+ }
817
+
818
+ // src/commands/users.ts
819
+ import { Command as Command9 } from "commander";
820
+
821
+ // src/output/paginate.ts
822
+ async function paginateResults(fetcher) {
823
+ const allResults = [];
824
+ let cursor;
825
+ let hasMore = true;
826
+ while (hasMore) {
827
+ const response = await fetcher(cursor);
828
+ allResults.push(...response.results);
829
+ cursor = response.next_cursor ?? void 0;
830
+ hasMore = response.has_more;
831
+ }
832
+ return allResults;
833
+ }
834
+
835
+ // src/commands/users.ts
836
+ function getEmailOrWorkspace(user) {
837
+ if (user.type === "person") {
838
+ return user.person.email ?? "\u2014";
839
+ }
840
+ if (user.type === "bot") {
841
+ const bot = user.bot;
842
+ return "workspace_name" in bot && bot.workspace_name ? bot.workspace_name : "\u2014";
843
+ }
844
+ return "\u2014";
845
+ }
846
+ function usersCommand() {
847
+ const cmd = new Command9("users");
848
+ cmd.description("list all users in the workspace").option("--json", "output as JSON").action(withErrorHandling(async (opts) => {
849
+ if (opts.json) setOutputMode("json");
850
+ const { token, source } = await resolveToken();
851
+ reportTokenSource(source);
852
+ const notion = createNotionClient(token);
853
+ const allUsers = await paginateResults(
854
+ (cursor) => notion.users.list({ start_cursor: cursor })
855
+ );
856
+ const users = allUsers.filter((u) => u.name !== void 0);
857
+ const rows = users.map((user) => [
858
+ user.type,
859
+ user.name ?? "(unnamed)",
860
+ getEmailOrWorkspace(user),
861
+ user.id
862
+ ]);
863
+ printOutput(users, ["TYPE", "NAME", "EMAIL / WORKSPACE", "ID"], rows);
864
+ }));
865
+ return cmd;
866
+ }
867
+
868
+ // src/commands/comments.ts
869
+ import { Command as Command10 } from "commander";
870
+ function commentsCommand() {
871
+ const cmd = new Command10("comments");
872
+ cmd.description("list comments on a Notion page").argument("<id/url>", "Notion page ID or URL").option("--json", "output as JSON").action(withErrorHandling(async (idOrUrl, opts) => {
873
+ if (opts.json) setOutputMode("json");
874
+ const id = parseNotionId(idOrUrl);
875
+ const uuid = toUuid(id);
876
+ const { token, source } = await resolveToken();
877
+ reportTokenSource(source);
878
+ const notion = createNotionClient(token);
879
+ const comments = await paginateResults(
880
+ (cursor) => notion.comments.list({ block_id: uuid, start_cursor: cursor })
881
+ );
882
+ if (comments.length === 0) {
883
+ process.stdout.write("No comments found on this page\n");
884
+ return;
885
+ }
886
+ const rows = comments.map((comment) => {
887
+ const text = comment.rich_text.map((t) => t.plain_text).join("");
888
+ return [
889
+ comment.created_time.split("T")[0],
890
+ comment.created_by.id.slice(0, 8) + "...",
891
+ text.slice(0, 80) + (text.length > 80 ? "\u2026" : "")
892
+ ];
893
+ });
894
+ printOutput(comments, ["DATE", "AUTHOR ID", "COMMENT"], rows);
895
+ }));
896
+ return cmd;
897
+ }
898
+
899
+ // src/commands/read.ts
900
+ import { Command as Command11 } from "commander";
901
+
902
+ // src/services/page.service.ts
903
+ import { collectPaginatedAPI, isFullBlock } from "@notionhq/client";
904
+ var MAX_CONCURRENT_REQUESTS = 3;
905
+ async function fetchBlockTree(client, blockId, depth, maxDepth) {
906
+ if (depth >= maxDepth) return [];
907
+ const rawBlocks = await collectPaginatedAPI(client.blocks.children.list, {
908
+ block_id: blockId
909
+ });
910
+ const blocks = rawBlocks.filter(isFullBlock);
911
+ const SKIP_RECURSE = /* @__PURE__ */ new Set(["child_page", "child_database"]);
912
+ const nodes = [];
913
+ for (let i = 0; i < blocks.length; i += MAX_CONCURRENT_REQUESTS) {
914
+ const batch = blocks.slice(i, i + MAX_CONCURRENT_REQUESTS);
915
+ const batchNodes = await Promise.all(
916
+ batch.map(async (block) => {
917
+ const children = block.has_children && !SKIP_RECURSE.has(block.type) ? await fetchBlockTree(client, block.id, depth + 1, maxDepth) : [];
918
+ return { block, children };
919
+ })
920
+ );
921
+ nodes.push(...batchNodes);
922
+ }
923
+ return nodes;
924
+ }
925
+ async function fetchPageWithBlocks(client, pageId) {
926
+ const page = await client.pages.retrieve({ page_id: pageId });
927
+ const blocks = await fetchBlockTree(client, pageId, 0, 10);
928
+ return { page, blocks };
929
+ }
930
+
931
+ // src/blocks/rich-text.ts
932
+ function richTextToMd(richText) {
933
+ return richText.map(segmentToMd).join("");
934
+ }
935
+ function segmentToMd(segment) {
936
+ if (segment.type === "equation") {
937
+ return `$${segment.equation.expression}$`;
938
+ }
939
+ if (segment.type === "mention") {
940
+ const text = segment.plain_text;
941
+ return segment.href ? `[${text}](${segment.href})` : text;
942
+ }
943
+ const annotated = applyAnnotations(segment.text.content, segment.annotations);
944
+ return segment.text.link ? `[${annotated}](${segment.text.link.url})` : annotated;
945
+ }
946
+ function applyAnnotations(text, annotations) {
947
+ let result = text;
948
+ if (annotations.code) result = `\`${result}\``;
949
+ if (annotations.strikethrough) result = `~~${result}~~`;
950
+ if (annotations.italic) result = `_${result}_`;
951
+ if (annotations.bold) result = `**${result}**`;
952
+ return result;
953
+ }
954
+
955
+ // src/blocks/converters.ts
956
+ function indentChildren(childrenMd) {
957
+ return childrenMd.split("\n").filter(Boolean).map((line) => " " + line).join("\n") + "\n";
958
+ }
959
+ var converters = {
960
+ paragraph(block) {
961
+ const b = block;
962
+ return `${richTextToMd(b.paragraph.rich_text)}
963
+ `;
964
+ },
965
+ heading_1(block) {
966
+ const b = block;
967
+ return `# ${richTextToMd(b.heading_1.rich_text)}
968
+ `;
969
+ },
970
+ heading_2(block) {
971
+ const b = block;
972
+ return `## ${richTextToMd(b.heading_2.rich_text)}
973
+ `;
974
+ },
975
+ heading_3(block) {
976
+ const b = block;
977
+ return `### ${richTextToMd(b.heading_3.rich_text)}
978
+ `;
979
+ },
980
+ bulleted_list_item(block, ctx) {
981
+ const b = block;
982
+ const text = richTextToMd(b.bulleted_list_item.rich_text);
983
+ const header = `- ${text}
984
+ `;
985
+ if (ctx?.childrenMd) {
986
+ return header + indentChildren(ctx.childrenMd);
987
+ }
988
+ return header;
989
+ },
990
+ numbered_list_item(block, ctx) {
991
+ const b = block;
992
+ const num = ctx?.listNumber ?? 1;
993
+ return `${num}. ${richTextToMd(b.numbered_list_item.rich_text)}
994
+ `;
995
+ },
996
+ to_do(block) {
997
+ const b = block;
998
+ const checkbox = b.to_do.checked ? "[x]" : "[ ]";
999
+ return `- ${checkbox} ${richTextToMd(b.to_do.rich_text)}
1000
+ `;
1001
+ },
1002
+ code(block) {
1003
+ const b = block;
1004
+ const lang = b.code.language === "plain text" ? "" : b.code.language;
1005
+ const content = richTextToMd(b.code.rich_text);
1006
+ return `\`\`\`${lang}
1007
+ ${content}
1008
+ \`\`\`
1009
+ `;
1010
+ },
1011
+ quote(block) {
1012
+ const b = block;
1013
+ return `> ${richTextToMd(b.quote.rich_text)}
1014
+ `;
1015
+ },
1016
+ divider() {
1017
+ return "---\n";
1018
+ },
1019
+ callout(block) {
1020
+ const b = block;
1021
+ const text = richTextToMd(b.callout.rich_text);
1022
+ const icon = b.callout.icon;
1023
+ if (icon?.type === "emoji") {
1024
+ return `> ${icon.emoji} ${text}
1025
+ `;
1026
+ }
1027
+ return `> ${text}
1028
+ `;
1029
+ },
1030
+ toggle(block, ctx) {
1031
+ const b = block;
1032
+ const header = `**${richTextToMd(b.toggle.rich_text)}**
1033
+ `;
1034
+ if (ctx?.childrenMd) {
1035
+ return header + ctx.childrenMd;
1036
+ }
1037
+ return header;
1038
+ },
1039
+ image(block) {
1040
+ const b = block;
1041
+ const caption = richTextToMd(b.image.caption);
1042
+ if (b.image.type === "file") {
1043
+ const url2 = b.image.file.url;
1044
+ const expiry = b.image.file.expiry_time;
1045
+ return `![${caption}](${url2}) <!-- expires: ${expiry} -->
1046
+ `;
1047
+ }
1048
+ const url = b.image.external.url;
1049
+ return `![${caption}](${url})
1050
+ `;
1051
+ },
1052
+ bookmark(block) {
1053
+ const b = block;
1054
+ const caption = richTextToMd(b.bookmark.caption);
1055
+ const text = caption || b.bookmark.url;
1056
+ return `[${text}](${b.bookmark.url})
1057
+ `;
1058
+ },
1059
+ child_page(block) {
1060
+ const b = block;
1061
+ return `### ${b.child_page.title}
1062
+ `;
1063
+ },
1064
+ child_database(block) {
1065
+ const b = block;
1066
+ return `### ${b.child_database.title}
1067
+ `;
1068
+ },
1069
+ link_preview(block) {
1070
+ const b = block;
1071
+ return `[${b.link_preview.url}](${b.link_preview.url})
1072
+ `;
1073
+ }
1074
+ };
1075
+ function blockToMd(block, ctx) {
1076
+ const converter = converters[block.type];
1077
+ if (converter) {
1078
+ return converter(block, ctx);
1079
+ }
1080
+ return `<!-- unsupported block: ${block.type} -->
1081
+ `;
1082
+ }
1083
+
1084
+ // src/blocks/properties.ts
1085
+ function formatPropertyValue(name, prop) {
1086
+ switch (prop.type) {
1087
+ case "title":
1088
+ return prop.title.map((rt) => rt.plain_text).join("");
1089
+ case "rich_text":
1090
+ return prop.rich_text.map((rt) => rt.plain_text).join("");
1091
+ case "number":
1092
+ return prop.number !== null ? String(prop.number) : "";
1093
+ case "select":
1094
+ return prop.select?.name ?? "";
1095
+ case "status":
1096
+ return prop.status?.name ?? "";
1097
+ case "multi_select":
1098
+ return prop.multi_select.map((s) => s.name).join(", ");
1099
+ case "date":
1100
+ if (!prop.date) return "";
1101
+ return prop.date.end ? `${prop.date.start} \u2192 ${prop.date.end}` : prop.date.start;
1102
+ case "checkbox":
1103
+ return prop.checkbox ? "true" : "false";
1104
+ case "url":
1105
+ return prop.url ?? "";
1106
+ case "email":
1107
+ return prop.email ?? "";
1108
+ case "phone_number":
1109
+ return prop.phone_number ?? "";
1110
+ case "people":
1111
+ return prop.people.map((p) => "name" in p && p.name ? p.name : p.id).join(", ");
1112
+ case "relation":
1113
+ return prop.relation.map((r) => r.id).join(", ");
1114
+ case "formula": {
1115
+ const f = prop.formula;
1116
+ if (f.type === "string") return f.string ?? "";
1117
+ if (f.type === "number") return f.number !== null ? String(f.number) : "";
1118
+ if (f.type === "boolean") return String(f.boolean);
1119
+ if (f.type === "date") return f.date?.start ?? "";
1120
+ return "";
1121
+ }
1122
+ case "rollup": {
1123
+ const r = prop.rollup;
1124
+ if (r.type === "number") return r.number !== null ? String(r.number) : "";
1125
+ if (r.type === "date") return r.date?.start ?? "";
1126
+ if (r.type === "array") return `[${r.array.length} items]`;
1127
+ return "";
1128
+ }
1129
+ case "created_time":
1130
+ return prop.created_time;
1131
+ case "last_edited_time":
1132
+ return prop.last_edited_time;
1133
+ case "created_by":
1134
+ return "name" in prop.created_by ? prop.created_by.name ?? prop.created_by.id : prop.created_by.id;
1135
+ case "last_edited_by":
1136
+ return "name" in prop.last_edited_by ? prop.last_edited_by.name ?? prop.last_edited_by.id : prop.last_edited_by.id;
1137
+ case "files":
1138
+ return prop.files.map((f) => {
1139
+ if (f.type === "external") return f.external.url;
1140
+ return f.name;
1141
+ }).join(", ");
1142
+ case "unique_id":
1143
+ return prop.unique_id.prefix ? `${prop.unique_id.prefix}-${prop.unique_id.number}` : String(prop.unique_id.number ?? "");
1144
+ default:
1145
+ return "";
1146
+ }
1147
+ }
1148
+
1149
+ // src/blocks/render.ts
1150
+ function buildPropertiesHeader(page) {
1151
+ const lines = ["---"];
1152
+ for (const [name, prop] of Object.entries(page.properties)) {
1153
+ const value = formatPropertyValue(name, prop);
1154
+ if (value) {
1155
+ lines.push(`${name}: ${value}`);
1156
+ }
1157
+ }
1158
+ lines.push("---", "");
1159
+ return lines.join("\n");
1160
+ }
1161
+ function renderBlockTree(blocks) {
1162
+ const parts = [];
1163
+ let listCounter = 0;
1164
+ for (const node of blocks) {
1165
+ if (node.block.type === "numbered_list_item") {
1166
+ listCounter++;
1167
+ } else {
1168
+ listCounter = 0;
1169
+ }
1170
+ const childrenMd = node.children.length > 0 ? renderBlockTree(node.children) : "";
1171
+ const md = blockToMd(node.block, {
1172
+ listNumber: node.block.type === "numbered_list_item" ? listCounter : void 0,
1173
+ childrenMd: childrenMd || void 0
1174
+ });
1175
+ parts.push(md);
1176
+ }
1177
+ return parts.join("");
1178
+ }
1179
+ function renderPageMarkdown({ page, blocks }) {
1180
+ const header = buildPropertiesHeader(page);
1181
+ const content = renderBlockTree(blocks);
1182
+ return header + content;
1183
+ }
1184
+
1185
+ // src/output/markdown.ts
1186
+ import { Chalk as Chalk2 } from "chalk";
1187
+ var c = new Chalk2({ level: 3 });
1188
+ function renderMarkdown(md) {
1189
+ if (!isatty()) return md;
1190
+ const lines = md.split("\n");
1191
+ const out = [];
1192
+ let inFence = false;
1193
+ let fenceLang = "";
1194
+ let fenceLines = [];
1195
+ for (const line of lines) {
1196
+ const fenceMatch = line.match(/^```(\w*)$/);
1197
+ if (fenceMatch && !inFence) {
1198
+ inFence = true;
1199
+ fenceLang = fenceMatch[1] ?? "";
1200
+ fenceLines = [];
1201
+ continue;
1202
+ }
1203
+ if (line === "```" && inFence) {
1204
+ inFence = false;
1205
+ const header = fenceLang ? c.dim(`[${fenceLang}]`) : "";
1206
+ if (header) out.push(header);
1207
+ for (const fl of fenceLines) {
1208
+ out.push(c.green(" " + fl));
1209
+ }
1210
+ out.push("");
1211
+ continue;
1212
+ }
1213
+ if (inFence) {
1214
+ fenceLines.push(line);
1215
+ continue;
1216
+ }
1217
+ if (line === "---") {
1218
+ out.push(c.dim("\u2500".repeat(40)));
1219
+ continue;
1220
+ }
1221
+ if (/^<!--.*-->$/.test(line.trim())) {
1222
+ continue;
1223
+ }
1224
+ const h1 = line.match(/^# (.+)/);
1225
+ if (h1) {
1226
+ out.push("\n" + c.bold.cyan(h1[1]));
1227
+ continue;
1228
+ }
1229
+ const h2 = line.match(/^## (.+)/);
1230
+ if (h2) {
1231
+ out.push("\n" + c.bold.blue(h2[1]));
1232
+ continue;
1233
+ }
1234
+ const h3 = line.match(/^### (.+)/);
1235
+ if (h3) {
1236
+ out.push("\n" + c.bold(h3[1]));
1237
+ continue;
1238
+ }
1239
+ const h4 = line.match(/^#### (.+)/);
1240
+ if (h4) {
1241
+ out.push(c.bold.underline(h4[1]));
1242
+ continue;
1243
+ }
1244
+ if (line.startsWith("> ")) {
1245
+ out.push(c.yellow("\u258E ") + renderInline(line.slice(2)));
1246
+ continue;
1247
+ }
1248
+ if (line === "---") {
1249
+ out.push(c.dim("\u2500".repeat(40)));
1250
+ continue;
1251
+ }
1252
+ const propMatch = line.match(/^([A-Za-z_][A-Za-z0-9_ ]*): (.+)$/);
1253
+ if (propMatch) {
1254
+ out.push(c.dim(propMatch[1] + ": ") + c.white(propMatch[2]));
1255
+ continue;
1256
+ }
1257
+ const bulletMatch = line.match(/^(\s*)- (\[[ x]\] )?(.+)/);
1258
+ if (bulletMatch) {
1259
+ const indent = bulletMatch[1] ?? "";
1260
+ const checkbox = bulletMatch[2];
1261
+ const text = bulletMatch[3] ?? "";
1262
+ if (checkbox) {
1263
+ const checked = checkbox.trim() === "[x]";
1264
+ const box = checked ? c.green("\u2611") : c.dim("\u2610");
1265
+ out.push(indent + box + " " + renderInline(text));
1266
+ } else {
1267
+ out.push(indent + c.cyan("\u2022") + " " + renderInline(text));
1268
+ }
1269
+ continue;
1270
+ }
1271
+ const numMatch = line.match(/^(\s*)(\d+)\. (.+)/);
1272
+ if (numMatch) {
1273
+ const indent = numMatch[1] ?? "";
1274
+ const num = numMatch[2] ?? "";
1275
+ const text = numMatch[3] ?? "";
1276
+ out.push(indent + c.cyan(num + ".") + " " + renderInline(text));
1277
+ continue;
1278
+ }
1279
+ out.push(renderInline(line));
1280
+ }
1281
+ return out.join("\n") + "\n";
1282
+ }
1283
+ function renderInline(text) {
1284
+ const codeSpans = [];
1285
+ let result = text.replace(/`([^`]+)`/g, (_, code) => {
1286
+ codeSpans.push(c.green(code));
1287
+ return `\0CODE${codeSpans.length - 1}\0`;
1288
+ });
1289
+ result = result.replace(/!\[([^\]]*)\]\([^)]+\)/g, (_, alt) => alt ? c.dim(`[image: ${alt}]`) : c.dim("[image]")).replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, t, url) => c.cyan.underline(t) + c.dim(` (${url})`)).replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => c.bold.italic(t)).replace(/\*\*(.+?)\*\*/g, (_, t) => c.bold(t)).replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, (_, t) => c.italic(t)).replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, (_, t) => c.italic(t)).replace(/~~(.+?)~~/g, (_, t) => c.strikethrough(t));
1290
+ result = result.replace(/\x00CODE(\d+)\x00/g, (_, i) => codeSpans[Number(i)] ?? "");
1291
+ return result;
1292
+ }
1293
+
1294
+ // src/commands/read.ts
1295
+ function readCommand() {
1296
+ return new Command11("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(
1297
+ withErrorHandling(async (id, options) => {
1298
+ const { token } = await resolveToken();
1299
+ const client = createNotionClient(token);
1300
+ const pageId = parseNotionId(id);
1301
+ const pageWithBlocks = await fetchPageWithBlocks(client, pageId);
1302
+ if (options.json) {
1303
+ process.stdout.write(JSON.stringify(pageWithBlocks, null, 2) + "\n");
1304
+ } else {
1305
+ const markdown = renderPageMarkdown(pageWithBlocks);
1306
+ if (options.md || !isatty()) {
1307
+ process.stdout.write(markdown);
1308
+ } else {
1309
+ process.stdout.write(renderMarkdown(markdown));
1310
+ }
1311
+ }
1312
+ })
1313
+ );
1314
+ }
1315
+
1316
+ // src/commands/db/schema.ts
1317
+ import { Command as Command12 } from "commander";
1318
+
1319
+ // src/services/database.service.ts
1320
+ import { isFullPage as isFullPage2 } from "@notionhq/client";
1321
+ async function fetchDatabaseSchema(client, dbId) {
1322
+ const ds = await client.dataSources.retrieve({ data_source_id: dbId });
1323
+ const title = "title" in ds ? ds.title.map((rt) => rt.plain_text).join("") || dbId : dbId;
1324
+ const properties = {};
1325
+ if ("properties" in ds) {
1326
+ for (const [name, prop] of Object.entries(ds.properties)) {
1327
+ const config = {
1328
+ id: prop.id,
1329
+ name,
1330
+ type: prop.type
1331
+ };
1332
+ if (prop.type === "select" && "select" in prop) {
1333
+ config.options = prop.select.options;
1334
+ } else if (prop.type === "status" && "status" in prop) {
1335
+ config.options = prop.status.options;
1336
+ } else if (prop.type === "multi_select" && "multi_select" in prop) {
1337
+ config.options = prop.multi_select.options;
1338
+ }
1339
+ properties[name] = config;
1340
+ }
1341
+ }
1342
+ return { id: dbId, title, properties };
1343
+ }
1344
+ async function queryDatabase(client, dbId, opts = {}) {
1345
+ const rawPages = await paginateResults(
1346
+ (cursor) => client.dataSources.query({
1347
+ data_source_id: dbId,
1348
+ filter: opts.filter,
1349
+ sorts: opts.sorts,
1350
+ start_cursor: cursor,
1351
+ page_size: 100
1352
+ })
1353
+ );
1354
+ return rawPages.filter(isFullPage2).map((page) => {
1355
+ const propValues = {};
1356
+ for (const [name, prop] of Object.entries(page.properties)) {
1357
+ if (opts.columns && !opts.columns.includes(name)) continue;
1358
+ propValues[name] = displayPropertyValue(prop);
1359
+ }
1360
+ return { id: page.id, properties: propValues, raw: page };
1361
+ });
1362
+ }
1363
+ function buildFilter(filterStrings, schema) {
1364
+ if (!filterStrings.length) return void 0;
1365
+ const filters = filterStrings.map((raw) => {
1366
+ const eqIdx = raw.indexOf("=");
1367
+ if (eqIdx === -1) {
1368
+ throw new CliError(
1369
+ ErrorCodes.INVALID_ARG,
1370
+ `Invalid filter syntax: "${raw}"`,
1371
+ 'Use format: --filter "PropertyName=Value"'
1372
+ );
1373
+ }
1374
+ const propName = raw.slice(0, eqIdx).trim();
1375
+ const value = raw.slice(eqIdx + 1).trim();
1376
+ const propConfig = schema.properties[propName];
1377
+ if (!propConfig) {
1378
+ const available = Object.keys(schema.properties).join(", ");
1379
+ throw new CliError(
1380
+ ErrorCodes.INVALID_ARG,
1381
+ `Property "${propName}" not found`,
1382
+ `Available properties: ${available}`
1383
+ );
1384
+ }
1385
+ return buildPropertyFilter(propName, propConfig.type, value);
1386
+ });
1387
+ return filters.length === 1 ? filters[0] : { and: filters };
1388
+ }
1389
+ function buildPropertyFilter(property, type, value) {
1390
+ switch (type) {
1391
+ case "select":
1392
+ return { property, select: { equals: value } };
1393
+ case "status":
1394
+ return { property, status: { equals: value } };
1395
+ case "multi_select":
1396
+ return { property, multi_select: { contains: value } };
1397
+ case "checkbox":
1398
+ return { property, checkbox: { equals: value.toLowerCase() === "true" } };
1399
+ case "number":
1400
+ return { property, number: { equals: Number(value) } };
1401
+ case "title":
1402
+ return { property, title: { contains: value } };
1403
+ case "rich_text":
1404
+ return { property, rich_text: { contains: value } };
1405
+ case "url":
1406
+ return { property, url: { contains: value } };
1407
+ case "email":
1408
+ return { property, email: { contains: value } };
1409
+ default:
1410
+ throw new CliError(
1411
+ ErrorCodes.INVALID_ARG,
1412
+ `Filtering by property type "${type}" is not supported`
1413
+ );
1414
+ }
1415
+ }
1416
+ function buildSorts(sortStrings) {
1417
+ return sortStrings.map((raw) => {
1418
+ const colonIdx = raw.lastIndexOf(":");
1419
+ if (colonIdx === -1) {
1420
+ return { property: raw.trim(), direction: "ascending" };
1421
+ }
1422
+ const property = raw.slice(0, colonIdx).trim();
1423
+ const dir = raw.slice(colonIdx + 1).trim().toLowerCase();
1424
+ return {
1425
+ property,
1426
+ direction: dir === "desc" || dir === "descending" ? "descending" : "ascending"
1427
+ };
1428
+ });
1429
+ }
1430
+ function displayPropertyValue(prop) {
1431
+ switch (prop.type) {
1432
+ case "title":
1433
+ return prop.title.map((r) => r.plain_text).join("").replace(/\n/g, " ");
1434
+ case "rich_text":
1435
+ return prop.rich_text.map((r) => r.plain_text).join("").replace(/\n/g, " ");
1436
+ case "number":
1437
+ return prop.number !== null && prop.number !== void 0 ? String(prop.number) : "";
1438
+ case "select":
1439
+ return prop.select?.name ?? "";
1440
+ case "status":
1441
+ return prop.status?.name ?? "";
1442
+ case "multi_select":
1443
+ return prop.multi_select.map((s) => s.name).join(", ");
1444
+ case "date":
1445
+ return prop.date ? prop.date.end ? `${prop.date.start} \u2192 ${prop.date.end}` : prop.date.start : "";
1446
+ case "checkbox":
1447
+ return prop.checkbox ? "\u2713" : "\u2717";
1448
+ case "url":
1449
+ return prop.url ?? "";
1450
+ case "email":
1451
+ return prop.email ?? "";
1452
+ case "phone_number":
1453
+ return prop.phone_number ?? "";
1454
+ case "people":
1455
+ return prop.people.map((p) => "name" in p && p.name ? p.name : p.id).join(", ");
1456
+ case "relation":
1457
+ return prop.relation.length > 0 ? `[${prop.relation.length}]` : "";
1458
+ case "formula": {
1459
+ const f = prop.formula;
1460
+ if (f.type === "string") return f.string ?? "";
1461
+ if (f.type === "number")
1462
+ return f.number !== null && f.number !== void 0 ? String(f.number) : "";
1463
+ if (f.type === "boolean") return f.boolean ? "true" : "false";
1464
+ if (f.type === "date") return f.date?.start ?? "";
1465
+ return "";
1466
+ }
1467
+ case "created_time":
1468
+ return prop.created_time;
1469
+ case "last_edited_time":
1470
+ return prop.last_edited_time;
1471
+ case "unique_id":
1472
+ return prop.unique_id.prefix ? `${prop.unique_id.prefix}-${prop.unique_id.number}` : String(prop.unique_id.number ?? "");
1473
+ default:
1474
+ return "";
1475
+ }
1476
+ }
1477
+
1478
+ // src/commands/db/schema.ts
1479
+ function dbSchemaCommand() {
1480
+ return new Command12("schema").description("Show database schema (property names, types, and options)").argument("<id>", "Notion database ID or URL").option("--json", "Output raw JSON").action(
1481
+ withErrorHandling(async (id, options) => {
1482
+ const { token } = await resolveToken();
1483
+ const client = createNotionClient(token);
1484
+ const dbId = parseNotionId(id);
1485
+ const schema = await fetchDatabaseSchema(client, dbId);
1486
+ if (options.json || !isHumanMode()) {
1487
+ process.stdout.write(formatJSON(schema) + "\n");
1488
+ return;
1489
+ }
1490
+ const headers = ["PROPERTY", "TYPE", "OPTIONS"];
1491
+ const rows = Object.values(schema.properties).map((prop) => [
1492
+ prop.name,
1493
+ prop.type,
1494
+ prop.options ? prop.options.map((o) => o.name).join(", ") : ""
1495
+ ]);
1496
+ process.stdout.write(formatTable(rows, headers) + "\n");
1497
+ })
1498
+ );
1499
+ }
1500
+
1501
+ // src/commands/db/query.ts
1502
+ import { Command as Command13 } from "commander";
1503
+ var SKIP_TYPES_IN_AUTO = /* @__PURE__ */ new Set(["relation", "rich_text", "people"]);
1504
+ function autoSelectColumns(schema, entries) {
1505
+ const termWidth = process.stdout.columns || 120;
1506
+ const COL_SEP = 2;
1507
+ const candidates = Object.values(schema.properties).filter((p) => !SKIP_TYPES_IN_AUTO.has(p.type)).map((p) => p.name);
1508
+ const widths = candidates.map((col) => {
1509
+ const header = col.toUpperCase().length;
1510
+ const maxData = entries.reduce((max, e) => Math.max(max, (e.properties[col] ?? "").length), 0);
1511
+ return Math.min(Math.max(header, maxData), 40);
1512
+ });
1513
+ const selected = [];
1514
+ let usedWidth = 0;
1515
+ for (let i = 0; i < candidates.length; i++) {
1516
+ const needed = (selected.length > 0 ? COL_SEP : 0) + widths[i];
1517
+ if (usedWidth + needed > termWidth) break;
1518
+ selected.push(candidates[i]);
1519
+ usedWidth += needed;
1520
+ }
1521
+ if (selected.length === 0 && candidates.length > 0) {
1522
+ selected.push(candidates[0]);
1523
+ }
1524
+ return selected;
1525
+ }
1526
+ function dbQueryCommand() {
1527
+ return new Command13("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option("--filter <filter>", 'Filter entries (repeatable): --filter "Status=Done"', collect, []).option("--sort <sort>", 'Sort entries (repeatable): --sort "Name:asc"', collect, []).option("--columns <columns>", 'Comma-separated list of columns to display: --columns "Title,Status"').option("--json", "Output raw JSON").action(
1528
+ withErrorHandling(
1529
+ async (id, options) => {
1530
+ const { token } = await resolveToken();
1531
+ const client = createNotionClient(token);
1532
+ const dbId = parseNotionId(id);
1533
+ const schema = await fetchDatabaseSchema(client, dbId);
1534
+ const columns = options.columns ? options.columns.split(",").map((c2) => c2.trim()) : void 0;
1535
+ const filter = options.filter.length ? buildFilter(options.filter, schema) : void 0;
1536
+ const sorts = options.sort.length ? buildSorts(options.sort) : void 0;
1537
+ const entries = await queryDatabase(client, dbId, { filter, sorts, columns });
1538
+ if (options.json || !isHumanMode()) {
1539
+ process.stdout.write(formatJSON(entries.map((e) => e.raw)) + "\n");
1540
+ return;
1541
+ }
1542
+ if (entries.length === 0) {
1543
+ process.stdout.write("No entries found.\n");
1544
+ return;
1545
+ }
1546
+ const displayColumns = columns ?? autoSelectColumns(schema, entries);
1547
+ const headers = displayColumns.map((c2) => c2.toUpperCase());
1548
+ const rows = entries.map(
1549
+ (entry) => displayColumns.map((col) => entry.properties[col] ?? "")
1550
+ );
1551
+ process.stdout.write(formatTable(rows, headers) + "\n");
1552
+ process.stderr.write(`${entries.length} entries
1553
+ `);
1554
+ }
1555
+ )
1556
+ );
1557
+ }
1558
+ function collect(value, previous) {
1559
+ return previous.concat([value]);
1560
+ }
1561
+
1562
+ // src/cli.ts
1563
+ var __filename = fileURLToPath(import.meta.url);
1564
+ var __dirname = dirname(__filename);
1565
+ var pkg = JSON.parse(readFileSync(join3(__dirname, "../package.json"), "utf-8"));
1566
+ var program = new Command14();
1567
+ program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
1568
+ 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");
1569
+ program.configureOutput({
1570
+ writeOut: (str) => process.stdout.write(str),
1571
+ writeErr: (str) => process.stderr.write(str),
1572
+ outputError: (str, write) => {
1573
+ write(str);
1574
+ }
1575
+ });
1576
+ program.hook("preAction", (thisCommand) => {
1577
+ const opts = thisCommand.opts();
1578
+ if (opts.color) {
1579
+ setColorForced(true);
1580
+ }
1581
+ if (opts.json) {
1582
+ setOutputMode("json");
1583
+ } else if (opts.md) {
1584
+ setOutputMode("md");
1585
+ }
1586
+ });
1587
+ program.addCommand(initCommand());
1588
+ var profileCmd = new Command14("profile").description("manage authentication profiles");
1589
+ profileCmd.addCommand(profileListCommand());
1590
+ profileCmd.addCommand(profileUseCommand());
1591
+ profileCmd.addCommand(profileRemoveCommand());
1592
+ program.addCommand(profileCmd);
1593
+ program.addCommand(searchCommand());
1594
+ program.addCommand(lsCommand());
1595
+ program.addCommand(openCommand());
1596
+ program.addCommand(usersCommand());
1597
+ program.addCommand(commentsCommand());
1598
+ program.addCommand(readCommand());
1599
+ var dbCmd = new Command14("db").description("Database operations");
1600
+ dbCmd.addCommand(dbSchemaCommand());
1601
+ dbCmd.addCommand(dbQueryCommand());
1602
+ program.addCommand(dbCmd);
1603
+ program.addCommand(completionCommand());
1604
+ await program.parseAsync();
1605
+ //# sourceMappingURL=cli.js.map