@andrzejchm/notion-cli 0.1.2 → 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 -8
- package/dist/cli.js +821 -34
- 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
|
}
|
|
@@ -347,14 +350,468 @@ function initCommand() {
|
|
|
347
350
|
} catch {
|
|
348
351
|
stderrWrite(dim("(Could not verify integration access \u2014 run `notion ls` to check)"));
|
|
349
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"));
|
|
350
361
|
}));
|
|
351
362
|
return cmd;
|
|
352
363
|
}
|
|
353
364
|
|
|
354
|
-
// src/commands/
|
|
365
|
+
// src/commands/auth/login.ts
|
|
355
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";
|
|
356
813
|
function profileListCommand() {
|
|
357
|
-
const cmd = new
|
|
814
|
+
const cmd = new Command5("list");
|
|
358
815
|
cmd.description("list all authentication profiles").action(withErrorHandling(async () => {
|
|
359
816
|
const config = await readGlobalConfig();
|
|
360
817
|
const profiles = config.profiles ?? {};
|
|
@@ -377,9 +834,9 @@ function profileListCommand() {
|
|
|
377
834
|
}
|
|
378
835
|
|
|
379
836
|
// src/commands/profile/use.ts
|
|
380
|
-
import { Command as
|
|
837
|
+
import { Command as Command6 } from "commander";
|
|
381
838
|
function profileUseCommand() {
|
|
382
|
-
const cmd = new
|
|
839
|
+
const cmd = new Command6("use");
|
|
383
840
|
cmd.description("switch the active profile").argument("<name>", "profile name to activate").action(withErrorHandling(async (name) => {
|
|
384
841
|
const config = await readGlobalConfig();
|
|
385
842
|
const profiles = config.profiles ?? {};
|
|
@@ -400,9 +857,9 @@ function profileUseCommand() {
|
|
|
400
857
|
}
|
|
401
858
|
|
|
402
859
|
// src/commands/profile/remove.ts
|
|
403
|
-
import { Command as
|
|
860
|
+
import { Command as Command7 } from "commander";
|
|
404
861
|
function profileRemoveCommand() {
|
|
405
|
-
const cmd = new
|
|
862
|
+
const cmd = new Command7("remove");
|
|
406
863
|
cmd.description("remove an authentication profile").argument("<name>", "profile name to remove").action(withErrorHandling(async (name) => {
|
|
407
864
|
const config = await readGlobalConfig();
|
|
408
865
|
const profiles = { ...config.profiles ?? {} };
|
|
@@ -426,7 +883,7 @@ function profileRemoveCommand() {
|
|
|
426
883
|
}
|
|
427
884
|
|
|
428
885
|
// src/commands/completion.ts
|
|
429
|
-
import { Command as
|
|
886
|
+
import { Command as Command8 } from "commander";
|
|
430
887
|
var BASH_COMPLETION = `# notion bash completion
|
|
431
888
|
_notion_completion() {
|
|
432
889
|
local cur prev words cword
|
|
@@ -528,7 +985,7 @@ complete -c notion -n '__fish_seen_subcommand_from completion' -a zsh -d 'zsh co
|
|
|
528
985
|
complete -c notion -n '__fish_seen_subcommand_from completion' -a fish -d 'fish completion script'
|
|
529
986
|
`;
|
|
530
987
|
function completionCommand() {
|
|
531
|
-
const cmd = new
|
|
988
|
+
const cmd = new Command8("completion");
|
|
532
989
|
cmd.description("output shell completion script").argument("<shell>", "shell type (bash, zsh, fish)").action(withErrorHandling(async (shell) => {
|
|
533
990
|
switch (shell) {
|
|
534
991
|
case "bash":
|
|
@@ -552,7 +1009,7 @@ function completionCommand() {
|
|
|
552
1009
|
}
|
|
553
1010
|
|
|
554
1011
|
// src/commands/search.ts
|
|
555
|
-
import { Command as
|
|
1012
|
+
import { Command as Command9 } from "commander";
|
|
556
1013
|
import { isFullPageOrDataSource } from "@notionhq/client";
|
|
557
1014
|
|
|
558
1015
|
// src/config/local-config.ts
|
|
@@ -595,6 +1052,36 @@ async function readLocalConfig() {
|
|
|
595
1052
|
}
|
|
596
1053
|
|
|
597
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
|
+
}
|
|
598
1085
|
async function resolveToken() {
|
|
599
1086
|
const envToken = process.env["NOTION_API_TOKEN"];
|
|
600
1087
|
if (envToken) {
|
|
@@ -607,17 +1094,29 @@ async function resolveToken() {
|
|
|
607
1094
|
}
|
|
608
1095
|
if (localConfig.profile) {
|
|
609
1096
|
const globalConfig2 = await readGlobalConfig();
|
|
610
|
-
const
|
|
611
|
-
if (
|
|
612
|
-
|
|
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
|
+
}
|
|
613
1106
|
}
|
|
614
1107
|
}
|
|
615
1108
|
}
|
|
616
1109
|
const globalConfig = await readGlobalConfig();
|
|
617
1110
|
if (globalConfig.active_profile) {
|
|
618
|
-
const
|
|
619
|
-
if (
|
|
620
|
-
|
|
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
|
+
}
|
|
621
1120
|
}
|
|
622
1121
|
}
|
|
623
1122
|
throw new CliError(
|
|
@@ -645,7 +1144,7 @@ function displayType(item) {
|
|
|
645
1144
|
return item.object === "data_source" ? "database" : item.object;
|
|
646
1145
|
}
|
|
647
1146
|
function searchCommand() {
|
|
648
|
-
const cmd = new
|
|
1147
|
+
const cmd = new Command9("search");
|
|
649
1148
|
cmd.description("search Notion workspace by keyword").argument("<query>", "search keyword").option("--type <type>", "filter by object type (page or database)", (val) => {
|
|
650
1149
|
if (val !== "page" && val !== "database") {
|
|
651
1150
|
throw new Error('--type must be "page" or "database"');
|
|
@@ -690,7 +1189,7 @@ function searchCommand() {
|
|
|
690
1189
|
}
|
|
691
1190
|
|
|
692
1191
|
// src/commands/ls.ts
|
|
693
|
-
import { Command as
|
|
1192
|
+
import { Command as Command10 } from "commander";
|
|
694
1193
|
import { isFullPageOrDataSource as isFullPageOrDataSource2 } from "@notionhq/client";
|
|
695
1194
|
function getTitle2(item) {
|
|
696
1195
|
if (item.object === "data_source") {
|
|
@@ -706,7 +1205,7 @@ function displayType2(item) {
|
|
|
706
1205
|
return item.object === "data_source" ? "database" : item.object;
|
|
707
1206
|
}
|
|
708
1207
|
function lsCommand() {
|
|
709
|
-
const cmd = new
|
|
1208
|
+
const cmd = new Command10("ls");
|
|
710
1209
|
cmd.description("list accessible Notion pages and databases").option("--type <type>", "filter by object type (page or database)", (val) => {
|
|
711
1210
|
if (val !== "page" && val !== "database") {
|
|
712
1211
|
throw new Error('--type must be "page" or "database"');
|
|
@@ -756,7 +1255,7 @@ function lsCommand() {
|
|
|
756
1255
|
// src/commands/open.ts
|
|
757
1256
|
import { exec } from "child_process";
|
|
758
1257
|
import { promisify } from "util";
|
|
759
|
-
import { Command as
|
|
1258
|
+
import { Command as Command11 } from "commander";
|
|
760
1259
|
|
|
761
1260
|
// src/notion/url-parser.ts
|
|
762
1261
|
var NOTION_ID_REGEX = /^[0-9a-f]{32}$/i;
|
|
@@ -790,7 +1289,7 @@ function toUuid(id) {
|
|
|
790
1289
|
// src/commands/open.ts
|
|
791
1290
|
var execAsync = promisify(exec);
|
|
792
1291
|
function openCommand() {
|
|
793
|
-
const cmd = new
|
|
1292
|
+
const cmd = new Command11("open");
|
|
794
1293
|
cmd.description("open a Notion page in the default browser").argument("<id/url>", "Notion page ID or URL").action(withErrorHandling(async (idOrUrl) => {
|
|
795
1294
|
const id = parseNotionId(idOrUrl);
|
|
796
1295
|
const url = `https://www.notion.so/${id}`;
|
|
@@ -804,7 +1303,7 @@ function openCommand() {
|
|
|
804
1303
|
}
|
|
805
1304
|
|
|
806
1305
|
// src/commands/users.ts
|
|
807
|
-
import { Command as
|
|
1306
|
+
import { Command as Command12 } from "commander";
|
|
808
1307
|
|
|
809
1308
|
// src/output/paginate.ts
|
|
810
1309
|
async function paginateResults(fetcher) {
|
|
@@ -832,7 +1331,7 @@ function getEmailOrWorkspace(user) {
|
|
|
832
1331
|
return "\u2014";
|
|
833
1332
|
}
|
|
834
1333
|
function usersCommand() {
|
|
835
|
-
const cmd = new
|
|
1334
|
+
const cmd = new Command12("users");
|
|
836
1335
|
cmd.description("list all users in the workspace").option("--json", "output as JSON").action(withErrorHandling(async (opts) => {
|
|
837
1336
|
if (opts.json) setOutputMode("json");
|
|
838
1337
|
const { token, source } = await resolveToken();
|
|
@@ -854,9 +1353,9 @@ function usersCommand() {
|
|
|
854
1353
|
}
|
|
855
1354
|
|
|
856
1355
|
// src/commands/comments.ts
|
|
857
|
-
import { Command as
|
|
1356
|
+
import { Command as Command13 } from "commander";
|
|
858
1357
|
function commentsCommand() {
|
|
859
|
-
const cmd = new
|
|
1358
|
+
const cmd = new Command13("comments");
|
|
860
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) => {
|
|
861
1360
|
if (opts.json) setOutputMode("json");
|
|
862
1361
|
const id = parseNotionId(idOrUrl);
|
|
@@ -885,7 +1384,7 @@ function commentsCommand() {
|
|
|
885
1384
|
}
|
|
886
1385
|
|
|
887
1386
|
// src/commands/read.ts
|
|
888
|
-
import { Command as
|
|
1387
|
+
import { Command as Command14 } from "commander";
|
|
889
1388
|
|
|
890
1389
|
// src/services/page.service.ts
|
|
891
1390
|
import { collectPaginatedAPI, isFullBlock } from "@notionhq/client";
|
|
@@ -1281,7 +1780,7 @@ function renderInline(text) {
|
|
|
1281
1780
|
|
|
1282
1781
|
// src/commands/read.ts
|
|
1283
1782
|
function readCommand() {
|
|
1284
|
-
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(
|
|
1285
1784
|
withErrorHandling(async (id, options) => {
|
|
1286
1785
|
const { token } = await resolveToken();
|
|
1287
1786
|
const client = createNotionClient(token);
|
|
@@ -1302,7 +1801,7 @@ function readCommand() {
|
|
|
1302
1801
|
}
|
|
1303
1802
|
|
|
1304
1803
|
// src/commands/db/schema.ts
|
|
1305
|
-
import { Command as
|
|
1804
|
+
import { Command as Command15 } from "commander";
|
|
1306
1805
|
|
|
1307
1806
|
// src/services/database.service.ts
|
|
1308
1807
|
import { isFullPage as isFullPage2 } from "@notionhq/client";
|
|
@@ -1465,7 +1964,7 @@ function displayPropertyValue(prop) {
|
|
|
1465
1964
|
|
|
1466
1965
|
// src/commands/db/schema.ts
|
|
1467
1966
|
function dbSchemaCommand() {
|
|
1468
|
-
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(
|
|
1469
1968
|
withErrorHandling(async (id, options) => {
|
|
1470
1969
|
const { token } = await resolveToken();
|
|
1471
1970
|
const client = createNotionClient(token);
|
|
@@ -1487,7 +1986,7 @@ function dbSchemaCommand() {
|
|
|
1487
1986
|
}
|
|
1488
1987
|
|
|
1489
1988
|
// src/commands/db/query.ts
|
|
1490
|
-
import { Command as
|
|
1989
|
+
import { Command as Command16 } from "commander";
|
|
1491
1990
|
var SKIP_TYPES_IN_AUTO = /* @__PURE__ */ new Set(["relation", "rich_text", "people"]);
|
|
1492
1991
|
function autoSelectColumns(schema, entries) {
|
|
1493
1992
|
const termWidth = process.stdout.columns || 120;
|
|
@@ -1512,7 +2011,7 @@ function autoSelectColumns(schema, entries) {
|
|
|
1512
2011
|
return selected;
|
|
1513
2012
|
}
|
|
1514
2013
|
function dbQueryCommand() {
|
|
1515
|
-
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(
|
|
1516
2015
|
withErrorHandling(
|
|
1517
2016
|
async (id, options) => {
|
|
1518
2017
|
const { token } = await resolveToken();
|
|
@@ -1547,11 +2046,291 @@ function collect(value, previous) {
|
|
|
1547
2046
|
return previous.concat([value]);
|
|
1548
2047
|
}
|
|
1549
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
|
+
|
|
1550
2329
|
// src/cli.ts
|
|
1551
2330
|
var __filename = fileURLToPath(import.meta.url);
|
|
1552
2331
|
var __dirname = dirname(__filename);
|
|
1553
2332
|
var pkg = JSON.parse(readFileSync(join3(__dirname, "../package.json"), "utf-8"));
|
|
1554
|
-
var program = new
|
|
2333
|
+
var program = new Command20();
|
|
1555
2334
|
program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
|
|
1556
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");
|
|
1557
2336
|
program.configureOutput({
|
|
@@ -1573,7 +2352,12 @@ program.hook("preAction", (thisCommand) => {
|
|
|
1573
2352
|
}
|
|
1574
2353
|
});
|
|
1575
2354
|
program.addCommand(initCommand());
|
|
1576
|
-
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");
|
|
1577
2361
|
profileCmd.addCommand(profileListCommand());
|
|
1578
2362
|
profileCmd.addCommand(profileUseCommand());
|
|
1579
2363
|
profileCmd.addCommand(profileRemoveCommand());
|
|
@@ -1584,7 +2368,10 @@ program.addCommand(openCommand());
|
|
|
1584
2368
|
program.addCommand(usersCommand());
|
|
1585
2369
|
program.addCommand(commentsCommand());
|
|
1586
2370
|
program.addCommand(readCommand());
|
|
1587
|
-
|
|
2371
|
+
program.addCommand(commentAddCommand());
|
|
2372
|
+
program.addCommand(appendCommand());
|
|
2373
|
+
program.addCommand(createPageCommand());
|
|
2374
|
+
var dbCmd = new Command20("db").description("Database operations");
|
|
1588
2375
|
dbCmd.addCommand(dbSchemaCommand());
|
|
1589
2376
|
dbCmd.addCommand(dbQueryCommand());
|
|
1590
2377
|
program.addCommand(dbCmd);
|