@andrzejchm/notion-cli 0.1.1 → 0.2.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/README.md +66 -6
- package/dist/cli.js +822 -46
- package/dist/cli.js.map +1 -1
- package/docs/README.agents.md +77 -0
- package/docs/demo.gif +0 -0
- package/docs/demo.tape +26 -0
- package/docs/notion-cli-icon.png +0 -0
- package/docs/skills/using-notion-cli/SKILL.md +186 -0
- package/package.json +2 -2
- package/docs/agent-skill.md +0 -474
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command20 } from "commander";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { dirname, join as join3 } from "path";
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -21,6 +21,9 @@ function createChalk() {
|
|
|
21
21
|
const level = isColorEnabled() ? void 0 : 0;
|
|
22
22
|
return new Chalk({ level });
|
|
23
23
|
}
|
|
24
|
+
function error(msg) {
|
|
25
|
+
return createChalk().red(msg);
|
|
26
|
+
}
|
|
24
27
|
function success(msg) {
|
|
25
28
|
return createChalk().green(msg);
|
|
26
29
|
}
|
|
@@ -32,7 +35,6 @@ function bold(msg) {
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
// src/output/format.ts
|
|
35
|
-
import { spawnSync } from "child_process";
|
|
36
38
|
var _mode = "auto";
|
|
37
39
|
function setOutputMode(mode) {
|
|
38
40
|
_mode = mode;
|
|
@@ -94,17 +96,7 @@ function printOutput(data, tableHeaders, tableRows) {
|
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
function printWithPager(text) {
|
|
97
|
-
|
|
98
|
-
process.stdout.write(text);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const result = spawnSync("less", ["-IR"], {
|
|
102
|
-
input: text,
|
|
103
|
-
stdio: ["pipe", "inherit", "inherit"]
|
|
104
|
-
});
|
|
105
|
-
if (result.error) {
|
|
106
|
-
process.stdout.write(text);
|
|
107
|
-
}
|
|
99
|
+
process.stdout.write(text);
|
|
108
100
|
}
|
|
109
101
|
|
|
110
102
|
// src/commands/init.ts
|
|
@@ -358,14 +350,468 @@ function initCommand() {
|
|
|
358
350
|
} catch {
|
|
359
351
|
stderrWrite(dim("(Could not verify integration access \u2014 run `notion ls` to check)"));
|
|
360
352
|
}
|
|
353
|
+
stderrWrite("");
|
|
354
|
+
stderrWrite(dim("Write commands (comment, append, create-page) require additional"));
|
|
355
|
+
stderrWrite(dim("capabilities in your integration settings:"));
|
|
356
|
+
stderrWrite(dim(" notion.so/profile/integrations/internal \u2192 your integration \u2192"));
|
|
357
|
+
stderrWrite(dim(' Capabilities: enable "Read content", "Insert content", "Read comments", "Insert comments"'));
|
|
358
|
+
stderrWrite("");
|
|
359
|
+
stderrWrite(dim("To post comments and create pages attributed to your user account:"));
|
|
360
|
+
stderrWrite(dim(" notion auth login"));
|
|
361
361
|
}));
|
|
362
362
|
return cmd;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
-
// src/commands/
|
|
365
|
+
// src/commands/auth/login.ts
|
|
366
366
|
import { Command as Command2 } from "commander";
|
|
367
|
+
|
|
368
|
+
// src/oauth/oauth-client.ts
|
|
369
|
+
var OAUTH_CLIENT_ID = "314d872b-594c-818d-8c02-0037a9d4be1f";
|
|
370
|
+
var OAUTH_CLIENT_SECRET = "secret_DfokG3sCdjylsYxIX26cSlFUadspf6D4Jr5gaVjO7xv";
|
|
371
|
+
var OAUTH_REDIRECT_URI = "http://localhost:54321/oauth/callback";
|
|
372
|
+
function buildAuthUrl(state) {
|
|
373
|
+
const params = new URLSearchParams({
|
|
374
|
+
client_id: OAUTH_CLIENT_ID,
|
|
375
|
+
redirect_uri: OAUTH_REDIRECT_URI,
|
|
376
|
+
response_type: "code",
|
|
377
|
+
owner: "user",
|
|
378
|
+
state
|
|
379
|
+
});
|
|
380
|
+
return `https://api.notion.com/v1/oauth/authorize?${params.toString()}`;
|
|
381
|
+
}
|
|
382
|
+
function basicAuth() {
|
|
383
|
+
return Buffer.from(`${OAUTH_CLIENT_ID}:${OAUTH_CLIENT_SECRET}`).toString("base64");
|
|
384
|
+
}
|
|
385
|
+
async function exchangeCode(code, redirectUri = OAUTH_REDIRECT_URI) {
|
|
386
|
+
const response = await fetch("https://api.notion.com/v1/oauth/token", {
|
|
387
|
+
method: "POST",
|
|
388
|
+
headers: {
|
|
389
|
+
"Authorization": `Basic ${basicAuth()}`,
|
|
390
|
+
"Content-Type": "application/json",
|
|
391
|
+
"Notion-Version": "2022-06-28"
|
|
392
|
+
},
|
|
393
|
+
body: JSON.stringify({
|
|
394
|
+
grant_type: "authorization_code",
|
|
395
|
+
code,
|
|
396
|
+
redirect_uri: redirectUri
|
|
397
|
+
})
|
|
398
|
+
});
|
|
399
|
+
if (!response.ok) {
|
|
400
|
+
let errorMessage = `OAuth token exchange failed (HTTP ${response.status})`;
|
|
401
|
+
try {
|
|
402
|
+
const body = await response.json();
|
|
403
|
+
if (body.error_description) errorMessage = body.error_description;
|
|
404
|
+
else if (body.error) errorMessage = body.error;
|
|
405
|
+
} catch {
|
|
406
|
+
}
|
|
407
|
+
throw new CliError(
|
|
408
|
+
ErrorCodes.AUTH_INVALID,
|
|
409
|
+
errorMessage,
|
|
410
|
+
'Run "notion auth login" to restart the OAuth flow'
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const data = await response.json();
|
|
414
|
+
return data;
|
|
415
|
+
}
|
|
416
|
+
async function refreshAccessToken(refreshToken) {
|
|
417
|
+
const response = await fetch("https://api.notion.com/v1/oauth/token", {
|
|
418
|
+
method: "POST",
|
|
419
|
+
headers: {
|
|
420
|
+
"Authorization": `Basic ${basicAuth()}`,
|
|
421
|
+
"Content-Type": "application/json",
|
|
422
|
+
"Notion-Version": "2022-06-28"
|
|
423
|
+
},
|
|
424
|
+
body: JSON.stringify({
|
|
425
|
+
grant_type: "refresh_token",
|
|
426
|
+
refresh_token: refreshToken
|
|
427
|
+
})
|
|
428
|
+
});
|
|
429
|
+
if (!response.ok) {
|
|
430
|
+
let errorMessage = `OAuth token refresh failed (HTTP ${response.status})`;
|
|
431
|
+
try {
|
|
432
|
+
const body = await response.json();
|
|
433
|
+
if (body.error_description) errorMessage = body.error_description;
|
|
434
|
+
else if (body.error) errorMessage = body.error;
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
throw new CliError(
|
|
438
|
+
ErrorCodes.AUTH_INVALID,
|
|
439
|
+
errorMessage,
|
|
440
|
+
'Run "notion auth login" to re-authenticate'
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
const data = await response.json();
|
|
444
|
+
return data;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/oauth/oauth-flow.ts
|
|
448
|
+
import { createServer } from "http";
|
|
449
|
+
import { randomBytes } from "crypto";
|
|
450
|
+
import { spawn } from "child_process";
|
|
451
|
+
import { createInterface } from "readline";
|
|
452
|
+
function openBrowser(url) {
|
|
453
|
+
const platform = process.platform;
|
|
454
|
+
let cmd;
|
|
455
|
+
let args;
|
|
456
|
+
if (platform === "darwin") {
|
|
457
|
+
cmd = "open";
|
|
458
|
+
args = [url];
|
|
459
|
+
} else if (platform === "win32") {
|
|
460
|
+
cmd = "cmd";
|
|
461
|
+
args = ["/c", "start", url];
|
|
462
|
+
} else {
|
|
463
|
+
cmd = "xdg-open";
|
|
464
|
+
args = [url];
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
const child = spawn(cmd, args, {
|
|
468
|
+
detached: true,
|
|
469
|
+
stdio: "ignore"
|
|
470
|
+
});
|
|
471
|
+
child.unref();
|
|
472
|
+
return true;
|
|
473
|
+
} catch {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async function manualFlow(url) {
|
|
478
|
+
process.stderr.write(
|
|
479
|
+
`
|
|
480
|
+
Opening browser to:
|
|
481
|
+
${url}
|
|
482
|
+
|
|
483
|
+
Paste the full redirect URL here (${OAUTH_REDIRECT_URI}?code=...):
|
|
484
|
+
> `
|
|
485
|
+
);
|
|
486
|
+
const rl = createInterface({
|
|
487
|
+
input: process.stdin,
|
|
488
|
+
output: process.stderr,
|
|
489
|
+
terminal: false
|
|
490
|
+
});
|
|
491
|
+
return new Promise((resolve, reject) => {
|
|
492
|
+
rl.once("line", (line) => {
|
|
493
|
+
rl.close();
|
|
494
|
+
try {
|
|
495
|
+
const parsed = new URL(line.trim());
|
|
496
|
+
const code = parsed.searchParams.get("code");
|
|
497
|
+
const state = parsed.searchParams.get("state");
|
|
498
|
+
const errorParam = parsed.searchParams.get("error");
|
|
499
|
+
if (errorParam === "access_denied") {
|
|
500
|
+
reject(
|
|
501
|
+
new CliError(
|
|
502
|
+
ErrorCodes.AUTH_INVALID,
|
|
503
|
+
"Notion OAuth access was denied.",
|
|
504
|
+
'Run "notion auth login" to try again'
|
|
505
|
+
)
|
|
506
|
+
);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (!code || !state) {
|
|
510
|
+
reject(
|
|
511
|
+
new CliError(
|
|
512
|
+
ErrorCodes.AUTH_INVALID,
|
|
513
|
+
"Invalid redirect URL \u2014 missing code or state parameter.",
|
|
514
|
+
"Make sure you paste the full redirect URL from the browser address bar"
|
|
515
|
+
)
|
|
516
|
+
);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
resolve({ code, state });
|
|
520
|
+
} catch {
|
|
521
|
+
reject(
|
|
522
|
+
new CliError(
|
|
523
|
+
ErrorCodes.AUTH_INVALID,
|
|
524
|
+
"Could not parse the pasted URL.",
|
|
525
|
+
"Make sure you paste the full redirect URL from the browser address bar"
|
|
526
|
+
)
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
rl.once("close", () => {
|
|
531
|
+
reject(
|
|
532
|
+
new CliError(
|
|
533
|
+
ErrorCodes.AUTH_INVALID,
|
|
534
|
+
"No redirect URL received.",
|
|
535
|
+
'Run "notion auth login" to try again'
|
|
536
|
+
)
|
|
537
|
+
);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
async function runOAuthFlow(options) {
|
|
542
|
+
const state = randomBytes(16).toString("hex");
|
|
543
|
+
const authUrl = buildAuthUrl(state);
|
|
544
|
+
if (options?.manual) {
|
|
545
|
+
return manualFlow(authUrl);
|
|
546
|
+
}
|
|
547
|
+
return new Promise((resolve, reject) => {
|
|
548
|
+
let settled = false;
|
|
549
|
+
let timeoutHandle = null;
|
|
550
|
+
const server = createServer((req, res) => {
|
|
551
|
+
if (settled) {
|
|
552
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
553
|
+
res.end("<html><body><h1>Already handled. You can close this tab.</h1></body></html>");
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
const reqUrl = new URL(req.url ?? "/", `http://localhost:54321`);
|
|
558
|
+
const code = reqUrl.searchParams.get("code");
|
|
559
|
+
const returnedState = reqUrl.searchParams.get("state");
|
|
560
|
+
const errorParam = reqUrl.searchParams.get("error");
|
|
561
|
+
if (errorParam === "access_denied") {
|
|
562
|
+
settled = true;
|
|
563
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
564
|
+
res.end(
|
|
565
|
+
"<html><body><h1>Access Denied</h1><p>You cancelled the Notion OAuth request. You can close this tab.</p></body></html>"
|
|
566
|
+
);
|
|
567
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
568
|
+
server.close(() => {
|
|
569
|
+
reject(
|
|
570
|
+
new CliError(
|
|
571
|
+
ErrorCodes.AUTH_INVALID,
|
|
572
|
+
"Notion OAuth access was denied.",
|
|
573
|
+
'Run "notion auth login" to try again'
|
|
574
|
+
)
|
|
575
|
+
);
|
|
576
|
+
});
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (!code || !returnedState) {
|
|
580
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
581
|
+
res.end("<html><body><p>Waiting for OAuth callback...</p></body></html>");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (returnedState !== state) {
|
|
585
|
+
settled = true;
|
|
586
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
587
|
+
res.end(
|
|
588
|
+
"<html><body><h1>Security Error</h1><p>State mismatch \u2014 possible CSRF attempt. You can close this tab.</p></body></html>"
|
|
589
|
+
);
|
|
590
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
591
|
+
server.close(() => {
|
|
592
|
+
reject(
|
|
593
|
+
new CliError(
|
|
594
|
+
ErrorCodes.AUTH_INVALID,
|
|
595
|
+
"OAuth state mismatch \u2014 possible CSRF attempt. Aborting.",
|
|
596
|
+
'Run "notion auth login" to start a fresh OAuth flow'
|
|
597
|
+
)
|
|
598
|
+
);
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
settled = true;
|
|
603
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
604
|
+
res.end(
|
|
605
|
+
"<html><body><h1>Authenticated!</h1><p>You can close this tab and return to the terminal.</p></body></html>"
|
|
606
|
+
);
|
|
607
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
608
|
+
server.close(() => {
|
|
609
|
+
resolve({ code, state: returnedState });
|
|
610
|
+
});
|
|
611
|
+
} catch {
|
|
612
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
613
|
+
res.end("<html><body><h1>Error processing callback</h1></body></html>");
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
server.on("error", (err) => {
|
|
617
|
+
if (settled) return;
|
|
618
|
+
settled = true;
|
|
619
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
620
|
+
reject(
|
|
621
|
+
new CliError(
|
|
622
|
+
ErrorCodes.AUTH_INVALID,
|
|
623
|
+
`Failed to start OAuth callback server: ${err.message}`,
|
|
624
|
+
"Make sure port 54321 is not in use, or use --manual flag"
|
|
625
|
+
)
|
|
626
|
+
);
|
|
627
|
+
});
|
|
628
|
+
server.listen(54321, "127.0.0.1", () => {
|
|
629
|
+
const browserOpened = openBrowser(authUrl);
|
|
630
|
+
if (!browserOpened) {
|
|
631
|
+
server.close();
|
|
632
|
+
settled = true;
|
|
633
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
634
|
+
manualFlow(authUrl).then(resolve, reject);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
process.stderr.write(
|
|
638
|
+
`
|
|
639
|
+
Opening browser for Notion OAuth...
|
|
640
|
+
If your browser didn't open, visit:
|
|
641
|
+
${authUrl}
|
|
642
|
+
|
|
643
|
+
Waiting for callback (up to 120 seconds)...
|
|
644
|
+
`
|
|
645
|
+
);
|
|
646
|
+
timeoutHandle = setTimeout(() => {
|
|
647
|
+
if (settled) return;
|
|
648
|
+
settled = true;
|
|
649
|
+
server.close(() => {
|
|
650
|
+
reject(
|
|
651
|
+
new CliError(
|
|
652
|
+
ErrorCodes.AUTH_INVALID,
|
|
653
|
+
"OAuth login timed out after 120 seconds.",
|
|
654
|
+
'Run "notion auth login" to try again, or use --manual flag'
|
|
655
|
+
)
|
|
656
|
+
);
|
|
657
|
+
});
|
|
658
|
+
}, 12e4);
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/oauth/token-store.ts
|
|
664
|
+
var OAUTH_EXPIRY_DURATION_MS = 60 * 60 * 1e3;
|
|
665
|
+
async function saveOAuthTokens(profileName, response) {
|
|
666
|
+
const config = await readGlobalConfig();
|
|
667
|
+
const existing = config.profiles?.[profileName] ?? {};
|
|
668
|
+
const updatedProfile = {
|
|
669
|
+
...existing,
|
|
670
|
+
oauth_access_token: response.access_token,
|
|
671
|
+
oauth_refresh_token: response.refresh_token,
|
|
672
|
+
oauth_expiry_ms: Date.now() + OAUTH_EXPIRY_DURATION_MS,
|
|
673
|
+
workspace_id: response.workspace_id,
|
|
674
|
+
workspace_name: response.workspace_name,
|
|
675
|
+
...response.owner?.user?.id != null && { oauth_user_id: response.owner.user.id },
|
|
676
|
+
...response.owner?.user?.name != null && { oauth_user_name: response.owner.user.name }
|
|
677
|
+
};
|
|
678
|
+
config.profiles = {
|
|
679
|
+
...config.profiles,
|
|
680
|
+
[profileName]: updatedProfile
|
|
681
|
+
};
|
|
682
|
+
await writeGlobalConfig(config);
|
|
683
|
+
}
|
|
684
|
+
async function clearOAuthTokens(profileName) {
|
|
685
|
+
const config = await readGlobalConfig();
|
|
686
|
+
const existing = config.profiles?.[profileName];
|
|
687
|
+
if (!existing) return;
|
|
688
|
+
const {
|
|
689
|
+
oauth_access_token: _access,
|
|
690
|
+
oauth_refresh_token: _refresh,
|
|
691
|
+
oauth_expiry_ms: _expiry,
|
|
692
|
+
oauth_user_id: _userId,
|
|
693
|
+
oauth_user_name: _userName,
|
|
694
|
+
...rest
|
|
695
|
+
} = existing;
|
|
696
|
+
config.profiles = {
|
|
697
|
+
...config.profiles,
|
|
698
|
+
[profileName]: rest
|
|
699
|
+
};
|
|
700
|
+
await writeGlobalConfig(config);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/commands/auth/login.ts
|
|
704
|
+
function loginCommand() {
|
|
705
|
+
const cmd = new Command2("login");
|
|
706
|
+
cmd.description("authenticate with Notion via OAuth browser flow").option("--profile <name>", "profile name to store credentials in").option("--manual", "print auth URL instead of opening browser (for headless environments)").action(
|
|
707
|
+
withErrorHandling(async (opts) => {
|
|
708
|
+
if (!process.stdin.isTTY && !opts.manual) {
|
|
709
|
+
throw new CliError(
|
|
710
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
711
|
+
"Cannot run interactive OAuth login in non-TTY mode.",
|
|
712
|
+
"Use --manual flag to get an auth URL you can open in a browser"
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
let profileName = opts.profile;
|
|
716
|
+
if (!profileName) {
|
|
717
|
+
const config = await readGlobalConfig();
|
|
718
|
+
profileName = config.active_profile ?? "default";
|
|
719
|
+
}
|
|
720
|
+
const result = await runOAuthFlow({ manual: opts.manual });
|
|
721
|
+
const response = await exchangeCode(result.code);
|
|
722
|
+
await saveOAuthTokens(profileName, response);
|
|
723
|
+
const userName = response.owner?.user?.name ?? "unknown user";
|
|
724
|
+
const workspaceName = response.workspace_name ?? "unknown workspace";
|
|
725
|
+
stderrWrite(success(`\u2713 Logged in as ${userName} to workspace ${workspaceName}`));
|
|
726
|
+
stderrWrite(dim("Your comments and pages will now be attributed to your Notion account."));
|
|
727
|
+
})
|
|
728
|
+
);
|
|
729
|
+
return cmd;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/commands/auth/logout.ts
|
|
733
|
+
import { Command as Command3 } from "commander";
|
|
734
|
+
function logoutCommand() {
|
|
735
|
+
const cmd = new Command3("logout");
|
|
736
|
+
cmd.description("remove OAuth tokens from the active profile").option("--profile <name>", "profile name to log out from").action(
|
|
737
|
+
withErrorHandling(async (opts) => {
|
|
738
|
+
let profileName = opts.profile;
|
|
739
|
+
if (!profileName) {
|
|
740
|
+
const config2 = await readGlobalConfig();
|
|
741
|
+
profileName = config2.active_profile ?? "default";
|
|
742
|
+
}
|
|
743
|
+
const config = await readGlobalConfig();
|
|
744
|
+
const profile = config.profiles?.[profileName];
|
|
745
|
+
if (!profile?.oauth_access_token) {
|
|
746
|
+
stderrWrite(`No OAuth session found for profile '${profileName}'.`);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
await clearOAuthTokens(profileName);
|
|
750
|
+
stderrWrite(success(`\u2713 Logged out. OAuth tokens removed from profile '${profileName}'.`));
|
|
751
|
+
stderrWrite(
|
|
752
|
+
dim(
|
|
753
|
+
`Internal integration token (if any) is still active. Run 'notion init' to change it.`
|
|
754
|
+
)
|
|
755
|
+
);
|
|
756
|
+
})
|
|
757
|
+
);
|
|
758
|
+
return cmd;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/commands/auth/status.ts
|
|
762
|
+
import { Command as Command4 } from "commander";
|
|
763
|
+
function statusCommand() {
|
|
764
|
+
const cmd = new Command4("status");
|
|
765
|
+
cmd.description("show authentication status for the active profile").option("--profile <name>", "profile name to check").action(
|
|
766
|
+
withErrorHandling(async (opts) => {
|
|
767
|
+
let profileName = opts.profile;
|
|
768
|
+
const config = await readGlobalConfig();
|
|
769
|
+
if (!profileName) {
|
|
770
|
+
profileName = config.active_profile ?? "default";
|
|
771
|
+
}
|
|
772
|
+
const profile = config.profiles?.[profileName];
|
|
773
|
+
stderrWrite(`Profile: ${profileName}`);
|
|
774
|
+
if (!profile) {
|
|
775
|
+
stderrWrite(` ${error("\u2717")} No profile found (run 'notion init' to create one)`);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (profile.oauth_access_token) {
|
|
779
|
+
const userName = profile.oauth_user_name ?? "unknown";
|
|
780
|
+
const userId = profile.oauth_user_id ?? "unknown";
|
|
781
|
+
stderrWrite(` OAuth: ${success("\u2713")} Logged in as ${userName} (user: ${userId})`);
|
|
782
|
+
if (profile.oauth_expiry_ms != null) {
|
|
783
|
+
const expiryDate = new Date(profile.oauth_expiry_ms).toISOString();
|
|
784
|
+
stderrWrite(dim(` Access token expires: ${expiryDate}`));
|
|
785
|
+
}
|
|
786
|
+
} else {
|
|
787
|
+
stderrWrite(
|
|
788
|
+
` OAuth: ${error("\u2717")} Not logged in (run 'notion auth login')`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
if (profile.token) {
|
|
792
|
+
const tokenPreview = profile.token.substring(0, 10) + "...";
|
|
793
|
+
stderrWrite(` Internal token: ${success("\u2713")} Configured (${tokenPreview})`);
|
|
794
|
+
} else {
|
|
795
|
+
stderrWrite(` Internal token: ${error("\u2717")} Not configured`);
|
|
796
|
+
}
|
|
797
|
+
if (profile.oauth_access_token) {
|
|
798
|
+
stderrWrite(` Active method: OAuth (user-attributed)`);
|
|
799
|
+
} else if (profile.token) {
|
|
800
|
+
stderrWrite(` Active method: Internal integration token (bot-attributed)`);
|
|
801
|
+
} else {
|
|
802
|
+
stderrWrite(
|
|
803
|
+
dim(` Active method: None (run 'notion auth login' or 'notion init')`)
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
})
|
|
807
|
+
);
|
|
808
|
+
return cmd;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/commands/profile/list.ts
|
|
812
|
+
import { Command as Command5 } from "commander";
|
|
367
813
|
function profileListCommand() {
|
|
368
|
-
const cmd = new
|
|
814
|
+
const cmd = new Command5("list");
|
|
369
815
|
cmd.description("list all authentication profiles").action(withErrorHandling(async () => {
|
|
370
816
|
const config = await readGlobalConfig();
|
|
371
817
|
const profiles = config.profiles ?? {};
|
|
@@ -388,9 +834,9 @@ function profileListCommand() {
|
|
|
388
834
|
}
|
|
389
835
|
|
|
390
836
|
// src/commands/profile/use.ts
|
|
391
|
-
import { Command as
|
|
837
|
+
import { Command as Command6 } from "commander";
|
|
392
838
|
function profileUseCommand() {
|
|
393
|
-
const cmd = new
|
|
839
|
+
const cmd = new Command6("use");
|
|
394
840
|
cmd.description("switch the active profile").argument("<name>", "profile name to activate").action(withErrorHandling(async (name) => {
|
|
395
841
|
const config = await readGlobalConfig();
|
|
396
842
|
const profiles = config.profiles ?? {};
|
|
@@ -411,9 +857,9 @@ function profileUseCommand() {
|
|
|
411
857
|
}
|
|
412
858
|
|
|
413
859
|
// src/commands/profile/remove.ts
|
|
414
|
-
import { Command as
|
|
860
|
+
import { Command as Command7 } from "commander";
|
|
415
861
|
function profileRemoveCommand() {
|
|
416
|
-
const cmd = new
|
|
862
|
+
const cmd = new Command7("remove");
|
|
417
863
|
cmd.description("remove an authentication profile").argument("<name>", "profile name to remove").action(withErrorHandling(async (name) => {
|
|
418
864
|
const config = await readGlobalConfig();
|
|
419
865
|
const profiles = { ...config.profiles ?? {} };
|
|
@@ -437,7 +883,7 @@ function profileRemoveCommand() {
|
|
|
437
883
|
}
|
|
438
884
|
|
|
439
885
|
// src/commands/completion.ts
|
|
440
|
-
import { Command as
|
|
886
|
+
import { Command as Command8 } from "commander";
|
|
441
887
|
var BASH_COMPLETION = `# notion bash completion
|
|
442
888
|
_notion_completion() {
|
|
443
889
|
local cur prev words cword
|
|
@@ -539,7 +985,7 @@ complete -c notion -n '__fish_seen_subcommand_from completion' -a zsh -d 'zsh co
|
|
|
539
985
|
complete -c notion -n '__fish_seen_subcommand_from completion' -a fish -d 'fish completion script'
|
|
540
986
|
`;
|
|
541
987
|
function completionCommand() {
|
|
542
|
-
const cmd = new
|
|
988
|
+
const cmd = new Command8("completion");
|
|
543
989
|
cmd.description("output shell completion script").argument("<shell>", "shell type (bash, zsh, fish)").action(withErrorHandling(async (shell) => {
|
|
544
990
|
switch (shell) {
|
|
545
991
|
case "bash":
|
|
@@ -563,7 +1009,7 @@ function completionCommand() {
|
|
|
563
1009
|
}
|
|
564
1010
|
|
|
565
1011
|
// src/commands/search.ts
|
|
566
|
-
import { Command as
|
|
1012
|
+
import { Command as Command9 } from "commander";
|
|
567
1013
|
import { isFullPageOrDataSource } from "@notionhq/client";
|
|
568
1014
|
|
|
569
1015
|
// src/config/local-config.ts
|
|
@@ -606,6 +1052,36 @@ async function readLocalConfig() {
|
|
|
606
1052
|
}
|
|
607
1053
|
|
|
608
1054
|
// src/config/token.ts
|
|
1055
|
+
function isOAuthExpired(profile) {
|
|
1056
|
+
if (profile.oauth_expiry_ms == null) return false;
|
|
1057
|
+
return Date.now() >= profile.oauth_expiry_ms;
|
|
1058
|
+
}
|
|
1059
|
+
async function resolveOAuthToken(profileName, profile) {
|
|
1060
|
+
if (!profile.oauth_access_token) return null;
|
|
1061
|
+
if (!isOAuthExpired(profile)) {
|
|
1062
|
+
return profile.oauth_access_token;
|
|
1063
|
+
}
|
|
1064
|
+
if (!profile.oauth_refresh_token) {
|
|
1065
|
+
await clearOAuthTokens(profileName);
|
|
1066
|
+
throw new CliError(
|
|
1067
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
1068
|
+
"OAuth session expired and no refresh token is available.",
|
|
1069
|
+
'Run "notion auth login" to re-authenticate'
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
try {
|
|
1073
|
+
const refreshed = await refreshAccessToken(profile.oauth_refresh_token);
|
|
1074
|
+
await saveOAuthTokens(profileName, refreshed);
|
|
1075
|
+
return refreshed.access_token;
|
|
1076
|
+
} catch {
|
|
1077
|
+
await clearOAuthTokens(profileName);
|
|
1078
|
+
throw new CliError(
|
|
1079
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
1080
|
+
'OAuth session expired. Run "notion auth login" to re-authenticate.',
|
|
1081
|
+
"Your session was revoked or the refresh token has expired"
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
609
1085
|
async function resolveToken() {
|
|
610
1086
|
const envToken = process.env["NOTION_API_TOKEN"];
|
|
611
1087
|
if (envToken) {
|
|
@@ -618,17 +1094,29 @@ async function resolveToken() {
|
|
|
618
1094
|
}
|
|
619
1095
|
if (localConfig.profile) {
|
|
620
1096
|
const globalConfig2 = await readGlobalConfig();
|
|
621
|
-
const
|
|
622
|
-
if (
|
|
623
|
-
|
|
1097
|
+
const profile = globalConfig2.profiles?.[localConfig.profile];
|
|
1098
|
+
if (profile) {
|
|
1099
|
+
const oauthToken = await resolveOAuthToken(localConfig.profile, profile);
|
|
1100
|
+
if (oauthToken) {
|
|
1101
|
+
return { token: oauthToken, source: "oauth" };
|
|
1102
|
+
}
|
|
1103
|
+
if (profile.token) {
|
|
1104
|
+
return { token: profile.token, source: `profile: ${localConfig.profile}` };
|
|
1105
|
+
}
|
|
624
1106
|
}
|
|
625
1107
|
}
|
|
626
1108
|
}
|
|
627
1109
|
const globalConfig = await readGlobalConfig();
|
|
628
1110
|
if (globalConfig.active_profile) {
|
|
629
|
-
const
|
|
630
|
-
if (
|
|
631
|
-
|
|
1111
|
+
const profile = globalConfig.profiles?.[globalConfig.active_profile];
|
|
1112
|
+
if (profile) {
|
|
1113
|
+
const oauthToken = await resolveOAuthToken(globalConfig.active_profile, profile);
|
|
1114
|
+
if (oauthToken) {
|
|
1115
|
+
return { token: oauthToken, source: "oauth" };
|
|
1116
|
+
}
|
|
1117
|
+
if (profile.token) {
|
|
1118
|
+
return { token: profile.token, source: `profile: ${globalConfig.active_profile}` };
|
|
1119
|
+
}
|
|
632
1120
|
}
|
|
633
1121
|
}
|
|
634
1122
|
throw new CliError(
|
|
@@ -656,7 +1144,7 @@ function displayType(item) {
|
|
|
656
1144
|
return item.object === "data_source" ? "database" : item.object;
|
|
657
1145
|
}
|
|
658
1146
|
function searchCommand() {
|
|
659
|
-
const cmd = new
|
|
1147
|
+
const cmd = new Command9("search");
|
|
660
1148
|
cmd.description("search Notion workspace by keyword").argument("<query>", "search keyword").option("--type <type>", "filter by object type (page or database)", (val) => {
|
|
661
1149
|
if (val !== "page" && val !== "database") {
|
|
662
1150
|
throw new Error('--type must be "page" or "database"');
|
|
@@ -701,7 +1189,7 @@ function searchCommand() {
|
|
|
701
1189
|
}
|
|
702
1190
|
|
|
703
1191
|
// src/commands/ls.ts
|
|
704
|
-
import { Command as
|
|
1192
|
+
import { Command as Command10 } from "commander";
|
|
705
1193
|
import { isFullPageOrDataSource as isFullPageOrDataSource2 } from "@notionhq/client";
|
|
706
1194
|
function getTitle2(item) {
|
|
707
1195
|
if (item.object === "data_source") {
|
|
@@ -717,7 +1205,7 @@ function displayType2(item) {
|
|
|
717
1205
|
return item.object === "data_source" ? "database" : item.object;
|
|
718
1206
|
}
|
|
719
1207
|
function lsCommand() {
|
|
720
|
-
const cmd = new
|
|
1208
|
+
const cmd = new Command10("ls");
|
|
721
1209
|
cmd.description("list accessible Notion pages and databases").option("--type <type>", "filter by object type (page or database)", (val) => {
|
|
722
1210
|
if (val !== "page" && val !== "database") {
|
|
723
1211
|
throw new Error('--type must be "page" or "database"');
|
|
@@ -767,7 +1255,7 @@ function lsCommand() {
|
|
|
767
1255
|
// src/commands/open.ts
|
|
768
1256
|
import { exec } from "child_process";
|
|
769
1257
|
import { promisify } from "util";
|
|
770
|
-
import { Command as
|
|
1258
|
+
import { Command as Command11 } from "commander";
|
|
771
1259
|
|
|
772
1260
|
// src/notion/url-parser.ts
|
|
773
1261
|
var NOTION_ID_REGEX = /^[0-9a-f]{32}$/i;
|
|
@@ -801,7 +1289,7 @@ function toUuid(id) {
|
|
|
801
1289
|
// src/commands/open.ts
|
|
802
1290
|
var execAsync = promisify(exec);
|
|
803
1291
|
function openCommand() {
|
|
804
|
-
const cmd = new
|
|
1292
|
+
const cmd = new Command11("open");
|
|
805
1293
|
cmd.description("open a Notion page in the default browser").argument("<id/url>", "Notion page ID or URL").action(withErrorHandling(async (idOrUrl) => {
|
|
806
1294
|
const id = parseNotionId(idOrUrl);
|
|
807
1295
|
const url = `https://www.notion.so/${id}`;
|
|
@@ -815,7 +1303,7 @@ function openCommand() {
|
|
|
815
1303
|
}
|
|
816
1304
|
|
|
817
1305
|
// src/commands/users.ts
|
|
818
|
-
import { Command as
|
|
1306
|
+
import { Command as Command12 } from "commander";
|
|
819
1307
|
|
|
820
1308
|
// src/output/paginate.ts
|
|
821
1309
|
async function paginateResults(fetcher) {
|
|
@@ -843,7 +1331,7 @@ function getEmailOrWorkspace(user) {
|
|
|
843
1331
|
return "\u2014";
|
|
844
1332
|
}
|
|
845
1333
|
function usersCommand() {
|
|
846
|
-
const cmd = new
|
|
1334
|
+
const cmd = new Command12("users");
|
|
847
1335
|
cmd.description("list all users in the workspace").option("--json", "output as JSON").action(withErrorHandling(async (opts) => {
|
|
848
1336
|
if (opts.json) setOutputMode("json");
|
|
849
1337
|
const { token, source } = await resolveToken();
|
|
@@ -865,9 +1353,9 @@ function usersCommand() {
|
|
|
865
1353
|
}
|
|
866
1354
|
|
|
867
1355
|
// src/commands/comments.ts
|
|
868
|
-
import { Command as
|
|
1356
|
+
import { Command as Command13 } from "commander";
|
|
869
1357
|
function commentsCommand() {
|
|
870
|
-
const cmd = new
|
|
1358
|
+
const cmd = new Command13("comments");
|
|
871
1359
|
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) => {
|
|
872
1360
|
if (opts.json) setOutputMode("json");
|
|
873
1361
|
const id = parseNotionId(idOrUrl);
|
|
@@ -896,7 +1384,7 @@ function commentsCommand() {
|
|
|
896
1384
|
}
|
|
897
1385
|
|
|
898
1386
|
// src/commands/read.ts
|
|
899
|
-
import { Command as
|
|
1387
|
+
import { Command as Command14 } from "commander";
|
|
900
1388
|
|
|
901
1389
|
// src/services/page.service.ts
|
|
902
1390
|
import { collectPaginatedAPI, isFullBlock } from "@notionhq/client";
|
|
@@ -1292,7 +1780,7 @@ function renderInline(text) {
|
|
|
1292
1780
|
|
|
1293
1781
|
// src/commands/read.ts
|
|
1294
1782
|
function readCommand() {
|
|
1295
|
-
return new
|
|
1783
|
+
return new Command14("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(
|
|
1296
1784
|
withErrorHandling(async (id, options) => {
|
|
1297
1785
|
const { token } = await resolveToken();
|
|
1298
1786
|
const client = createNotionClient(token);
|
|
@@ -1313,7 +1801,7 @@ function readCommand() {
|
|
|
1313
1801
|
}
|
|
1314
1802
|
|
|
1315
1803
|
// src/commands/db/schema.ts
|
|
1316
|
-
import { Command as
|
|
1804
|
+
import { Command as Command15 } from "commander";
|
|
1317
1805
|
|
|
1318
1806
|
// src/services/database.service.ts
|
|
1319
1807
|
import { isFullPage as isFullPage2 } from "@notionhq/client";
|
|
@@ -1476,7 +1964,7 @@ function displayPropertyValue(prop) {
|
|
|
1476
1964
|
|
|
1477
1965
|
// src/commands/db/schema.ts
|
|
1478
1966
|
function dbSchemaCommand() {
|
|
1479
|
-
return new
|
|
1967
|
+
return new Command15("schema").description("Show database schema (property names, types, and options)").argument("<id>", "Notion database ID or URL").option("--json", "Output raw JSON").action(
|
|
1480
1968
|
withErrorHandling(async (id, options) => {
|
|
1481
1969
|
const { token } = await resolveToken();
|
|
1482
1970
|
const client = createNotionClient(token);
|
|
@@ -1498,7 +1986,7 @@ function dbSchemaCommand() {
|
|
|
1498
1986
|
}
|
|
1499
1987
|
|
|
1500
1988
|
// src/commands/db/query.ts
|
|
1501
|
-
import { Command as
|
|
1989
|
+
import { Command as Command16 } from "commander";
|
|
1502
1990
|
var SKIP_TYPES_IN_AUTO = /* @__PURE__ */ new Set(["relation", "rich_text", "people"]);
|
|
1503
1991
|
function autoSelectColumns(schema, entries) {
|
|
1504
1992
|
const termWidth = process.stdout.columns || 120;
|
|
@@ -1523,7 +2011,7 @@ function autoSelectColumns(schema, entries) {
|
|
|
1523
2011
|
return selected;
|
|
1524
2012
|
}
|
|
1525
2013
|
function dbQueryCommand() {
|
|
1526
|
-
return new
|
|
2014
|
+
return new Command16("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(
|
|
1527
2015
|
withErrorHandling(
|
|
1528
2016
|
async (id, options) => {
|
|
1529
2017
|
const { token } = await resolveToken();
|
|
@@ -1558,11 +2046,291 @@ function collect(value, previous) {
|
|
|
1558
2046
|
return previous.concat([value]);
|
|
1559
2047
|
}
|
|
1560
2048
|
|
|
2049
|
+
// src/commands/comment-add.ts
|
|
2050
|
+
import { Command as Command17 } from "commander";
|
|
2051
|
+
|
|
2052
|
+
// src/services/write.service.ts
|
|
2053
|
+
async function addComment(client, pageId, text, options = {}) {
|
|
2054
|
+
await client.comments.create({
|
|
2055
|
+
parent: { page_id: pageId },
|
|
2056
|
+
rich_text: [
|
|
2057
|
+
{
|
|
2058
|
+
type: "text",
|
|
2059
|
+
text: { content: text, link: null },
|
|
2060
|
+
annotations: {
|
|
2061
|
+
bold: false,
|
|
2062
|
+
italic: false,
|
|
2063
|
+
strikethrough: false,
|
|
2064
|
+
underline: false,
|
|
2065
|
+
code: false,
|
|
2066
|
+
color: "default"
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
],
|
|
2070
|
+
...options.asUser && { display_name: { type: "user" } }
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
async function appendBlocks(client, blockId, blocks) {
|
|
2074
|
+
await client.blocks.children.append({
|
|
2075
|
+
block_id: blockId,
|
|
2076
|
+
children: blocks
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
async function createPage(client, parentId, title, blocks) {
|
|
2080
|
+
const response = await client.pages.create({
|
|
2081
|
+
parent: { type: "page_id", page_id: parentId },
|
|
2082
|
+
properties: {
|
|
2083
|
+
title: {
|
|
2084
|
+
title: [{ type: "text", text: { content: title, link: null } }]
|
|
2085
|
+
}
|
|
2086
|
+
},
|
|
2087
|
+
children: blocks
|
|
2088
|
+
});
|
|
2089
|
+
return response.url;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// src/commands/comment-add.ts
|
|
2093
|
+
function commentAddCommand() {
|
|
2094
|
+
const cmd = new Command17("comment");
|
|
2095
|
+
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(withErrorHandling(async (idOrUrl, opts) => {
|
|
2096
|
+
const { token, source } = await resolveToken();
|
|
2097
|
+
reportTokenSource(source);
|
|
2098
|
+
const client = createNotionClient(token);
|
|
2099
|
+
const id = parseNotionId(idOrUrl);
|
|
2100
|
+
const uuid = toUuid(id);
|
|
2101
|
+
await addComment(client, uuid, opts.message, { asUser: source === "oauth" });
|
|
2102
|
+
process.stdout.write("Comment added.\n");
|
|
2103
|
+
}));
|
|
2104
|
+
return cmd;
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// src/commands/append.ts
|
|
2108
|
+
import { Command as Command18 } from "commander";
|
|
2109
|
+
|
|
2110
|
+
// src/blocks/md-to-blocks.ts
|
|
2111
|
+
var INLINE_RE = /(\*\*[^*]+\*\*|_[^_]+_|\*[^*]+\*|`[^`]+`|\[[^\]]+\]\([^)]+\)|[^*_`\[]+)/g;
|
|
2112
|
+
function parseInlineMarkdown(text) {
|
|
2113
|
+
const result = [];
|
|
2114
|
+
let match;
|
|
2115
|
+
INLINE_RE.lastIndex = 0;
|
|
2116
|
+
while ((match = INLINE_RE.exec(text)) !== null) {
|
|
2117
|
+
const segment = match[0];
|
|
2118
|
+
if (segment.startsWith("**") && segment.endsWith("**")) {
|
|
2119
|
+
const content = segment.slice(2, -2);
|
|
2120
|
+
result.push({
|
|
2121
|
+
type: "text",
|
|
2122
|
+
text: { content, link: null },
|
|
2123
|
+
annotations: { bold: true, italic: false, strikethrough: false, underline: false, code: false, color: "default" }
|
|
2124
|
+
});
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
2127
|
+
if (segment.startsWith("_") && segment.endsWith("_") || segment.startsWith("*") && segment.endsWith("*")) {
|
|
2128
|
+
const content = segment.slice(1, -1);
|
|
2129
|
+
result.push({
|
|
2130
|
+
type: "text",
|
|
2131
|
+
text: { content, link: null },
|
|
2132
|
+
annotations: { bold: false, italic: true, strikethrough: false, underline: false, code: false, color: "default" }
|
|
2133
|
+
});
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
if (segment.startsWith("`") && segment.endsWith("`")) {
|
|
2137
|
+
const content = segment.slice(1, -1);
|
|
2138
|
+
result.push({
|
|
2139
|
+
type: "text",
|
|
2140
|
+
text: { content, link: null },
|
|
2141
|
+
annotations: { bold: false, italic: false, strikethrough: false, underline: false, code: true, color: "default" }
|
|
2142
|
+
});
|
|
2143
|
+
continue;
|
|
2144
|
+
}
|
|
2145
|
+
const linkMatch = segment.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
2146
|
+
if (linkMatch) {
|
|
2147
|
+
const [, label, url] = linkMatch;
|
|
2148
|
+
result.push({
|
|
2149
|
+
type: "text",
|
|
2150
|
+
text: { content: label, link: { url } },
|
|
2151
|
+
annotations: { bold: false, italic: false, strikethrough: false, underline: false, code: false, color: "default" }
|
|
2152
|
+
});
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
result.push({
|
|
2156
|
+
type: "text",
|
|
2157
|
+
text: { content: segment, link: null },
|
|
2158
|
+
annotations: { bold: false, italic: false, strikethrough: false, underline: false, code: false, color: "default" }
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
return result;
|
|
2162
|
+
}
|
|
2163
|
+
function makeRichText(text) {
|
|
2164
|
+
return parseInlineMarkdown(text);
|
|
2165
|
+
}
|
|
2166
|
+
function mdToBlocks(md) {
|
|
2167
|
+
if (!md) return [];
|
|
2168
|
+
const lines = md.split("\n");
|
|
2169
|
+
const blocks = [];
|
|
2170
|
+
let inFence = false;
|
|
2171
|
+
let fenceLang = "";
|
|
2172
|
+
const fenceLines = [];
|
|
2173
|
+
for (const line of lines) {
|
|
2174
|
+
if (!inFence && line.startsWith("```")) {
|
|
2175
|
+
inFence = true;
|
|
2176
|
+
fenceLang = line.slice(3).trim() || "plain text";
|
|
2177
|
+
fenceLines.length = 0;
|
|
2178
|
+
continue;
|
|
2179
|
+
}
|
|
2180
|
+
if (inFence) {
|
|
2181
|
+
if (line.startsWith("```")) {
|
|
2182
|
+
const content = fenceLines.join("\n");
|
|
2183
|
+
blocks.push({
|
|
2184
|
+
type: "code",
|
|
2185
|
+
code: {
|
|
2186
|
+
rich_text: [
|
|
2187
|
+
{
|
|
2188
|
+
type: "text",
|
|
2189
|
+
text: { content, link: null },
|
|
2190
|
+
annotations: { bold: false, italic: false, strikethrough: false, underline: false, code: false, color: "default" }
|
|
2191
|
+
}
|
|
2192
|
+
],
|
|
2193
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2194
|
+
language: fenceLang
|
|
2195
|
+
}
|
|
2196
|
+
});
|
|
2197
|
+
inFence = false;
|
|
2198
|
+
fenceLang = "";
|
|
2199
|
+
fenceLines.length = 0;
|
|
2200
|
+
} else {
|
|
2201
|
+
fenceLines.push(line);
|
|
2202
|
+
}
|
|
2203
|
+
continue;
|
|
2204
|
+
}
|
|
2205
|
+
if (line.trim() === "") continue;
|
|
2206
|
+
const h1 = line.match(/^# (.+)$/);
|
|
2207
|
+
if (h1) {
|
|
2208
|
+
blocks.push({
|
|
2209
|
+
type: "heading_1",
|
|
2210
|
+
heading_1: { rich_text: makeRichText(h1[1]) }
|
|
2211
|
+
});
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
const h2 = line.match(/^## (.+)$/);
|
|
2215
|
+
if (h2) {
|
|
2216
|
+
blocks.push({
|
|
2217
|
+
type: "heading_2",
|
|
2218
|
+
heading_2: { rich_text: makeRichText(h2[1]) }
|
|
2219
|
+
});
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
const h3 = line.match(/^### (.+)$/);
|
|
2223
|
+
if (h3) {
|
|
2224
|
+
blocks.push({
|
|
2225
|
+
type: "heading_3",
|
|
2226
|
+
heading_3: { rich_text: makeRichText(h3[1]) }
|
|
2227
|
+
});
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
const bullet = line.match(/^[-*] (.+)$/);
|
|
2231
|
+
if (bullet) {
|
|
2232
|
+
blocks.push({
|
|
2233
|
+
type: "bulleted_list_item",
|
|
2234
|
+
bulleted_list_item: { rich_text: makeRichText(bullet[1]) }
|
|
2235
|
+
});
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
const numbered = line.match(/^\d+\. (.+)$/);
|
|
2239
|
+
if (numbered) {
|
|
2240
|
+
blocks.push({
|
|
2241
|
+
type: "numbered_list_item",
|
|
2242
|
+
numbered_list_item: { rich_text: makeRichText(numbered[1]) }
|
|
2243
|
+
});
|
|
2244
|
+
continue;
|
|
2245
|
+
}
|
|
2246
|
+
const quote = line.match(/^> (.+)$/);
|
|
2247
|
+
if (quote) {
|
|
2248
|
+
blocks.push({
|
|
2249
|
+
type: "quote",
|
|
2250
|
+
quote: { rich_text: makeRichText(quote[1]) }
|
|
2251
|
+
});
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
blocks.push({
|
|
2255
|
+
type: "paragraph",
|
|
2256
|
+
paragraph: { rich_text: makeRichText(line) }
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
if (inFence && fenceLines.length > 0) {
|
|
2260
|
+
const content = fenceLines.join("\n");
|
|
2261
|
+
blocks.push({
|
|
2262
|
+
type: "code",
|
|
2263
|
+
code: {
|
|
2264
|
+
rich_text: [
|
|
2265
|
+
{
|
|
2266
|
+
type: "text",
|
|
2267
|
+
text: { content, link: null },
|
|
2268
|
+
annotations: { bold: false, italic: false, strikethrough: false, underline: false, code: false, color: "default" }
|
|
2269
|
+
}
|
|
2270
|
+
],
|
|
2271
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2272
|
+
language: fenceLang
|
|
2273
|
+
}
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
return blocks;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// src/commands/append.ts
|
|
2280
|
+
function appendCommand() {
|
|
2281
|
+
const cmd = new Command18("append");
|
|
2282
|
+
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(withErrorHandling(async (idOrUrl, opts) => {
|
|
2283
|
+
const { token, source } = await resolveToken();
|
|
2284
|
+
reportTokenSource(source);
|
|
2285
|
+
const client = createNotionClient(token);
|
|
2286
|
+
const pageId = parseNotionId(idOrUrl);
|
|
2287
|
+
const uuid = toUuid(pageId);
|
|
2288
|
+
const blocks = mdToBlocks(opts.message);
|
|
2289
|
+
if (blocks.length === 0) {
|
|
2290
|
+
process.stdout.write("Nothing to append.\n");
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
await appendBlocks(client, uuid, blocks);
|
|
2294
|
+
process.stdout.write(`Appended ${blocks.length} block(s).
|
|
2295
|
+
`);
|
|
2296
|
+
}));
|
|
2297
|
+
return cmd;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
// src/commands/create-page.ts
|
|
2301
|
+
import { Command as Command19 } from "commander";
|
|
2302
|
+
async function readStdin() {
|
|
2303
|
+
const chunks = [];
|
|
2304
|
+
for await (const chunk of process.stdin) {
|
|
2305
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2306
|
+
}
|
|
2307
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
2308
|
+
}
|
|
2309
|
+
function createPageCommand() {
|
|
2310
|
+
const cmd = new Command19("create-page");
|
|
2311
|
+
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("-m, --message <markdown>", "inline markdown content for the page body").action(withErrorHandling(async (opts) => {
|
|
2312
|
+
const { token, source } = await resolveToken();
|
|
2313
|
+
reportTokenSource(source);
|
|
2314
|
+
const client = createNotionClient(token);
|
|
2315
|
+
let markdown = "";
|
|
2316
|
+
if (opts.message) {
|
|
2317
|
+
markdown = opts.message;
|
|
2318
|
+
} else if (!process.stdin.isTTY) {
|
|
2319
|
+
markdown = await readStdin();
|
|
2320
|
+
}
|
|
2321
|
+
const blocks = mdToBlocks(markdown);
|
|
2322
|
+
const parentUuid = toUuid(parseNotionId(opts.parent));
|
|
2323
|
+
const url = await createPage(client, parentUuid, opts.title, blocks);
|
|
2324
|
+
process.stdout.write(url + "\n");
|
|
2325
|
+
}));
|
|
2326
|
+
return cmd;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
1561
2329
|
// src/cli.ts
|
|
1562
2330
|
var __filename = fileURLToPath(import.meta.url);
|
|
1563
2331
|
var __dirname = dirname(__filename);
|
|
1564
2332
|
var pkg = JSON.parse(readFileSync(join3(__dirname, "../package.json"), "utf-8"));
|
|
1565
|
-
var program = new
|
|
2333
|
+
var program = new Command20();
|
|
1566
2334
|
program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
|
|
1567
2335
|
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");
|
|
1568
2336
|
program.configureOutput({
|
|
@@ -1584,7 +2352,12 @@ program.hook("preAction", (thisCommand) => {
|
|
|
1584
2352
|
}
|
|
1585
2353
|
});
|
|
1586
2354
|
program.addCommand(initCommand());
|
|
1587
|
-
var
|
|
2355
|
+
var authCmd = new Command20("auth").description("manage Notion authentication");
|
|
2356
|
+
authCmd.addCommand(loginCommand());
|
|
2357
|
+
authCmd.addCommand(logoutCommand());
|
|
2358
|
+
authCmd.addCommand(statusCommand());
|
|
2359
|
+
program.addCommand(authCmd);
|
|
2360
|
+
var profileCmd = new Command20("profile").description("manage authentication profiles");
|
|
1588
2361
|
profileCmd.addCommand(profileListCommand());
|
|
1589
2362
|
profileCmd.addCommand(profileUseCommand());
|
|
1590
2363
|
profileCmd.addCommand(profileRemoveCommand());
|
|
@@ -1595,7 +2368,10 @@ program.addCommand(openCommand());
|
|
|
1595
2368
|
program.addCommand(usersCommand());
|
|
1596
2369
|
program.addCommand(commentsCommand());
|
|
1597
2370
|
program.addCommand(readCommand());
|
|
1598
|
-
|
|
2371
|
+
program.addCommand(commentAddCommand());
|
|
2372
|
+
program.addCommand(appendCommand());
|
|
2373
|
+
program.addCommand(createPageCommand());
|
|
2374
|
+
var dbCmd = new Command20("db").description("Database operations");
|
|
1599
2375
|
dbCmd.addCommand(dbSchemaCommand());
|
|
1600
2376
|
dbCmd.addCommand(dbQueryCommand());
|
|
1601
2377
|
program.addCommand(dbCmd);
|