@andrzejchm/notion-cli 0.1.2 → 0.3.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 +100 -20
- package/dist/cli.js +2172 -1062
- package/dist/cli.js.map +1 -1
- package/docs/README.agents.md +81 -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 +176 -0
- package/package.json +11 -3
- package/docs/agent-skill.md +0 -474
package/dist/cli.js
CHANGED
|
@@ -1,109 +1,236 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command as Command14 } from "commander";
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
6
|
-
import { dirname, join as join3 } from "path";
|
|
7
4
|
import { readFileSync } from "fs";
|
|
5
|
+
import { dirname, join as join3 } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { Command as Command19 } from "commander";
|
|
8
8
|
|
|
9
|
-
// src/
|
|
10
|
-
import {
|
|
11
|
-
var _colorForced = false;
|
|
12
|
-
function setColorForced(forced) {
|
|
13
|
-
_colorForced = forced;
|
|
14
|
-
}
|
|
15
|
-
function isColorEnabled() {
|
|
16
|
-
if (process.env.NO_COLOR) return false;
|
|
17
|
-
if (_colorForced) return true;
|
|
18
|
-
return Boolean(process.stderr.isTTY);
|
|
19
|
-
}
|
|
20
|
-
function createChalk() {
|
|
21
|
-
const level = isColorEnabled() ? void 0 : 0;
|
|
22
|
-
return new Chalk({ level });
|
|
23
|
-
}
|
|
24
|
-
function success(msg) {
|
|
25
|
-
return createChalk().green(msg);
|
|
26
|
-
}
|
|
27
|
-
function dim(msg) {
|
|
28
|
-
return createChalk().dim(msg);
|
|
29
|
-
}
|
|
30
|
-
function bold(msg) {
|
|
31
|
-
return createChalk().bold(msg);
|
|
32
|
-
}
|
|
9
|
+
// src/commands/append.ts
|
|
10
|
+
import { Command } from "commander";
|
|
33
11
|
|
|
34
|
-
// src/
|
|
35
|
-
var
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
12
|
+
// src/blocks/md-to-blocks.ts
|
|
13
|
+
var INLINE_RE = /(\*\*[^*]+\*\*|_[^_]+_|\*[^*]+\*|`[^`]+`|\[[^\]]+\]\([^)]+\)|[^*_`[]+)/g;
|
|
14
|
+
function parseInlineMarkdown(text) {
|
|
15
|
+
const result = [];
|
|
16
|
+
let match;
|
|
17
|
+
INLINE_RE.lastIndex = 0;
|
|
18
|
+
while ((match = INLINE_RE.exec(text)) !== null) {
|
|
19
|
+
const segment = match[0];
|
|
20
|
+
if (segment.startsWith("**") && segment.endsWith("**")) {
|
|
21
|
+
const content = segment.slice(2, -2);
|
|
22
|
+
result.push({
|
|
23
|
+
type: "text",
|
|
24
|
+
text: { content, link: null },
|
|
25
|
+
annotations: {
|
|
26
|
+
bold: true,
|
|
27
|
+
italic: false,
|
|
28
|
+
strikethrough: false,
|
|
29
|
+
underline: false,
|
|
30
|
+
code: false,
|
|
31
|
+
color: "default"
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (segment.startsWith("_") && segment.endsWith("_") || segment.startsWith("*") && segment.endsWith("*")) {
|
|
37
|
+
const content = segment.slice(1, -1);
|
|
38
|
+
result.push({
|
|
39
|
+
type: "text",
|
|
40
|
+
text: { content, link: null },
|
|
41
|
+
annotations: {
|
|
42
|
+
bold: false,
|
|
43
|
+
italic: true,
|
|
44
|
+
strikethrough: false,
|
|
45
|
+
underline: false,
|
|
46
|
+
code: false,
|
|
47
|
+
color: "default"
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (segment.startsWith("`") && segment.endsWith("`")) {
|
|
53
|
+
const content = segment.slice(1, -1);
|
|
54
|
+
result.push({
|
|
55
|
+
type: "text",
|
|
56
|
+
text: { content, link: null },
|
|
57
|
+
annotations: {
|
|
58
|
+
bold: false,
|
|
59
|
+
italic: false,
|
|
60
|
+
strikethrough: false,
|
|
61
|
+
underline: false,
|
|
62
|
+
code: true,
|
|
63
|
+
color: "default"
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const linkMatch = segment.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
69
|
+
if (linkMatch) {
|
|
70
|
+
const [, label, url] = linkMatch;
|
|
71
|
+
result.push({
|
|
72
|
+
type: "text",
|
|
73
|
+
text: { content: label, link: { url } },
|
|
74
|
+
annotations: {
|
|
75
|
+
bold: false,
|
|
76
|
+
italic: false,
|
|
77
|
+
strikethrough: false,
|
|
78
|
+
underline: false,
|
|
79
|
+
code: false,
|
|
80
|
+
color: "default"
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
result.push({
|
|
86
|
+
type: "text",
|
|
87
|
+
text: { content: segment, link: null },
|
|
88
|
+
annotations: {
|
|
89
|
+
bold: false,
|
|
90
|
+
italic: false,
|
|
91
|
+
strikethrough: false,
|
|
92
|
+
underline: false,
|
|
93
|
+
code: false,
|
|
94
|
+
color: "default"
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
64
99
|
}
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
const cap = getColumnCap(header);
|
|
68
|
-
const headerLen = header.length;
|
|
69
|
-
const maxRowLen = rows.reduce((max, row) => {
|
|
70
|
-
const cell = row[colIdx] ?? "";
|
|
71
|
-
return Math.max(max, cell.length);
|
|
72
|
-
}, 0);
|
|
73
|
-
return Math.min(Math.max(headerLen, maxRowLen), cap);
|
|
74
|
-
});
|
|
75
|
-
const sep = "\u2500";
|
|
76
|
-
const colSep = " ";
|
|
77
|
-
const headerRow = headers.map((h, i) => h.padEnd(colWidths[i])).join(colSep);
|
|
78
|
-
const separatorRow = colWidths.map((w) => sep.repeat(w)).join(colSep);
|
|
79
|
-
const dataRows = rows.map(
|
|
80
|
-
(row) => headers.map((_, i) => {
|
|
81
|
-
const cell = row[i] ?? "";
|
|
82
|
-
return truncate(cell, colWidths[i]).padEnd(colWidths[i]);
|
|
83
|
-
}).join(colSep)
|
|
84
|
-
);
|
|
85
|
-
return [headerRow, separatorRow, ...dataRows].join("\n");
|
|
100
|
+
function makeRichText(text) {
|
|
101
|
+
return parseInlineMarkdown(text);
|
|
86
102
|
}
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
function mdToBlocks(md) {
|
|
104
|
+
if (!md) return [];
|
|
105
|
+
const lines = md.split("\n");
|
|
106
|
+
const blocks = [];
|
|
107
|
+
let inFence = false;
|
|
108
|
+
let fenceLang = "";
|
|
109
|
+
const fenceLines = [];
|
|
110
|
+
for (const line of lines) {
|
|
111
|
+
if (!inFence && line.startsWith("```")) {
|
|
112
|
+
inFence = true;
|
|
113
|
+
fenceLang = line.slice(3).trim() || "plain text";
|
|
114
|
+
fenceLines.length = 0;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (inFence) {
|
|
118
|
+
if (line.startsWith("```")) {
|
|
119
|
+
const content = fenceLines.join("\n");
|
|
120
|
+
blocks.push({
|
|
121
|
+
type: "code",
|
|
122
|
+
code: {
|
|
123
|
+
rich_text: [
|
|
124
|
+
{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: { content, link: null },
|
|
127
|
+
annotations: {
|
|
128
|
+
bold: false,
|
|
129
|
+
italic: false,
|
|
130
|
+
strikethrough: false,
|
|
131
|
+
underline: false,
|
|
132
|
+
code: false,
|
|
133
|
+
color: "default"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
// biome-ignore lint/suspicious/noExplicitAny: Notion SDK language type is too narrow
|
|
138
|
+
language: fenceLang
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
inFence = false;
|
|
142
|
+
fenceLang = "";
|
|
143
|
+
fenceLines.length = 0;
|
|
144
|
+
} else {
|
|
145
|
+
fenceLines.push(line);
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (line.trim() === "") continue;
|
|
150
|
+
const h1 = line.match(/^# (.+)$/);
|
|
151
|
+
if (h1) {
|
|
152
|
+
blocks.push({
|
|
153
|
+
type: "heading_1",
|
|
154
|
+
heading_1: { rich_text: makeRichText(h1[1]) }
|
|
155
|
+
});
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const h2 = line.match(/^## (.+)$/);
|
|
159
|
+
if (h2) {
|
|
160
|
+
blocks.push({
|
|
161
|
+
type: "heading_2",
|
|
162
|
+
heading_2: { rich_text: makeRichText(h2[1]) }
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const h3 = line.match(/^### (.+)$/);
|
|
167
|
+
if (h3) {
|
|
168
|
+
blocks.push({
|
|
169
|
+
type: "heading_3",
|
|
170
|
+
heading_3: { rich_text: makeRichText(h3[1]) }
|
|
171
|
+
});
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const bullet = line.match(/^[-*] (.+)$/);
|
|
175
|
+
if (bullet) {
|
|
176
|
+
blocks.push({
|
|
177
|
+
type: "bulleted_list_item",
|
|
178
|
+
bulleted_list_item: { rich_text: makeRichText(bullet[1]) }
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const numbered = line.match(/^\d+\. (.+)$/);
|
|
183
|
+
if (numbered) {
|
|
184
|
+
blocks.push({
|
|
185
|
+
type: "numbered_list_item",
|
|
186
|
+
numbered_list_item: { rich_text: makeRichText(numbered[1]) }
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const quote = line.match(/^> (.+)$/);
|
|
191
|
+
if (quote) {
|
|
192
|
+
blocks.push({
|
|
193
|
+
type: "quote",
|
|
194
|
+
quote: { rich_text: makeRichText(quote[1]) }
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
blocks.push({
|
|
199
|
+
type: "paragraph",
|
|
200
|
+
paragraph: { rich_text: makeRichText(line) }
|
|
201
|
+
});
|
|
93
202
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
203
|
+
if (inFence && fenceLines.length > 0) {
|
|
204
|
+
const content = fenceLines.join("\n");
|
|
205
|
+
blocks.push({
|
|
206
|
+
type: "code",
|
|
207
|
+
code: {
|
|
208
|
+
rich_text: [
|
|
209
|
+
{
|
|
210
|
+
type: "text",
|
|
211
|
+
text: { content, link: null },
|
|
212
|
+
annotations: {
|
|
213
|
+
bold: false,
|
|
214
|
+
italic: false,
|
|
215
|
+
strikethrough: false,
|
|
216
|
+
underline: false,
|
|
217
|
+
code: false,
|
|
218
|
+
color: "default"
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
],
|
|
222
|
+
// biome-ignore lint/suspicious/noExplicitAny: Notion SDK language type is too narrow
|
|
223
|
+
language: fenceLang
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return blocks;
|
|
97
228
|
}
|
|
98
229
|
|
|
99
|
-
// src/commands/init.ts
|
|
100
|
-
import { Command } from "commander";
|
|
101
|
-
import { input, password, confirm } from "@inquirer/prompts";
|
|
102
|
-
|
|
103
230
|
// src/errors/cli-error.ts
|
|
104
231
|
var CliError = class extends Error {
|
|
105
|
-
constructor(code, message, suggestion) {
|
|
106
|
-
super(message);
|
|
232
|
+
constructor(code, message, suggestion, cause) {
|
|
233
|
+
super(message, { cause });
|
|
107
234
|
this.code = code;
|
|
108
235
|
this.suggestion = suggestion;
|
|
109
236
|
this.name = "CliError";
|
|
@@ -141,6 +268,120 @@ var ErrorCodes = {
|
|
|
141
268
|
UNKNOWN: "UNKNOWN"
|
|
142
269
|
};
|
|
143
270
|
|
|
271
|
+
// src/oauth/oauth-client.ts
|
|
272
|
+
var _k = 90;
|
|
273
|
+
var _d = (parts) => Buffer.from(parts.join(""), "base64").toString().split("").map((c2) => String.fromCharCode(c2.charCodeAt(0) ^ _k)).join("");
|
|
274
|
+
var OAUTH_CLIENT_ID = _d([
|
|
275
|
+
"aWtu",
|
|
276
|
+
"PmJt",
|
|
277
|
+
"aDh3",
|
|
278
|
+
"b2Nu",
|
|
279
|
+
"OXdi",
|
|
280
|
+
"a2I+",
|
|
281
|
+
"d2I5",
|
|
282
|
+
"amh3",
|
|
283
|
+
"ampp",
|
|
284
|
+
"bTtj",
|
|
285
|
+
"Pm44",
|
|
286
|
+
"P2s8"
|
|
287
|
+
]);
|
|
288
|
+
var OAUTH_CLIENT_SECRET = _d([
|
|
289
|
+
"KT85",
|
|
290
|
+
"KD8u",
|
|
291
|
+
"BWMM",
|
|
292
|
+
"axcx",
|
|
293
|
+
"P28P",
|
|
294
|
+
"ahYp",
|
|
295
|
+
"MCti",
|
|
296
|
+
"MQtt",
|
|
297
|
+
"Hj4V",
|
|
298
|
+
"NywV",
|
|
299
|
+
"I2sp",
|
|
300
|
+
"bzlv",
|
|
301
|
+
"ECIK",
|
|
302
|
+
"NTAx",
|
|
303
|
+
"IGwA",
|
|
304
|
+
"ETU7",
|
|
305
|
+
"ahU="
|
|
306
|
+
]);
|
|
307
|
+
var OAUTH_REDIRECT_URI = "http://localhost:54321/oauth/callback";
|
|
308
|
+
function buildAuthUrl(state) {
|
|
309
|
+
const params = new URLSearchParams({
|
|
310
|
+
client_id: OAUTH_CLIENT_ID,
|
|
311
|
+
redirect_uri: OAUTH_REDIRECT_URI,
|
|
312
|
+
response_type: "code",
|
|
313
|
+
owner: "user",
|
|
314
|
+
state
|
|
315
|
+
});
|
|
316
|
+
return `https://api.notion.com/v1/oauth/authorize?${params.toString()}`;
|
|
317
|
+
}
|
|
318
|
+
function basicAuth() {
|
|
319
|
+
return Buffer.from(`${OAUTH_CLIENT_ID}:${OAUTH_CLIENT_SECRET}`).toString(
|
|
320
|
+
"base64"
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
async function exchangeCode(code, redirectUri = OAUTH_REDIRECT_URI) {
|
|
324
|
+
const response = await fetch("https://api.notion.com/v1/oauth/token", {
|
|
325
|
+
method: "POST",
|
|
326
|
+
headers: {
|
|
327
|
+
Authorization: `Basic ${basicAuth()}`,
|
|
328
|
+
"Content-Type": "application/json",
|
|
329
|
+
"Notion-Version": "2022-06-28"
|
|
330
|
+
},
|
|
331
|
+
body: JSON.stringify({
|
|
332
|
+
grant_type: "authorization_code",
|
|
333
|
+
code,
|
|
334
|
+
redirect_uri: redirectUri
|
|
335
|
+
})
|
|
336
|
+
});
|
|
337
|
+
if (!response.ok) {
|
|
338
|
+
let errorMessage = `OAuth token exchange failed (HTTP ${response.status})`;
|
|
339
|
+
try {
|
|
340
|
+
const body = await response.json();
|
|
341
|
+
if (body.error_description) errorMessage = body.error_description;
|
|
342
|
+
else if (body.error) errorMessage = body.error;
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
throw new CliError(
|
|
346
|
+
ErrorCodes.AUTH_INVALID,
|
|
347
|
+
errorMessage,
|
|
348
|
+
'Run "notion auth login" to restart the OAuth flow'
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
const data = await response.json();
|
|
352
|
+
return data;
|
|
353
|
+
}
|
|
354
|
+
async function refreshAccessToken(refreshToken) {
|
|
355
|
+
const response = await fetch("https://api.notion.com/v1/oauth/token", {
|
|
356
|
+
method: "POST",
|
|
357
|
+
headers: {
|
|
358
|
+
Authorization: `Basic ${basicAuth()}`,
|
|
359
|
+
"Content-Type": "application/json",
|
|
360
|
+
"Notion-Version": "2022-06-28"
|
|
361
|
+
},
|
|
362
|
+
body: JSON.stringify({
|
|
363
|
+
grant_type: "refresh_token",
|
|
364
|
+
refresh_token: refreshToken
|
|
365
|
+
})
|
|
366
|
+
});
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
let errorMessage = `OAuth token refresh failed (HTTP ${response.status})`;
|
|
369
|
+
try {
|
|
370
|
+
const body = await response.json();
|
|
371
|
+
if (body.error_description) errorMessage = body.error_description;
|
|
372
|
+
else if (body.error) errorMessage = body.error;
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
throw new CliError(
|
|
376
|
+
ErrorCodes.AUTH_INVALID,
|
|
377
|
+
errorMessage,
|
|
378
|
+
'Run "notion auth login" to re-authenticate'
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const data = await response.json();
|
|
382
|
+
return data;
|
|
383
|
+
}
|
|
384
|
+
|
|
144
385
|
// src/config/config.ts
|
|
145
386
|
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
146
387
|
import { parse, stringify } from "yaml";
|
|
@@ -149,7 +390,7 @@ import { parse, stringify } from "yaml";
|
|
|
149
390
|
import { homedir } from "os";
|
|
150
391
|
import { join } from "path";
|
|
151
392
|
function getConfigDir() {
|
|
152
|
-
const xdgConfigHome = process.env
|
|
393
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
153
394
|
const base = xdgConfigHome ? xdgConfigHome : join(homedir(), ".config");
|
|
154
395
|
return join(base, "notion-cli");
|
|
155
396
|
}
|
|
@@ -170,17 +411,19 @@ async function readGlobalConfig() {
|
|
|
170
411
|
throw new CliError(
|
|
171
412
|
ErrorCodes.CONFIG_READ_ERROR,
|
|
172
413
|
`Failed to read config file: ${configPath}`,
|
|
173
|
-
'Check file permissions or run "notion init" to create a new config'
|
|
414
|
+
'Check file permissions or run "notion init" to create a new config',
|
|
415
|
+
err
|
|
174
416
|
);
|
|
175
417
|
}
|
|
176
418
|
try {
|
|
177
419
|
const parsed = parse(raw);
|
|
178
420
|
return parsed ?? {};
|
|
179
|
-
} catch {
|
|
421
|
+
} catch (err) {
|
|
180
422
|
throw new CliError(
|
|
181
423
|
ErrorCodes.CONFIG_READ_ERROR,
|
|
182
424
|
`Failed to parse config file: ${configPath}`,
|
|
183
|
-
'The config file may be corrupted. Delete it and run "notion init" to start fresh'
|
|
425
|
+
'The config file may be corrupted. Delete it and run "notion init" to start fresh',
|
|
426
|
+
err
|
|
184
427
|
);
|
|
185
428
|
}
|
|
186
429
|
}
|
|
@@ -190,70 +433,203 @@ async function writeGlobalConfig(config) {
|
|
|
190
433
|
const tmpPath = `${configPath}.tmp`;
|
|
191
434
|
try {
|
|
192
435
|
await mkdir(configDir, { recursive: true, mode: 448 });
|
|
193
|
-
} catch {
|
|
436
|
+
} catch (err) {
|
|
194
437
|
throw new CliError(
|
|
195
438
|
ErrorCodes.CONFIG_WRITE_ERROR,
|
|
196
439
|
`Failed to create config directory: ${configDir}`,
|
|
197
|
-
"Check that you have write permissions to your home directory"
|
|
440
|
+
"Check that you have write permissions to your home directory",
|
|
441
|
+
err
|
|
198
442
|
);
|
|
199
443
|
}
|
|
200
444
|
const content = stringify(config);
|
|
201
445
|
try {
|
|
202
446
|
await writeFile(tmpPath, content, { mode: 384 });
|
|
203
447
|
await rename(tmpPath, configPath);
|
|
204
|
-
} catch {
|
|
448
|
+
} catch (err) {
|
|
205
449
|
throw new CliError(
|
|
206
450
|
ErrorCodes.CONFIG_WRITE_ERROR,
|
|
207
451
|
`Failed to write config file: ${configPath}`,
|
|
208
|
-
"Check file permissions in the config directory"
|
|
452
|
+
"Check file permissions in the config directory",
|
|
453
|
+
err
|
|
209
454
|
);
|
|
210
455
|
}
|
|
211
456
|
}
|
|
212
457
|
|
|
213
|
-
// src/
|
|
214
|
-
|
|
215
|
-
async function
|
|
216
|
-
const
|
|
458
|
+
// src/oauth/token-store.ts
|
|
459
|
+
var OAUTH_EXPIRY_DURATION_MS = 60 * 60 * 1e3;
|
|
460
|
+
async function saveOAuthTokens(profileName, response) {
|
|
461
|
+
const config = await readGlobalConfig();
|
|
462
|
+
const existing = config.profiles?.[profileName] ?? {};
|
|
463
|
+
const updatedProfile = {
|
|
464
|
+
...existing,
|
|
465
|
+
oauth_access_token: response.access_token,
|
|
466
|
+
oauth_refresh_token: response.refresh_token,
|
|
467
|
+
oauth_expiry_ms: Date.now() + OAUTH_EXPIRY_DURATION_MS,
|
|
468
|
+
workspace_id: response.workspace_id,
|
|
469
|
+
workspace_name: response.workspace_name,
|
|
470
|
+
...response.owner?.user?.id != null && {
|
|
471
|
+
oauth_user_id: response.owner.user.id
|
|
472
|
+
},
|
|
473
|
+
...response.owner?.user?.name != null && {
|
|
474
|
+
oauth_user_name: response.owner.user.name
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
config.profiles = {
|
|
478
|
+
...config.profiles,
|
|
479
|
+
[profileName]: updatedProfile
|
|
480
|
+
};
|
|
481
|
+
await writeGlobalConfig(config);
|
|
482
|
+
}
|
|
483
|
+
async function clearOAuthTokens(profileName) {
|
|
484
|
+
const config = await readGlobalConfig();
|
|
485
|
+
const existing = config.profiles?.[profileName];
|
|
486
|
+
if (!existing) return;
|
|
487
|
+
const {
|
|
488
|
+
oauth_access_token: _access,
|
|
489
|
+
oauth_refresh_token: _refresh,
|
|
490
|
+
oauth_expiry_ms: _expiry,
|
|
491
|
+
oauth_user_id: _userId,
|
|
492
|
+
oauth_user_name: _userName,
|
|
493
|
+
...rest
|
|
494
|
+
} = existing;
|
|
495
|
+
config.profiles = {
|
|
496
|
+
...config.profiles,
|
|
497
|
+
[profileName]: rest
|
|
498
|
+
};
|
|
499
|
+
await writeGlobalConfig(config);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/config/local-config.ts
|
|
503
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
504
|
+
import { join as join2 } from "path";
|
|
505
|
+
import { parse as parse2 } from "yaml";
|
|
506
|
+
async function readLocalConfig() {
|
|
507
|
+
const localConfigPath = join2(process.cwd(), ".notion.yaml");
|
|
508
|
+
let raw;
|
|
217
509
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return { workspaceName, workspaceId };
|
|
223
|
-
} catch (error2) {
|
|
224
|
-
if (isNotionClientError(error2) && error2.code === APIErrorCode.Unauthorized) {
|
|
225
|
-
throw new CliError(
|
|
226
|
-
ErrorCodes.AUTH_INVALID,
|
|
227
|
-
"Invalid integration token.",
|
|
228
|
-
"Check your token at notion.so/profile/integrations/internal"
|
|
229
|
-
);
|
|
510
|
+
raw = await readFile2(localConfigPath, "utf-8");
|
|
511
|
+
} catch (err) {
|
|
512
|
+
if (err.code === "ENOENT") {
|
|
513
|
+
return null;
|
|
230
514
|
}
|
|
231
|
-
throw
|
|
515
|
+
throw new CliError(
|
|
516
|
+
ErrorCodes.CONFIG_READ_ERROR,
|
|
517
|
+
`Failed to read local config: ${localConfigPath}`,
|
|
518
|
+
"Check file permissions",
|
|
519
|
+
err
|
|
520
|
+
);
|
|
232
521
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
522
|
+
let parsed;
|
|
523
|
+
try {
|
|
524
|
+
parsed = parse2(raw) ?? {};
|
|
525
|
+
} catch (err) {
|
|
526
|
+
throw new CliError(
|
|
527
|
+
ErrorCodes.CONFIG_INVALID,
|
|
528
|
+
`Failed to parse .notion.yaml`,
|
|
529
|
+
"Check that the file contains valid YAML",
|
|
530
|
+
err
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
if (parsed.profile !== void 0 && parsed.token !== void 0) {
|
|
534
|
+
throw new CliError(
|
|
535
|
+
ErrorCodes.CONFIG_INVALID,
|
|
536
|
+
'.notion.yaml cannot specify both "profile" and "token"',
|
|
537
|
+
'Use either "profile: <name>" to reference a saved profile, or "token: <value>" for a direct token'
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
return parsed;
|
|
236
541
|
}
|
|
237
542
|
|
|
238
|
-
// src/
|
|
239
|
-
function
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
543
|
+
// src/config/token.ts
|
|
544
|
+
function isOAuthExpired(profile) {
|
|
545
|
+
if (profile.oauth_expiry_ms == null) return false;
|
|
546
|
+
return Date.now() >= profile.oauth_expiry_ms;
|
|
547
|
+
}
|
|
548
|
+
async function resolveOAuthToken(profileName, profile) {
|
|
549
|
+
if (!profile.oauth_access_token) return null;
|
|
550
|
+
if (!isOAuthExpired(profile)) {
|
|
551
|
+
return profile.oauth_access_token;
|
|
552
|
+
}
|
|
553
|
+
if (!profile.oauth_refresh_token) {
|
|
554
|
+
await clearOAuthTokens(profileName);
|
|
555
|
+
throw new CliError(
|
|
556
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
557
|
+
"OAuth session expired and no refresh token is available.",
|
|
558
|
+
'Run "notion auth login" to re-authenticate'
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
const refreshed = await refreshAccessToken(profile.oauth_refresh_token);
|
|
563
|
+
await saveOAuthTokens(profileName, refreshed);
|
|
564
|
+
return refreshed.access_token;
|
|
565
|
+
} catch (err) {
|
|
566
|
+
await clearOAuthTokens(profileName);
|
|
567
|
+
throw new CliError(
|
|
568
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
569
|
+
'OAuth session expired. Run "notion auth login" to re-authenticate.',
|
|
570
|
+
"Your session was revoked or the refresh token has expired",
|
|
571
|
+
err
|
|
572
|
+
);
|
|
573
|
+
}
|
|
244
574
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
575
|
+
async function resolveProfileToken(profileName, profile) {
|
|
576
|
+
const oauthToken = await resolveOAuthToken(profileName, profile);
|
|
577
|
+
if (oauthToken) {
|
|
578
|
+
return { token: oauthToken, source: "oauth" };
|
|
579
|
+
}
|
|
580
|
+
if (profile.token) {
|
|
581
|
+
return { token: profile.token, source: `profile: ${profileName}` };
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
async function resolveToken() {
|
|
586
|
+
const envToken = process.env.NOTION_API_TOKEN;
|
|
587
|
+
if (envToken) {
|
|
588
|
+
return { token: envToken, source: "NOTION_API_TOKEN" };
|
|
589
|
+
}
|
|
590
|
+
const localConfig = await readLocalConfig();
|
|
591
|
+
if (localConfig !== null) {
|
|
592
|
+
if (localConfig.token) {
|
|
593
|
+
return { token: localConfig.token, source: ".notion.yaml" };
|
|
594
|
+
}
|
|
595
|
+
if (localConfig.profile) {
|
|
596
|
+
const globalConfig2 = await readGlobalConfig();
|
|
597
|
+
const profile = globalConfig2.profiles?.[localConfig.profile];
|
|
598
|
+
if (profile) {
|
|
599
|
+
const result = await resolveProfileToken(localConfig.profile, profile);
|
|
600
|
+
if (result) return result;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const globalConfig = await readGlobalConfig();
|
|
605
|
+
if (globalConfig.active_profile) {
|
|
606
|
+
const profile = globalConfig.profiles?.[globalConfig.active_profile];
|
|
607
|
+
if (profile) {
|
|
608
|
+
const result = await resolveProfileToken(
|
|
609
|
+
globalConfig.active_profile,
|
|
610
|
+
profile
|
|
611
|
+
);
|
|
612
|
+
if (result) return result;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
throw new CliError(
|
|
616
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
617
|
+
"No authentication token found.",
|
|
618
|
+
'Run "notion init" to set up a profile'
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/errors/error-handler.ts
|
|
623
|
+
function mapNotionErrorCode(code) {
|
|
624
|
+
switch (code) {
|
|
625
|
+
case "unauthorized":
|
|
626
|
+
return ErrorCodes.AUTH_INVALID;
|
|
627
|
+
case "rate_limited":
|
|
628
|
+
return ErrorCodes.API_RATE_LIMITED;
|
|
629
|
+
case "object_not_found":
|
|
630
|
+
return ErrorCodes.API_NOT_FOUND;
|
|
631
|
+
default:
|
|
632
|
+
return ErrorCodes.API_ERROR;
|
|
257
633
|
}
|
|
258
634
|
}
|
|
259
635
|
function withErrorHandling(fn) {
|
|
@@ -262,7 +638,8 @@ function withErrorHandling(fn) {
|
|
|
262
638
|
await fn(...args);
|
|
263
639
|
} catch (error2) {
|
|
264
640
|
if (error2 instanceof CliError) {
|
|
265
|
-
process.stderr.write(error2.format()
|
|
641
|
+
process.stderr.write(`${error2.format()}
|
|
642
|
+
`);
|
|
266
643
|
process.exit(1);
|
|
267
644
|
}
|
|
268
645
|
const { isNotionClientError: isNotionClientError2 } = await import("@notionhq/client");
|
|
@@ -273,7 +650,8 @@ function withErrorHandling(fn) {
|
|
|
273
650
|
error2.message,
|
|
274
651
|
code === ErrorCodes.AUTH_INVALID ? 'Run "notion init" to reconfigure your integration token' : void 0
|
|
275
652
|
);
|
|
276
|
-
process.stderr.write(mappedError.format()
|
|
653
|
+
process.stderr.write(`${mappedError.format()}
|
|
654
|
+
`);
|
|
277
655
|
process.exit(1);
|
|
278
656
|
}
|
|
279
657
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
@@ -284,149 +662,828 @@ function withErrorHandling(fn) {
|
|
|
284
662
|
});
|
|
285
663
|
}
|
|
286
664
|
|
|
287
|
-
// src/
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
665
|
+
// src/notion/client.ts
|
|
666
|
+
import { APIErrorCode, Client, isNotionClientError } from "@notionhq/client";
|
|
667
|
+
async function validateToken(token) {
|
|
668
|
+
const notion = new Client({ auth: token });
|
|
669
|
+
try {
|
|
670
|
+
const me = await notion.users.me({});
|
|
671
|
+
const bot = me;
|
|
672
|
+
const workspaceName = bot.bot?.workspace_name ?? "Unknown Workspace";
|
|
673
|
+
const workspaceId = bot.bot?.workspace_id ?? "";
|
|
674
|
+
return { workspaceName, workspaceId };
|
|
675
|
+
} catch (error2) {
|
|
676
|
+
if (isNotionClientError(error2) && error2.code === APIErrorCode.Unauthorized) {
|
|
292
677
|
throw new CliError(
|
|
293
|
-
ErrorCodes.
|
|
294
|
-
"
|
|
295
|
-
"
|
|
678
|
+
ErrorCodes.AUTH_INVALID,
|
|
679
|
+
"Invalid integration token.",
|
|
680
|
+
"Check your token at notion.so/profile/integrations/internal",
|
|
681
|
+
error2
|
|
296
682
|
);
|
|
297
683
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
684
|
+
throw error2;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function createNotionClient(token) {
|
|
688
|
+
return new Client({ auth: token, timeoutMs: 12e4 });
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/notion/url-parser.ts
|
|
692
|
+
var NOTION_ID_REGEX = /^[0-9a-f]{32}$/i;
|
|
693
|
+
var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
694
|
+
var NOTION_URL_REGEX = /https?:\/\/(?:[a-zA-Z0-9-]+\.)?notion\.(?:so|site)\/.*?([0-9a-f]{32})(?:[?#]|$)/i;
|
|
695
|
+
function throwInvalidId(input2) {
|
|
696
|
+
throw new CliError(
|
|
697
|
+
ErrorCodes.INVALID_ID,
|
|
698
|
+
`Cannot parse Notion ID from: ${input2}`,
|
|
699
|
+
"Provide a valid Notion URL or page/database ID"
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
function parseNotionId(input2) {
|
|
703
|
+
if (!input2) throwInvalidId(input2);
|
|
704
|
+
if (NOTION_ID_REGEX.test(input2)) {
|
|
705
|
+
return input2.toLowerCase();
|
|
706
|
+
}
|
|
707
|
+
if (UUID_REGEX.test(input2)) {
|
|
708
|
+
return input2.replace(/-/g, "").toLowerCase();
|
|
709
|
+
}
|
|
710
|
+
const urlMatch = NOTION_URL_REGEX.exec(input2);
|
|
711
|
+
if (urlMatch) {
|
|
712
|
+
return urlMatch[1].toLowerCase();
|
|
713
|
+
}
|
|
714
|
+
throwInvalidId(input2);
|
|
715
|
+
}
|
|
716
|
+
function toUuid(id) {
|
|
717
|
+
return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/output/color.ts
|
|
721
|
+
import { Chalk } from "chalk";
|
|
722
|
+
var _colorForced = false;
|
|
723
|
+
function setColorForced(forced) {
|
|
724
|
+
_colorForced = forced;
|
|
725
|
+
}
|
|
726
|
+
function isColorEnabled() {
|
|
727
|
+
if (process.env.NO_COLOR) return false;
|
|
728
|
+
if (_colorForced) return true;
|
|
729
|
+
return Boolean(process.stderr.isTTY);
|
|
730
|
+
}
|
|
731
|
+
function createChalk() {
|
|
732
|
+
const level = isColorEnabled() ? void 0 : 0;
|
|
733
|
+
return new Chalk({ level });
|
|
734
|
+
}
|
|
735
|
+
function error(msg) {
|
|
736
|
+
return createChalk().red(msg);
|
|
737
|
+
}
|
|
738
|
+
function success(msg) {
|
|
739
|
+
return createChalk().green(msg);
|
|
740
|
+
}
|
|
741
|
+
function dim(msg) {
|
|
742
|
+
return createChalk().dim(msg);
|
|
743
|
+
}
|
|
744
|
+
function bold(msg) {
|
|
745
|
+
return createChalk().bold(msg);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/output/stderr.ts
|
|
749
|
+
function stderrWrite(msg) {
|
|
750
|
+
process.stderr.write(`${msg}
|
|
751
|
+
`);
|
|
752
|
+
}
|
|
753
|
+
function reportTokenSource(source) {
|
|
754
|
+
stderrWrite(dim(`Using token from ${source}`));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/services/write.service.ts
|
|
758
|
+
async function addComment(client, pageId, text, options = {}) {
|
|
759
|
+
await client.comments.create({
|
|
760
|
+
parent: { page_id: pageId },
|
|
761
|
+
rich_text: [
|
|
762
|
+
{
|
|
763
|
+
type: "text",
|
|
764
|
+
text: { content: text, link: null },
|
|
765
|
+
annotations: {
|
|
766
|
+
bold: false,
|
|
767
|
+
italic: false,
|
|
768
|
+
strikethrough: false,
|
|
769
|
+
underline: false,
|
|
770
|
+
code: false,
|
|
771
|
+
color: "default"
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
],
|
|
775
|
+
...options.asUser && { display_name: { type: "user" } }
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
async function appendBlocks(client, blockId, blocks) {
|
|
779
|
+
await client.blocks.children.append({
|
|
780
|
+
block_id: blockId,
|
|
781
|
+
children: blocks
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
async function createPage(client, parentId, title, blocks) {
|
|
785
|
+
const response = await client.pages.create({
|
|
786
|
+
parent: { type: "page_id", page_id: parentId },
|
|
787
|
+
properties: {
|
|
788
|
+
title: {
|
|
789
|
+
title: [{ type: "text", text: { content: title, link: null } }]
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
children: blocks
|
|
793
|
+
});
|
|
794
|
+
return response.url;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/commands/append.ts
|
|
798
|
+
function appendCommand() {
|
|
799
|
+
const cmd = new Command("append");
|
|
800
|
+
cmd.description("append markdown content to a Notion page").argument("<id/url>", "Notion page ID or URL").requiredOption("-m, --message <markdown>", "markdown content to append").action(
|
|
801
|
+
withErrorHandling(async (idOrUrl, opts) => {
|
|
802
|
+
const { token, source } = await resolveToken();
|
|
803
|
+
reportTokenSource(source);
|
|
804
|
+
const client = createNotionClient(token);
|
|
805
|
+
const pageId = parseNotionId(idOrUrl);
|
|
806
|
+
const uuid = toUuid(pageId);
|
|
807
|
+
const blocks = mdToBlocks(opts.message);
|
|
808
|
+
if (blocks.length === 0) {
|
|
809
|
+
process.stdout.write("Nothing to append.\n");
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
await appendBlocks(client, uuid, blocks);
|
|
813
|
+
process.stdout.write(`Appended ${blocks.length} block(s).
|
|
814
|
+
`);
|
|
815
|
+
})
|
|
816
|
+
);
|
|
817
|
+
return cmd;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/commands/auth/index.ts
|
|
821
|
+
function authDefaultAction(authCmd2) {
|
|
822
|
+
return async () => {
|
|
823
|
+
authCmd2.help();
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/commands/auth/login.ts
|
|
828
|
+
import { select } from "@inquirer/prompts";
|
|
829
|
+
import { Command as Command3 } from "commander";
|
|
830
|
+
|
|
831
|
+
// src/oauth/oauth-flow.ts
|
|
832
|
+
import { spawn } from "child_process";
|
|
833
|
+
import { randomBytes } from "crypto";
|
|
834
|
+
import {
|
|
835
|
+
createServer
|
|
836
|
+
} from "http";
|
|
837
|
+
import { createInterface } from "readline";
|
|
838
|
+
function openBrowser(url) {
|
|
839
|
+
const platform = process.platform;
|
|
840
|
+
let cmd;
|
|
841
|
+
let args;
|
|
842
|
+
if (platform === "darwin") {
|
|
843
|
+
cmd = "open";
|
|
844
|
+
args = [url];
|
|
845
|
+
} else if (platform === "win32") {
|
|
846
|
+
cmd = "cmd";
|
|
847
|
+
args = ["/c", "start", url];
|
|
848
|
+
} else {
|
|
849
|
+
cmd = "xdg-open";
|
|
850
|
+
args = [url];
|
|
851
|
+
}
|
|
852
|
+
try {
|
|
853
|
+
const child = spawn(cmd, args, {
|
|
854
|
+
detached: true,
|
|
855
|
+
stdio: "ignore"
|
|
856
|
+
});
|
|
857
|
+
child.unref();
|
|
858
|
+
return true;
|
|
859
|
+
} catch {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
async function manualFlow(url) {
|
|
864
|
+
process.stderr.write(
|
|
865
|
+
`
|
|
866
|
+
Opening browser to:
|
|
867
|
+
${url}
|
|
868
|
+
|
|
869
|
+
Paste the full redirect URL here (${OAUTH_REDIRECT_URI}?code=...):
|
|
870
|
+
> `
|
|
871
|
+
);
|
|
872
|
+
const rl = createInterface({
|
|
873
|
+
input: process.stdin,
|
|
874
|
+
output: process.stderr,
|
|
875
|
+
terminal: false
|
|
876
|
+
});
|
|
877
|
+
return new Promise((resolve, reject) => {
|
|
878
|
+
rl.once("line", (line) => {
|
|
879
|
+
rl.close();
|
|
880
|
+
try {
|
|
881
|
+
const parsed = new URL(line.trim());
|
|
882
|
+
const code = parsed.searchParams.get("code");
|
|
883
|
+
const state = parsed.searchParams.get("state");
|
|
884
|
+
const errorParam = parsed.searchParams.get("error");
|
|
885
|
+
if (errorParam === "access_denied") {
|
|
886
|
+
reject(
|
|
887
|
+
new CliError(
|
|
888
|
+
ErrorCodes.AUTH_INVALID,
|
|
889
|
+
"Notion OAuth access was denied.",
|
|
890
|
+
'Run "notion auth login" to try again'
|
|
891
|
+
)
|
|
892
|
+
);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (!code || !state) {
|
|
896
|
+
reject(
|
|
897
|
+
new CliError(
|
|
898
|
+
ErrorCodes.AUTH_INVALID,
|
|
899
|
+
"Invalid redirect URL \u2014 missing code or state parameter.",
|
|
900
|
+
"Make sure you paste the full redirect URL from the browser address bar"
|
|
901
|
+
)
|
|
902
|
+
);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
resolve({ code, state });
|
|
906
|
+
} catch {
|
|
907
|
+
reject(
|
|
908
|
+
new CliError(
|
|
909
|
+
ErrorCodes.AUTH_INVALID,
|
|
910
|
+
"Could not parse the pasted URL.",
|
|
911
|
+
"Make sure you paste the full redirect URL from the browser address bar"
|
|
912
|
+
)
|
|
913
|
+
);
|
|
914
|
+
}
|
|
301
915
|
});
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
916
|
+
rl.once("close", () => {
|
|
917
|
+
reject(
|
|
918
|
+
new CliError(
|
|
919
|
+
ErrorCodes.AUTH_INVALID,
|
|
920
|
+
"No redirect URL received.",
|
|
921
|
+
'Run "notion auth login" to try again'
|
|
922
|
+
)
|
|
923
|
+
);
|
|
305
924
|
});
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
function handleCallbackRequest(req, res, ctx) {
|
|
928
|
+
if (ctx.settled) {
|
|
929
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
930
|
+
res.end(
|
|
931
|
+
"<html><body><h1>Already handled. You can close this tab.</h1></body></html>"
|
|
932
|
+
);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
try {
|
|
936
|
+
const reqUrl = new URL(req.url ?? "/", "http://localhost:54321");
|
|
937
|
+
const code = reqUrl.searchParams.get("code");
|
|
938
|
+
const returnedState = reqUrl.searchParams.get("state");
|
|
939
|
+
const errorParam = reqUrl.searchParams.get("error");
|
|
940
|
+
if (errorParam === "access_denied") {
|
|
941
|
+
ctx.settled = true;
|
|
942
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
943
|
+
res.end(
|
|
944
|
+
"<html><body><h1>Access Denied</h1><p>You cancelled the Notion OAuth request. You can close this tab.</p></body></html>"
|
|
945
|
+
);
|
|
946
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
947
|
+
ctx.server.close(() => {
|
|
948
|
+
ctx.reject(
|
|
949
|
+
new CliError(
|
|
950
|
+
ErrorCodes.AUTH_INVALID,
|
|
951
|
+
"Notion OAuth access was denied.",
|
|
952
|
+
'Run "notion auth login" to try again'
|
|
953
|
+
)
|
|
954
|
+
);
|
|
314
955
|
});
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (!code || !returnedState) {
|
|
959
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
960
|
+
res.end("<html><body><p>Waiting for OAuth callback...</p></body></html>");
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (returnedState !== ctx.expectedState) {
|
|
964
|
+
ctx.settled = true;
|
|
965
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
966
|
+
res.end(
|
|
967
|
+
"<html><body><h1>Security Error</h1><p>State mismatch \u2014 possible CSRF attempt. You can close this tab.</p></body></html>"
|
|
968
|
+
);
|
|
969
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
970
|
+
ctx.server.close(() => {
|
|
971
|
+
ctx.reject(
|
|
972
|
+
new CliError(
|
|
973
|
+
ErrorCodes.AUTH_INVALID,
|
|
974
|
+
"OAuth state mismatch \u2014 possible CSRF attempt. Aborting.",
|
|
975
|
+
'Run "notion auth login" to start a fresh OAuth flow'
|
|
976
|
+
)
|
|
977
|
+
);
|
|
978
|
+
});
|
|
979
|
+
return;
|
|
319
980
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
981
|
+
ctx.settled = true;
|
|
982
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
983
|
+
res.end(
|
|
984
|
+
"<html><body><h1>Authenticated!</h1><p>You can close this tab and return to the terminal.</p></body></html>"
|
|
985
|
+
);
|
|
986
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
987
|
+
ctx.server.close(() => {
|
|
988
|
+
ctx.resolve({ code, state: returnedState });
|
|
989
|
+
});
|
|
990
|
+
} catch {
|
|
991
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
992
|
+
res.end("<html><body><h1>Error processing callback</h1></body></html>");
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
async function runOAuthFlow(options) {
|
|
996
|
+
const state = randomBytes(16).toString("hex");
|
|
997
|
+
const authUrl = buildAuthUrl(state);
|
|
998
|
+
if (options?.manual) {
|
|
999
|
+
return manualFlow(authUrl);
|
|
1000
|
+
}
|
|
1001
|
+
return new Promise((resolve, reject) => {
|
|
1002
|
+
const ctx = {
|
|
1003
|
+
expectedState: state,
|
|
1004
|
+
settled: false,
|
|
1005
|
+
timeoutHandle: null,
|
|
1006
|
+
resolve,
|
|
1007
|
+
reject,
|
|
1008
|
+
// assigned below after server is created
|
|
1009
|
+
server: null
|
|
325
1010
|
};
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
1011
|
+
const server = createServer(
|
|
1012
|
+
(req, res) => handleCallbackRequest(req, res, ctx)
|
|
1013
|
+
);
|
|
1014
|
+
ctx.server = server;
|
|
1015
|
+
server.on("error", (err) => {
|
|
1016
|
+
if (ctx.settled) return;
|
|
1017
|
+
ctx.settled = true;
|
|
1018
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
1019
|
+
reject(
|
|
1020
|
+
new CliError(
|
|
1021
|
+
ErrorCodes.AUTH_INVALID,
|
|
1022
|
+
`Failed to start OAuth callback server: ${err.message}`,
|
|
1023
|
+
"Make sure port 54321 is not in use, or use --manual flag"
|
|
1024
|
+
)
|
|
1025
|
+
);
|
|
330
1026
|
});
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
stderrWrite(" To grant access, open any Notion page or database:");
|
|
340
|
-
stderrWrite(" 1. Click \xB7\xB7\xB7 (three dots) in the top-right corner");
|
|
341
|
-
stderrWrite(' 2. Select "Connect to"');
|
|
342
|
-
stderrWrite(` 3. Choose "${workspaceName}"`);
|
|
343
|
-
stderrWrite(" Then re-run any notion command to confirm access.");
|
|
344
|
-
} else {
|
|
345
|
-
stderrWrite(success(`\u2713 Integration has access to content in ${bold(workspaceName)}.`));
|
|
1027
|
+
server.listen(54321, "127.0.0.1", () => {
|
|
1028
|
+
const browserOpened = openBrowser(authUrl);
|
|
1029
|
+
if (!browserOpened) {
|
|
1030
|
+
server.close();
|
|
1031
|
+
ctx.settled = true;
|
|
1032
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
1033
|
+
manualFlow(authUrl).then(resolve, reject);
|
|
1034
|
+
return;
|
|
346
1035
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
1036
|
+
process.stderr.write(
|
|
1037
|
+
`
|
|
1038
|
+
Opening browser for Notion OAuth...
|
|
1039
|
+
If your browser didn't open, visit:
|
|
1040
|
+
${authUrl}
|
|
1041
|
+
|
|
1042
|
+
Waiting for callback (up to 120 seconds)...
|
|
1043
|
+
`
|
|
1044
|
+
);
|
|
1045
|
+
ctx.timeoutHandle = setTimeout(() => {
|
|
1046
|
+
if (ctx.settled) return;
|
|
1047
|
+
ctx.settled = true;
|
|
1048
|
+
server.close(() => {
|
|
1049
|
+
reject(
|
|
1050
|
+
new CliError(
|
|
1051
|
+
ErrorCodes.AUTH_INVALID,
|
|
1052
|
+
"OAuth login timed out after 120 seconds.",
|
|
1053
|
+
'Run "notion auth login" to try again, or use --manual flag'
|
|
1054
|
+
)
|
|
1055
|
+
);
|
|
1056
|
+
});
|
|
1057
|
+
}, 12e4);
|
|
1058
|
+
});
|
|
1059
|
+
});
|
|
352
1060
|
}
|
|
353
1061
|
|
|
354
|
-
// src/commands/
|
|
1062
|
+
// src/commands/init.ts
|
|
1063
|
+
import { confirm, input, password } from "@inquirer/prompts";
|
|
355
1064
|
import { Command as Command2 } from "commander";
|
|
356
|
-
function
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
1065
|
+
async function runInitFlow() {
|
|
1066
|
+
const profileName = await input({
|
|
1067
|
+
message: "Profile name:",
|
|
1068
|
+
default: "default"
|
|
1069
|
+
});
|
|
1070
|
+
const token = await password({
|
|
1071
|
+
message: "Integration token (from notion.so/profile/integrations/internal):",
|
|
1072
|
+
mask: "*"
|
|
1073
|
+
});
|
|
1074
|
+
stderrWrite("Validating token...");
|
|
1075
|
+
const { workspaceName, workspaceId } = await validateToken(token);
|
|
1076
|
+
stderrWrite(success(`\u2713 Connected to workspace: ${bold(workspaceName)}`));
|
|
1077
|
+
const config = await readGlobalConfig();
|
|
1078
|
+
if (config.profiles?.[profileName]) {
|
|
1079
|
+
const replace = await confirm({
|
|
1080
|
+
message: `Profile "${profileName}" already exists. Replace?`,
|
|
1081
|
+
default: false
|
|
1082
|
+
});
|
|
1083
|
+
if (!replace) {
|
|
1084
|
+
stderrWrite("Aborted.");
|
|
364
1085
|
return;
|
|
365
1086
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
1087
|
+
}
|
|
1088
|
+
const profiles = config.profiles ?? {};
|
|
1089
|
+
profiles[profileName] = {
|
|
1090
|
+
token,
|
|
1091
|
+
workspace_name: workspaceName,
|
|
1092
|
+
workspace_id: workspaceId
|
|
1093
|
+
};
|
|
1094
|
+
await writeGlobalConfig({
|
|
1095
|
+
...config,
|
|
1096
|
+
profiles,
|
|
1097
|
+
active_profile: profileName
|
|
1098
|
+
});
|
|
1099
|
+
stderrWrite(success(`Profile "${profileName}" saved and set as active.`));
|
|
1100
|
+
stderrWrite(dim("Checking integration access..."));
|
|
1101
|
+
try {
|
|
1102
|
+
const notion = createNotionClient(token);
|
|
1103
|
+
const probe = await notion.search({ page_size: 1 });
|
|
1104
|
+
if (probe.results.length === 0) {
|
|
1105
|
+
stderrWrite("");
|
|
1106
|
+
stderrWrite("\u26A0\uFE0F Your integration has no pages connected.");
|
|
1107
|
+
stderrWrite(" To grant access, open any Notion page or database:");
|
|
1108
|
+
stderrWrite(" 1. Click \xB7\xB7\xB7 (three dots) in the top-right corner");
|
|
1109
|
+
stderrWrite(' 2. Select "Connect to"');
|
|
1110
|
+
stderrWrite(` 3. Choose "${workspaceName}"`);
|
|
1111
|
+
stderrWrite(" Then re-run any notion command to confirm access.");
|
|
1112
|
+
} else {
|
|
1113
|
+
stderrWrite(
|
|
1114
|
+
success(
|
|
1115
|
+
`\u2713 Integration has access to content in ${bold(workspaceName)}.`
|
|
1116
|
+
)
|
|
391
1117
|
);
|
|
392
1118
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
1119
|
+
} catch {
|
|
1120
|
+
stderrWrite(
|
|
1121
|
+
dim("(Could not verify integration access \u2014 run `notion ls` to check)")
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
stderrWrite("");
|
|
1125
|
+
stderrWrite(
|
|
1126
|
+
dim("Write commands (comment, append, create-page) require additional")
|
|
1127
|
+
);
|
|
1128
|
+
stderrWrite(dim("capabilities in your integration settings:"));
|
|
1129
|
+
stderrWrite(
|
|
1130
|
+
dim(" notion.so/profile/integrations/internal \u2192 your integration \u2192")
|
|
1131
|
+
);
|
|
1132
|
+
stderrWrite(
|
|
1133
|
+
dim(
|
|
1134
|
+
' Capabilities: enable "Read content", "Insert content", "Read comments", "Insert comments"'
|
|
1135
|
+
)
|
|
1136
|
+
);
|
|
1137
|
+
stderrWrite("");
|
|
1138
|
+
stderrWrite(
|
|
1139
|
+
dim("To post comments and create pages attributed to your user account:")
|
|
1140
|
+
);
|
|
1141
|
+
stderrWrite(dim(" notion auth login"));
|
|
1142
|
+
}
|
|
1143
|
+
function initCommand() {
|
|
1144
|
+
const cmd = new Command2("init");
|
|
1145
|
+
cmd.description("authenticate with Notion and save a profile").action(
|
|
1146
|
+
withErrorHandling(async () => {
|
|
1147
|
+
if (!process.stdin.isTTY) {
|
|
1148
|
+
throw new CliError(
|
|
1149
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
1150
|
+
"Cannot run interactive init in non-TTY mode.",
|
|
1151
|
+
"Set NOTION_API_TOKEN environment variable or create .notion.yaml"
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
await runInitFlow();
|
|
1155
|
+
})
|
|
1156
|
+
);
|
|
399
1157
|
return cmd;
|
|
400
1158
|
}
|
|
401
1159
|
|
|
402
|
-
// src/commands/
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
1160
|
+
// src/commands/auth/login.ts
|
|
1161
|
+
function loginCommand() {
|
|
1162
|
+
const cmd = new Command3("login");
|
|
1163
|
+
cmd.description("authenticate with Notion \u2014 choose OAuth or integration token").option("--profile <name>", "profile name to store credentials in").option(
|
|
1164
|
+
"--manual",
|
|
1165
|
+
"print auth URL instead of opening browser (for headless OAuth)"
|
|
1166
|
+
).action(
|
|
1167
|
+
withErrorHandling(async (opts) => {
|
|
1168
|
+
if (!process.stdin.isTTY && !opts.manual) {
|
|
1169
|
+
throw new CliError(
|
|
1170
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
1171
|
+
"Cannot run interactive login in non-TTY mode.",
|
|
1172
|
+
"Use --manual flag to get an auth URL you can open in a browser"
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
const method = await select({
|
|
1176
|
+
message: "How do you want to authenticate with Notion?",
|
|
1177
|
+
choices: [
|
|
1178
|
+
{
|
|
1179
|
+
name: "OAuth user login (browser required)",
|
|
1180
|
+
value: "oauth",
|
|
1181
|
+
description: "Opens Notion in your browser. Comments and pages are attributed to your account. Tokens auto-refresh."
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
name: "Internal integration token (CI/headless friendly)",
|
|
1185
|
+
value: "token",
|
|
1186
|
+
description: "Paste a token from notion.so/profile/integrations. No browser needed. Write ops attributed to integration bot."
|
|
1187
|
+
}
|
|
1188
|
+
]
|
|
1189
|
+
});
|
|
1190
|
+
if (method === "oauth") {
|
|
1191
|
+
let profileName = opts.profile;
|
|
1192
|
+
if (!profileName) {
|
|
1193
|
+
const config = await readGlobalConfig();
|
|
1194
|
+
profileName = config.active_profile ?? "default";
|
|
1195
|
+
}
|
|
1196
|
+
const result = await runOAuthFlow({ manual: opts.manual });
|
|
1197
|
+
const response = await exchangeCode(result.code);
|
|
1198
|
+
await saveOAuthTokens(profileName, response);
|
|
1199
|
+
const userName = response.owner?.user?.name ?? "unknown user";
|
|
1200
|
+
const workspaceName = response.workspace_name ?? "unknown workspace";
|
|
1201
|
+
stderrWrite(
|
|
1202
|
+
success(`\u2713 Logged in as ${userName} to workspace ${workspaceName}`)
|
|
1203
|
+
);
|
|
1204
|
+
stderrWrite(
|
|
1205
|
+
dim(
|
|
1206
|
+
"Your comments and pages will now be attributed to your Notion account."
|
|
1207
|
+
)
|
|
1208
|
+
);
|
|
1209
|
+
} else {
|
|
1210
|
+
await runInitFlow();
|
|
1211
|
+
}
|
|
1212
|
+
})
|
|
1213
|
+
);
|
|
425
1214
|
return cmd;
|
|
426
1215
|
}
|
|
427
1216
|
|
|
428
|
-
// src/commands/
|
|
1217
|
+
// src/commands/auth/logout.ts
|
|
1218
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
1219
|
+
import { Command as Command4 } from "commander";
|
|
1220
|
+
function profileLabel(name, profile) {
|
|
1221
|
+
const parts = [];
|
|
1222
|
+
if (profile.oauth_access_token)
|
|
1223
|
+
parts.push(
|
|
1224
|
+
`OAuth${profile.oauth_user_name ? ` (${profile.oauth_user_name})` : ""}`
|
|
1225
|
+
);
|
|
1226
|
+
if (profile.token) parts.push("integration token");
|
|
1227
|
+
const authDesc = parts.length > 0 ? parts.join(" + ") : "no credentials";
|
|
1228
|
+
const workspace = profile.workspace_name ? dim(` \u2014 ${profile.workspace_name}`) : "";
|
|
1229
|
+
return `${bold(name)} ${dim(authDesc)}${workspace}`;
|
|
1230
|
+
}
|
|
1231
|
+
function logoutCommand() {
|
|
1232
|
+
const cmd = new Command4("logout");
|
|
1233
|
+
cmd.description("remove a profile and its credentials").option(
|
|
1234
|
+
"--profile <name>",
|
|
1235
|
+
"profile name to remove (skips interactive selector)"
|
|
1236
|
+
).action(
|
|
1237
|
+
withErrorHandling(async (opts) => {
|
|
1238
|
+
const config = await readGlobalConfig();
|
|
1239
|
+
const profiles = config.profiles ?? {};
|
|
1240
|
+
const profileNames = Object.keys(profiles);
|
|
1241
|
+
if (profileNames.length === 0) {
|
|
1242
|
+
stderrWrite("No profiles configured.");
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
let profileName = opts.profile;
|
|
1246
|
+
if (!profileName) {
|
|
1247
|
+
if (!process.stdin.isTTY) {
|
|
1248
|
+
throw new CliError(
|
|
1249
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
1250
|
+
"Cannot run interactive logout in non-TTY mode.",
|
|
1251
|
+
"Use --profile <name> to specify the profile to remove"
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
profileName = await select2({
|
|
1255
|
+
message: "Which profile do you want to log out of?",
|
|
1256
|
+
choices: profileNames.map((name) => ({
|
|
1257
|
+
// biome-ignore lint/style/noNonNullAssertion: key is from Object.keys, always present
|
|
1258
|
+
name: profileLabel(name, profiles[name]),
|
|
1259
|
+
value: name
|
|
1260
|
+
}))
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
if (!profiles[profileName]) {
|
|
1264
|
+
throw new CliError(
|
|
1265
|
+
ErrorCodes.AUTH_PROFILE_NOT_FOUND,
|
|
1266
|
+
`Profile "${profileName}" not found.`,
|
|
1267
|
+
`Run "notion auth list" to see available profiles`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
const updatedProfiles = { ...profiles };
|
|
1271
|
+
delete updatedProfiles[profileName];
|
|
1272
|
+
const newActiveProfile = config.active_profile === profileName ? void 0 : config.active_profile;
|
|
1273
|
+
await writeGlobalConfig({
|
|
1274
|
+
...config,
|
|
1275
|
+
profiles: updatedProfiles,
|
|
1276
|
+
active_profile: newActiveProfile
|
|
1277
|
+
});
|
|
1278
|
+
stderrWrite(success(`\u2713 Logged out of profile "${profileName}".`));
|
|
1279
|
+
if (newActiveProfile === void 0 && Object.keys(updatedProfiles).length > 0) {
|
|
1280
|
+
stderrWrite(
|
|
1281
|
+
dim(`Run "notion auth use <name>" to set a new active profile.`)
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
})
|
|
1285
|
+
);
|
|
1286
|
+
return cmd;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// src/commands/auth/status.ts
|
|
429
1290
|
import { Command as Command5 } from "commander";
|
|
1291
|
+
function statusCommand() {
|
|
1292
|
+
const cmd = new Command5("status");
|
|
1293
|
+
cmd.description("show authentication status for the active profile").option("--profile <name>", "profile name to check").action(
|
|
1294
|
+
withErrorHandling(async (opts) => {
|
|
1295
|
+
let profileName = opts.profile;
|
|
1296
|
+
const config = await readGlobalConfig();
|
|
1297
|
+
if (!profileName) {
|
|
1298
|
+
profileName = config.active_profile ?? "default";
|
|
1299
|
+
}
|
|
1300
|
+
const profile = config.profiles?.[profileName];
|
|
1301
|
+
stderrWrite(`Profile: ${profileName}`);
|
|
1302
|
+
if (!profile) {
|
|
1303
|
+
stderrWrite(
|
|
1304
|
+
` ${error("\u2717")} No profile found (run 'notion init' to create one)`
|
|
1305
|
+
);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
if (profile.oauth_access_token) {
|
|
1309
|
+
const userName = profile.oauth_user_name ?? "unknown";
|
|
1310
|
+
const userId = profile.oauth_user_id ?? "unknown";
|
|
1311
|
+
stderrWrite(
|
|
1312
|
+
` OAuth: ${success("\u2713")} Logged in as ${userName} (user: ${userId})`
|
|
1313
|
+
);
|
|
1314
|
+
if (profile.oauth_expiry_ms != null) {
|
|
1315
|
+
const expiryDate = new Date(profile.oauth_expiry_ms).toISOString();
|
|
1316
|
+
stderrWrite(dim(` Access token expires: ${expiryDate}`));
|
|
1317
|
+
}
|
|
1318
|
+
} else {
|
|
1319
|
+
stderrWrite(
|
|
1320
|
+
` OAuth: ${error("\u2717")} Not logged in (run 'notion auth login')`
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
if (profile.token) {
|
|
1324
|
+
const tokenPreview = `${profile.token.substring(0, 10)}...`;
|
|
1325
|
+
stderrWrite(
|
|
1326
|
+
` Internal token: ${success("\u2713")} Configured (${tokenPreview})`
|
|
1327
|
+
);
|
|
1328
|
+
} else {
|
|
1329
|
+
stderrWrite(` Internal token: ${error("\u2717")} Not configured`);
|
|
1330
|
+
}
|
|
1331
|
+
if (profile.oauth_access_token) {
|
|
1332
|
+
stderrWrite(` Active method: OAuth (user-attributed)`);
|
|
1333
|
+
} else if (profile.token) {
|
|
1334
|
+
stderrWrite(
|
|
1335
|
+
` Active method: Internal integration token (bot-attributed)`
|
|
1336
|
+
);
|
|
1337
|
+
} else {
|
|
1338
|
+
stderrWrite(
|
|
1339
|
+
dim(
|
|
1340
|
+
` Active method: None (run 'notion auth login' or 'notion init')`
|
|
1341
|
+
)
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
})
|
|
1345
|
+
);
|
|
1346
|
+
return cmd;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/commands/comment-add.ts
|
|
1350
|
+
import { Command as Command6 } from "commander";
|
|
1351
|
+
function commentAddCommand() {
|
|
1352
|
+
const cmd = new Command6("comment");
|
|
1353
|
+
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(
|
|
1354
|
+
withErrorHandling(async (idOrUrl, opts) => {
|
|
1355
|
+
const { token, source } = await resolveToken();
|
|
1356
|
+
reportTokenSource(source);
|
|
1357
|
+
const client = createNotionClient(token);
|
|
1358
|
+
const id = parseNotionId(idOrUrl);
|
|
1359
|
+
const uuid = toUuid(id);
|
|
1360
|
+
await addComment(client, uuid, opts.message, {
|
|
1361
|
+
asUser: source === "oauth"
|
|
1362
|
+
});
|
|
1363
|
+
process.stdout.write("Comment added.\n");
|
|
1364
|
+
})
|
|
1365
|
+
);
|
|
1366
|
+
return cmd;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/commands/comments.ts
|
|
1370
|
+
import { Command as Command7 } from "commander";
|
|
1371
|
+
|
|
1372
|
+
// src/output/format.ts
|
|
1373
|
+
var _mode = "auto";
|
|
1374
|
+
function setOutputMode(mode) {
|
|
1375
|
+
_mode = mode;
|
|
1376
|
+
}
|
|
1377
|
+
function getOutputMode() {
|
|
1378
|
+
return _mode;
|
|
1379
|
+
}
|
|
1380
|
+
function isatty() {
|
|
1381
|
+
return Boolean(process.stdout.isTTY);
|
|
1382
|
+
}
|
|
1383
|
+
function isHumanMode() {
|
|
1384
|
+
if (_mode === "json") return false;
|
|
1385
|
+
if (_mode === "md") return false;
|
|
1386
|
+
return true;
|
|
1387
|
+
}
|
|
1388
|
+
function formatJSON(data) {
|
|
1389
|
+
return JSON.stringify(data, null, 2);
|
|
1390
|
+
}
|
|
1391
|
+
var COLUMN_CAPS = {
|
|
1392
|
+
TITLE: 50,
|
|
1393
|
+
ID: 36
|
|
1394
|
+
};
|
|
1395
|
+
var DEFAULT_MAX_COL_WIDTH = 40;
|
|
1396
|
+
function getColumnCap(header) {
|
|
1397
|
+
return COLUMN_CAPS[header.toUpperCase()] ?? DEFAULT_MAX_COL_WIDTH;
|
|
1398
|
+
}
|
|
1399
|
+
function truncate(str, maxLen) {
|
|
1400
|
+
if (str.length <= maxLen) return str;
|
|
1401
|
+
return `${str.slice(0, maxLen - 1)}\u2026`;
|
|
1402
|
+
}
|
|
1403
|
+
function formatTable(rows, headers) {
|
|
1404
|
+
const colWidths = headers.map((header, colIdx) => {
|
|
1405
|
+
const cap = getColumnCap(header);
|
|
1406
|
+
const headerLen = header.length;
|
|
1407
|
+
const maxRowLen = rows.reduce((max, row) => {
|
|
1408
|
+
const cell = row[colIdx] ?? "";
|
|
1409
|
+
return Math.max(max, cell.length);
|
|
1410
|
+
}, 0);
|
|
1411
|
+
return Math.min(Math.max(headerLen, maxRowLen), cap);
|
|
1412
|
+
});
|
|
1413
|
+
const sep = "\u2500";
|
|
1414
|
+
const colSep = " ";
|
|
1415
|
+
const headerRow = headers.map((h, i) => h.padEnd(colWidths[i])).join(colSep);
|
|
1416
|
+
const separatorRow = colWidths.map((w) => sep.repeat(w)).join(colSep);
|
|
1417
|
+
const dataRows = rows.map(
|
|
1418
|
+
(row) => headers.map((_, i) => {
|
|
1419
|
+
const cell = row[i] ?? "";
|
|
1420
|
+
return truncate(cell, colWidths[i]).padEnd(colWidths[i]);
|
|
1421
|
+
}).join(colSep)
|
|
1422
|
+
);
|
|
1423
|
+
return [headerRow, separatorRow, ...dataRows].join("\n");
|
|
1424
|
+
}
|
|
1425
|
+
function printOutput(data, tableHeaders, tableRows) {
|
|
1426
|
+
const mode = getOutputMode();
|
|
1427
|
+
if (mode === "json") {
|
|
1428
|
+
process.stdout.write(`${formatJSON(data)}
|
|
1429
|
+
`);
|
|
1430
|
+
} else if (isHumanMode() && tableHeaders && tableRows) {
|
|
1431
|
+
printWithPager(`${formatTable(tableRows, tableHeaders)}
|
|
1432
|
+
`);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
function printWithPager(text) {
|
|
1436
|
+
process.stdout.write(text);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/output/paginate.ts
|
|
1440
|
+
async function paginateResults(fetcher) {
|
|
1441
|
+
const allResults = [];
|
|
1442
|
+
let cursor;
|
|
1443
|
+
let hasMore = true;
|
|
1444
|
+
while (hasMore) {
|
|
1445
|
+
const response = await fetcher(cursor);
|
|
1446
|
+
allResults.push(...response.results);
|
|
1447
|
+
cursor = response.next_cursor ?? void 0;
|
|
1448
|
+
hasMore = response.has_more;
|
|
1449
|
+
}
|
|
1450
|
+
return allResults;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// src/commands/comments.ts
|
|
1454
|
+
function commentsCommand() {
|
|
1455
|
+
const cmd = new Command7("comments");
|
|
1456
|
+
cmd.description("list comments on a Notion page").argument("<id/url>", "Notion page ID or URL").option("--json", "output as JSON").action(
|
|
1457
|
+
withErrorHandling(async (idOrUrl, opts) => {
|
|
1458
|
+
if (opts.json) setOutputMode("json");
|
|
1459
|
+
const id = parseNotionId(idOrUrl);
|
|
1460
|
+
const uuid = toUuid(id);
|
|
1461
|
+
const { token, source } = await resolveToken();
|
|
1462
|
+
reportTokenSource(source);
|
|
1463
|
+
const notion = createNotionClient(token);
|
|
1464
|
+
const comments = await paginateResults(
|
|
1465
|
+
(cursor) => notion.comments.list({ block_id: uuid, start_cursor: cursor })
|
|
1466
|
+
);
|
|
1467
|
+
if (comments.length === 0) {
|
|
1468
|
+
process.stdout.write("No comments found on this page\n");
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
const rows = comments.map((comment) => {
|
|
1472
|
+
const text = comment.rich_text.map((t) => t.plain_text).join("");
|
|
1473
|
+
return [
|
|
1474
|
+
comment.created_time.split("T")[0],
|
|
1475
|
+
`${comment.created_by.id.slice(0, 8)}...`,
|
|
1476
|
+
text.slice(0, 80) + (text.length > 80 ? "\u2026" : "")
|
|
1477
|
+
];
|
|
1478
|
+
});
|
|
1479
|
+
printOutput(comments, ["DATE", "AUTHOR ID", "COMMENT"], rows);
|
|
1480
|
+
})
|
|
1481
|
+
);
|
|
1482
|
+
return cmd;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/commands/completion.ts
|
|
1486
|
+
import { Command as Command8 } from "commander";
|
|
430
1487
|
var BASH_COMPLETION = `# notion bash completion
|
|
431
1488
|
_notion_completion() {
|
|
432
1489
|
local cur prev words cword
|
|
@@ -528,394 +1585,502 @@ complete -c notion -n '__fish_seen_subcommand_from completion' -a zsh -d 'zsh co
|
|
|
528
1585
|
complete -c notion -n '__fish_seen_subcommand_from completion' -a fish -d 'fish completion script'
|
|
529
1586
|
`;
|
|
530
1587
|
function completionCommand() {
|
|
531
|
-
const cmd = new
|
|
532
|
-
cmd.description("output shell completion script").argument("<shell>", "shell type (bash, zsh, fish)").action(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}));
|
|
551
|
-
return cmd;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// src/commands/search.ts
|
|
555
|
-
import { Command as Command6 } from "commander";
|
|
556
|
-
import { isFullPageOrDataSource } from "@notionhq/client";
|
|
557
|
-
|
|
558
|
-
// src/config/local-config.ts
|
|
559
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
560
|
-
import { join as join2 } from "path";
|
|
561
|
-
import { parse as parse2 } from "yaml";
|
|
562
|
-
async function readLocalConfig() {
|
|
563
|
-
const localConfigPath = join2(process.cwd(), ".notion.yaml");
|
|
564
|
-
let raw;
|
|
565
|
-
try {
|
|
566
|
-
raw = await readFile2(localConfigPath, "utf-8");
|
|
567
|
-
} catch (err) {
|
|
568
|
-
if (err.code === "ENOENT") {
|
|
569
|
-
return null;
|
|
570
|
-
}
|
|
571
|
-
throw new CliError(
|
|
572
|
-
ErrorCodes.CONFIG_READ_ERROR,
|
|
573
|
-
`Failed to read local config: ${localConfigPath}`,
|
|
574
|
-
"Check file permissions"
|
|
575
|
-
);
|
|
576
|
-
}
|
|
577
|
-
let parsed;
|
|
578
|
-
try {
|
|
579
|
-
parsed = parse2(raw) ?? {};
|
|
580
|
-
} catch {
|
|
581
|
-
throw new CliError(
|
|
582
|
-
ErrorCodes.CONFIG_INVALID,
|
|
583
|
-
`Failed to parse .notion.yaml`,
|
|
584
|
-
"Check that the file contains valid YAML"
|
|
585
|
-
);
|
|
586
|
-
}
|
|
587
|
-
if (parsed.profile !== void 0 && parsed.token !== void 0) {
|
|
588
|
-
throw new CliError(
|
|
589
|
-
ErrorCodes.CONFIG_INVALID,
|
|
590
|
-
'.notion.yaml cannot specify both "profile" and "token"',
|
|
591
|
-
'Use either "profile: <name>" to reference a saved profile, or "token: <value>" for a direct token'
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
return parsed;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// src/config/token.ts
|
|
598
|
-
async function resolveToken() {
|
|
599
|
-
const envToken = process.env["NOTION_API_TOKEN"];
|
|
600
|
-
if (envToken) {
|
|
601
|
-
return { token: envToken, source: "NOTION_API_TOKEN" };
|
|
602
|
-
}
|
|
603
|
-
const localConfig = await readLocalConfig();
|
|
604
|
-
if (localConfig !== null) {
|
|
605
|
-
if (localConfig.token) {
|
|
606
|
-
return { token: localConfig.token, source: ".notion.yaml" };
|
|
607
|
-
}
|
|
608
|
-
if (localConfig.profile) {
|
|
609
|
-
const globalConfig2 = await readGlobalConfig();
|
|
610
|
-
const profileToken = globalConfig2.profiles?.[localConfig.profile]?.token;
|
|
611
|
-
if (profileToken) {
|
|
612
|
-
return { token: profileToken, source: `profile: ${localConfig.profile}` };
|
|
1588
|
+
const cmd = new Command8("completion");
|
|
1589
|
+
cmd.description("output shell completion script").argument("<shell>", "shell type (bash, zsh, fish)").action(
|
|
1590
|
+
withErrorHandling(async (shell) => {
|
|
1591
|
+
switch (shell) {
|
|
1592
|
+
case "bash":
|
|
1593
|
+
process.stdout.write(BASH_COMPLETION);
|
|
1594
|
+
break;
|
|
1595
|
+
case "zsh":
|
|
1596
|
+
process.stdout.write(ZSH_COMPLETION);
|
|
1597
|
+
break;
|
|
1598
|
+
case "fish":
|
|
1599
|
+
process.stdout.write(FISH_COMPLETION);
|
|
1600
|
+
break;
|
|
1601
|
+
default:
|
|
1602
|
+
throw new CliError(
|
|
1603
|
+
ErrorCodes.UNKNOWN,
|
|
1604
|
+
`Unknown shell: "${shell}".`,
|
|
1605
|
+
"Supported shells: bash, zsh, fish"
|
|
1606
|
+
);
|
|
613
1607
|
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
const globalConfig = await readGlobalConfig();
|
|
617
|
-
if (globalConfig.active_profile) {
|
|
618
|
-
const profileToken = globalConfig.profiles?.[globalConfig.active_profile]?.token;
|
|
619
|
-
if (profileToken) {
|
|
620
|
-
return { token: profileToken, source: `profile: ${globalConfig.active_profile}` };
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
throw new CliError(
|
|
624
|
-
ErrorCodes.AUTH_NO_TOKEN,
|
|
625
|
-
"No authentication token found.",
|
|
626
|
-
'Run "notion init" to set up a profile'
|
|
1608
|
+
})
|
|
627
1609
|
);
|
|
1610
|
+
return cmd;
|
|
628
1611
|
}
|
|
629
1612
|
|
|
630
|
-
// src/commands/
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (titleProp?.type === "title") {
|
|
637
|
-
return titleProp.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
1613
|
+
// src/commands/create-page.ts
|
|
1614
|
+
import { Command as Command9 } from "commander";
|
|
1615
|
+
async function readStdin() {
|
|
1616
|
+
const chunks = [];
|
|
1617
|
+
for await (const chunk of process.stdin) {
|
|
1618
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
638
1619
|
}
|
|
639
|
-
return
|
|
640
|
-
}
|
|
641
|
-
function
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
const response = await notion.search({
|
|
663
|
-
query,
|
|
664
|
-
filter: opts.type ? { property: "object", value: toSdkFilterValue(opts.type) } : void 0,
|
|
665
|
-
start_cursor: opts.cursor,
|
|
666
|
-
page_size: 20
|
|
667
|
-
});
|
|
668
|
-
const fullResults = response.results.filter((r) => isFullPageOrDataSource(r));
|
|
669
|
-
if (fullResults.length === 0) {
|
|
670
|
-
process.stdout.write(`No results found for "${query}"
|
|
671
|
-
`);
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
const headers = ["TYPE", "TITLE", "ID", "MODIFIED"];
|
|
675
|
-
const rows = fullResults.map((item) => [
|
|
676
|
-
displayType(item),
|
|
677
|
-
getTitle(item),
|
|
678
|
-
item.id,
|
|
679
|
-
item.last_edited_time.split("T")[0]
|
|
680
|
-
]);
|
|
681
|
-
printOutput(fullResults, headers, rows);
|
|
682
|
-
if (response.has_more && response.next_cursor) {
|
|
683
|
-
process.stderr.write(`
|
|
684
|
-
--next: notion search "${query}" --cursor ${response.next_cursor}
|
|
1620
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
1621
|
+
}
|
|
1622
|
+
function createPageCommand() {
|
|
1623
|
+
const cmd = new Command9("create-page");
|
|
1624
|
+
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(
|
|
1625
|
+
"-m, --message <markdown>",
|
|
1626
|
+
"inline markdown content for the page body"
|
|
1627
|
+
).action(
|
|
1628
|
+
withErrorHandling(
|
|
1629
|
+
async (opts) => {
|
|
1630
|
+
const { token, source } = await resolveToken();
|
|
1631
|
+
reportTokenSource(source);
|
|
1632
|
+
const client = createNotionClient(token);
|
|
1633
|
+
let markdown = "";
|
|
1634
|
+
if (opts.message) {
|
|
1635
|
+
markdown = opts.message;
|
|
1636
|
+
} else if (!process.stdin.isTTY) {
|
|
1637
|
+
markdown = await readStdin();
|
|
1638
|
+
}
|
|
1639
|
+
const blocks = mdToBlocks(markdown);
|
|
1640
|
+
const parentUuid = toUuid(parseNotionId(opts.parent));
|
|
1641
|
+
const url = await createPage(client, parentUuid, opts.title, blocks);
|
|
1642
|
+
process.stdout.write(`${url}
|
|
685
1643
|
`);
|
|
686
1644
|
}
|
|
687
|
-
|
|
1645
|
+
)
|
|
688
1646
|
);
|
|
689
1647
|
return cmd;
|
|
690
1648
|
}
|
|
691
1649
|
|
|
692
|
-
// src/commands/
|
|
693
|
-
import { Command as
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
|
|
1650
|
+
// src/commands/db/query.ts
|
|
1651
|
+
import { Command as Command10 } from "commander";
|
|
1652
|
+
|
|
1653
|
+
// src/services/database.service.ts
|
|
1654
|
+
import { isFullPage } from "@notionhq/client";
|
|
1655
|
+
async function fetchDatabaseSchema(client, dbId) {
|
|
1656
|
+
const ds = await client.dataSources.retrieve({ data_source_id: dbId });
|
|
1657
|
+
const title = "title" in ds ? ds.title.map((rt) => rt.plain_text).join("") || dbId : dbId;
|
|
1658
|
+
const properties = {};
|
|
1659
|
+
if ("properties" in ds) {
|
|
1660
|
+
for (const [name, prop] of Object.entries(ds.properties)) {
|
|
1661
|
+
const config = {
|
|
1662
|
+
id: prop.id,
|
|
1663
|
+
name,
|
|
1664
|
+
type: prop.type
|
|
1665
|
+
};
|
|
1666
|
+
if (prop.type === "select" && "select" in prop) {
|
|
1667
|
+
config.options = prop.select.options;
|
|
1668
|
+
} else if (prop.type === "status" && "status" in prop) {
|
|
1669
|
+
config.options = prop.status.options;
|
|
1670
|
+
} else if (prop.type === "multi_select" && "multi_select" in prop) {
|
|
1671
|
+
config.options = prop.multi_select.options;
|
|
1672
|
+
}
|
|
1673
|
+
properties[name] = config;
|
|
1674
|
+
}
|
|
702
1675
|
}
|
|
703
|
-
return
|
|
704
|
-
}
|
|
705
|
-
function displayType2(item) {
|
|
706
|
-
return item.object === "data_source" ? "database" : item.object;
|
|
1676
|
+
return { id: dbId, title, properties };
|
|
707
1677
|
}
|
|
708
|
-
function
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
withErrorHandling(async (opts) => {
|
|
717
|
-
if (opts.json) {
|
|
718
|
-
setOutputMode("json");
|
|
719
|
-
}
|
|
720
|
-
const { token, source } = await resolveToken();
|
|
721
|
-
reportTokenSource(source);
|
|
722
|
-
const notion = createNotionClient(token);
|
|
723
|
-
const response = await notion.search({
|
|
724
|
-
start_cursor: opts.cursor,
|
|
725
|
-
page_size: 20
|
|
726
|
-
});
|
|
727
|
-
let items = response.results.filter((r) => isFullPageOrDataSource2(r));
|
|
728
|
-
if (opts.type) {
|
|
729
|
-
const filterType = opts.type;
|
|
730
|
-
items = items.filter(
|
|
731
|
-
(r) => filterType === "database" ? r.object === "data_source" : r.object === filterType
|
|
732
|
-
);
|
|
733
|
-
}
|
|
734
|
-
if (items.length === 0) {
|
|
735
|
-
process.stdout.write("No accessible content found\n");
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
const headers = ["TYPE", "TITLE", "ID", "MODIFIED"];
|
|
739
|
-
const rows = items.map((item) => [
|
|
740
|
-
displayType2(item),
|
|
741
|
-
getTitle2(item),
|
|
742
|
-
item.id,
|
|
743
|
-
item.last_edited_time.split("T")[0]
|
|
744
|
-
]);
|
|
745
|
-
printOutput(items, headers, rows);
|
|
746
|
-
if (response.has_more && response.next_cursor) {
|
|
747
|
-
process.stderr.write(`
|
|
748
|
-
--next: notion ls --cursor ${response.next_cursor}
|
|
749
|
-
`);
|
|
750
|
-
}
|
|
1678
|
+
async function queryDatabase(client, dbId, opts = {}) {
|
|
1679
|
+
const rawPages = await paginateResults(
|
|
1680
|
+
(cursor) => client.dataSources.query({
|
|
1681
|
+
data_source_id: dbId,
|
|
1682
|
+
filter: opts.filter,
|
|
1683
|
+
sorts: opts.sorts,
|
|
1684
|
+
start_cursor: cursor,
|
|
1685
|
+
page_size: 100
|
|
751
1686
|
})
|
|
752
1687
|
);
|
|
753
|
-
return
|
|
1688
|
+
return rawPages.filter(isFullPage).map((page) => {
|
|
1689
|
+
const propValues = {};
|
|
1690
|
+
for (const [name, prop] of Object.entries(page.properties)) {
|
|
1691
|
+
if (opts.columns && !opts.columns.includes(name)) continue;
|
|
1692
|
+
propValues[name] = displayPropertyValue(prop);
|
|
1693
|
+
}
|
|
1694
|
+
return { id: page.id, properties: propValues, raw: page };
|
|
1695
|
+
});
|
|
754
1696
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1697
|
+
function buildFilter(filterStrings, schema) {
|
|
1698
|
+
if (!filterStrings.length) return void 0;
|
|
1699
|
+
const filters = filterStrings.map((raw) => {
|
|
1700
|
+
const eqIdx = raw.indexOf("=");
|
|
1701
|
+
if (eqIdx === -1) {
|
|
1702
|
+
throw new CliError(
|
|
1703
|
+
ErrorCodes.INVALID_ARG,
|
|
1704
|
+
`Invalid filter syntax: "${raw}"`,
|
|
1705
|
+
'Use format: --filter "PropertyName=Value"'
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
const propName = raw.slice(0, eqIdx).trim();
|
|
1709
|
+
const value = raw.slice(eqIdx + 1).trim();
|
|
1710
|
+
const propConfig = schema.properties[propName];
|
|
1711
|
+
if (!propConfig) {
|
|
1712
|
+
const available = Object.keys(schema.properties).join(", ");
|
|
1713
|
+
throw new CliError(
|
|
1714
|
+
ErrorCodes.INVALID_ARG,
|
|
1715
|
+
`Property "${propName}" not found`,
|
|
1716
|
+
`Available properties: ${available}`
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
return buildPropertyFilter(propName, propConfig.type, value);
|
|
1720
|
+
});
|
|
1721
|
+
return filters.length === 1 ? filters[0] : { and: filters };
|
|
771
1722
|
}
|
|
772
|
-
function
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1723
|
+
function buildPropertyFilter(property, type, value) {
|
|
1724
|
+
switch (type) {
|
|
1725
|
+
case "select":
|
|
1726
|
+
return { property, select: { equals: value } };
|
|
1727
|
+
case "status":
|
|
1728
|
+
return { property, status: { equals: value } };
|
|
1729
|
+
case "multi_select":
|
|
1730
|
+
return { property, multi_select: { contains: value } };
|
|
1731
|
+
case "checkbox":
|
|
1732
|
+
return { property, checkbox: { equals: value.toLowerCase() === "true" } };
|
|
1733
|
+
case "number":
|
|
1734
|
+
return { property, number: { equals: Number(value) } };
|
|
1735
|
+
case "title":
|
|
1736
|
+
return { property, title: { contains: value } };
|
|
1737
|
+
case "rich_text":
|
|
1738
|
+
return { property, rich_text: { contains: value } };
|
|
1739
|
+
case "url":
|
|
1740
|
+
return { property, url: { contains: value } };
|
|
1741
|
+
case "email":
|
|
1742
|
+
return { property, email: { contains: value } };
|
|
1743
|
+
default:
|
|
1744
|
+
throw new CliError(
|
|
1745
|
+
ErrorCodes.INVALID_ARG,
|
|
1746
|
+
`Filtering by property type "${type}" is not supported`
|
|
1747
|
+
);
|
|
783
1748
|
}
|
|
784
|
-
throwInvalidId(input2);
|
|
785
1749
|
}
|
|
786
|
-
function
|
|
787
|
-
return
|
|
1750
|
+
function buildSorts(sortStrings) {
|
|
1751
|
+
return sortStrings.map((raw) => {
|
|
1752
|
+
const colonIdx = raw.lastIndexOf(":");
|
|
1753
|
+
if (colonIdx === -1) {
|
|
1754
|
+
return { property: raw.trim(), direction: "ascending" };
|
|
1755
|
+
}
|
|
1756
|
+
const property = raw.slice(0, colonIdx).trim();
|
|
1757
|
+
const dir = raw.slice(colonIdx + 1).trim().toLowerCase();
|
|
1758
|
+
return {
|
|
1759
|
+
property,
|
|
1760
|
+
direction: dir === "desc" || dir === "descending" ? "descending" : "ascending"
|
|
1761
|
+
};
|
|
1762
|
+
});
|
|
788
1763
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const url = `https://www.notion.so/${id}`;
|
|
797
|
-
const platform = process.platform;
|
|
798
|
-
const opener = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
799
|
-
await execAsync(`${opener} "${url}"`);
|
|
800
|
-
process.stdout.write(`Opening ${url}
|
|
801
|
-
`);
|
|
802
|
-
}));
|
|
803
|
-
return cmd;
|
|
1764
|
+
function displayFormula(f) {
|
|
1765
|
+
if (f.type === "string") return f.string ?? "";
|
|
1766
|
+
if (f.type === "number")
|
|
1767
|
+
return f.number !== null && f.number !== void 0 ? String(f.number) : "";
|
|
1768
|
+
if (f.type === "boolean") return f.boolean ? "true" : "false";
|
|
1769
|
+
if (f.type === "date") return f.date?.start ?? "";
|
|
1770
|
+
return "";
|
|
804
1771
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1772
|
+
function displayDate(date) {
|
|
1773
|
+
if (!date) return "";
|
|
1774
|
+
return date.end ? `${date.start} \u2192 ${date.end}` : date.start;
|
|
1775
|
+
}
|
|
1776
|
+
function displayPropertyValue(prop) {
|
|
1777
|
+
switch (prop.type) {
|
|
1778
|
+
case "title":
|
|
1779
|
+
return prop.title.map((r) => r.plain_text).join("").replace(/\n/g, " ");
|
|
1780
|
+
case "rich_text":
|
|
1781
|
+
return prop.rich_text.map((r) => r.plain_text).join("").replace(/\n/g, " ");
|
|
1782
|
+
case "number":
|
|
1783
|
+
return prop.number !== null && prop.number !== void 0 ? String(prop.number) : "";
|
|
1784
|
+
case "select":
|
|
1785
|
+
return prop.select?.name ?? "";
|
|
1786
|
+
case "status":
|
|
1787
|
+
return prop.status?.name ?? "";
|
|
1788
|
+
case "multi_select":
|
|
1789
|
+
return prop.multi_select.map((s) => s.name).join(", ");
|
|
1790
|
+
case "date":
|
|
1791
|
+
return displayDate(prop.date);
|
|
1792
|
+
case "checkbox":
|
|
1793
|
+
return prop.checkbox ? "\u2713" : "\u2717";
|
|
1794
|
+
case "url":
|
|
1795
|
+
return prop.url ?? "";
|
|
1796
|
+
case "email":
|
|
1797
|
+
return prop.email ?? "";
|
|
1798
|
+
case "phone_number":
|
|
1799
|
+
return prop.phone_number ?? "";
|
|
1800
|
+
case "people":
|
|
1801
|
+
return prop.people.map((p) => "name" in p && p.name ? p.name : p.id).join(", ");
|
|
1802
|
+
case "relation":
|
|
1803
|
+
return prop.relation.length > 0 ? `[${prop.relation.length}]` : "";
|
|
1804
|
+
case "formula":
|
|
1805
|
+
return displayFormula(prop.formula);
|
|
1806
|
+
case "created_time":
|
|
1807
|
+
return prop.created_time;
|
|
1808
|
+
case "last_edited_time":
|
|
1809
|
+
return prop.last_edited_time;
|
|
1810
|
+
case "unique_id":
|
|
1811
|
+
return prop.unique_id.prefix ? `${prop.unique_id.prefix}-${prop.unique_id.number}` : String(prop.unique_id.number ?? "");
|
|
1812
|
+
default:
|
|
1813
|
+
return "";
|
|
819
1814
|
}
|
|
820
|
-
return allResults;
|
|
821
1815
|
}
|
|
822
1816
|
|
|
823
|
-
// src/commands/
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1817
|
+
// src/commands/db/query.ts
|
|
1818
|
+
var SKIP_TYPES_IN_AUTO = /* @__PURE__ */ new Set(["relation", "rich_text", "people"]);
|
|
1819
|
+
function autoSelectColumns(schema, entries) {
|
|
1820
|
+
const termWidth = process.stdout.columns || 120;
|
|
1821
|
+
const COL_SEP = 2;
|
|
1822
|
+
const candidates = Object.values(schema.properties).filter((p) => !SKIP_TYPES_IN_AUTO.has(p.type)).map((p) => p.name);
|
|
1823
|
+
const widths = candidates.map((col) => {
|
|
1824
|
+
const header = col.toUpperCase().length;
|
|
1825
|
+
const maxData = entries.reduce(
|
|
1826
|
+
(max, e) => Math.max(max, (e.properties[col] ?? "").length),
|
|
1827
|
+
0
|
|
1828
|
+
);
|
|
1829
|
+
return Math.min(Math.max(header, maxData), 40);
|
|
1830
|
+
});
|
|
1831
|
+
const selected = [];
|
|
1832
|
+
let usedWidth = 0;
|
|
1833
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
1834
|
+
const needed = (selected.length > 0 ? COL_SEP : 0) + widths[i];
|
|
1835
|
+
if (usedWidth + needed > termWidth) break;
|
|
1836
|
+
selected.push(candidates[i]);
|
|
1837
|
+
usedWidth += needed;
|
|
827
1838
|
}
|
|
828
|
-
if (
|
|
829
|
-
|
|
830
|
-
return "workspace_name" in bot && bot.workspace_name ? bot.workspace_name : "\u2014";
|
|
1839
|
+
if (selected.length === 0 && candidates.length > 0) {
|
|
1840
|
+
selected.push(candidates[0]);
|
|
831
1841
|
}
|
|
832
|
-
return
|
|
833
|
-
}
|
|
834
|
-
function usersCommand() {
|
|
835
|
-
const cmd = new Command9("users");
|
|
836
|
-
cmd.description("list all users in the workspace").option("--json", "output as JSON").action(withErrorHandling(async (opts) => {
|
|
837
|
-
if (opts.json) setOutputMode("json");
|
|
838
|
-
const { token, source } = await resolveToken();
|
|
839
|
-
reportTokenSource(source);
|
|
840
|
-
const notion = createNotionClient(token);
|
|
841
|
-
const allUsers = await paginateResults(
|
|
842
|
-
(cursor) => notion.users.list({ start_cursor: cursor })
|
|
843
|
-
);
|
|
844
|
-
const users = allUsers.filter((u) => u.name !== void 0);
|
|
845
|
-
const rows = users.map((user) => [
|
|
846
|
-
user.type,
|
|
847
|
-
user.name ?? "(unnamed)",
|
|
848
|
-
getEmailOrWorkspace(user),
|
|
849
|
-
user.id
|
|
850
|
-
]);
|
|
851
|
-
printOutput(users, ["TYPE", "NAME", "EMAIL / WORKSPACE", "ID"], rows);
|
|
852
|
-
}));
|
|
853
|
-
return cmd;
|
|
1842
|
+
return selected;
|
|
854
1843
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1844
|
+
function dbQueryCommand() {
|
|
1845
|
+
return new Command10("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option(
|
|
1846
|
+
"--filter <filter>",
|
|
1847
|
+
'Filter entries (repeatable): --filter "Status=Done"',
|
|
1848
|
+
collect,
|
|
1849
|
+
[]
|
|
1850
|
+
).option(
|
|
1851
|
+
"--sort <sort>",
|
|
1852
|
+
'Sort entries (repeatable): --sort "Name:asc"',
|
|
1853
|
+
collect,
|
|
1854
|
+
[]
|
|
1855
|
+
).option(
|
|
1856
|
+
"--columns <columns>",
|
|
1857
|
+
'Comma-separated list of columns to display: --columns "Title,Status"'
|
|
1858
|
+
).option("--json", "Output raw JSON").action(
|
|
1859
|
+
withErrorHandling(
|
|
1860
|
+
async (id, options) => {
|
|
1861
|
+
const { token } = await resolveToken();
|
|
1862
|
+
const client = createNotionClient(token);
|
|
1863
|
+
const dbId = parseNotionId(id);
|
|
1864
|
+
const schema = await fetchDatabaseSchema(client, dbId);
|
|
1865
|
+
const columns = options.columns ? options.columns.split(",").map((c2) => c2.trim()) : void 0;
|
|
1866
|
+
const filter = options.filter.length ? buildFilter(options.filter, schema) : void 0;
|
|
1867
|
+
const sorts = options.sort.length ? buildSorts(options.sort) : void 0;
|
|
1868
|
+
const entries = await queryDatabase(client, dbId, {
|
|
1869
|
+
filter,
|
|
1870
|
+
sorts,
|
|
1871
|
+
columns
|
|
1872
|
+
});
|
|
1873
|
+
if (options.json) {
|
|
1874
|
+
process.stdout.write(`${formatJSON(entries.map((e) => e.raw))}
|
|
1875
|
+
`);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
if (entries.length === 0) {
|
|
1879
|
+
process.stdout.write("No entries found.\n");
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
const displayColumns = columns ?? autoSelectColumns(schema, entries);
|
|
1883
|
+
const headers = displayColumns.map((c2) => c2.toUpperCase());
|
|
1884
|
+
const rows = entries.map(
|
|
1885
|
+
(entry) => displayColumns.map((col) => entry.properties[col] ?? "")
|
|
1886
|
+
);
|
|
1887
|
+
process.stdout.write(`${formatTable(rows, headers)}
|
|
1888
|
+
`);
|
|
1889
|
+
process.stderr.write(`${entries.length} entries
|
|
1890
|
+
`);
|
|
1891
|
+
}
|
|
1892
|
+
)
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
function collect(value, previous) {
|
|
1896
|
+
return previous.concat([value]);
|
|
885
1897
|
}
|
|
886
1898
|
|
|
887
|
-
// src/commands/
|
|
1899
|
+
// src/commands/db/schema.ts
|
|
888
1900
|
import { Command as Command11 } from "commander";
|
|
1901
|
+
function dbSchemaCommand() {
|
|
1902
|
+
return new Command11("schema").description("Show database schema (property names, types, and options)").argument("<id>", "Notion database ID or URL").option("--json", "Output raw JSON").action(
|
|
1903
|
+
withErrorHandling(async (id, options) => {
|
|
1904
|
+
const { token } = await resolveToken();
|
|
1905
|
+
const client = createNotionClient(token);
|
|
1906
|
+
const dbId = parseNotionId(id);
|
|
1907
|
+
const schema = await fetchDatabaseSchema(client, dbId);
|
|
1908
|
+
if (options.json) {
|
|
1909
|
+
process.stdout.write(`${formatJSON(schema)}
|
|
1910
|
+
`);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
const headers = ["PROPERTY", "TYPE", "OPTIONS"];
|
|
1914
|
+
const rows = Object.values(schema.properties).map((prop) => [
|
|
1915
|
+
prop.name,
|
|
1916
|
+
prop.type,
|
|
1917
|
+
prop.options ? prop.options.map((o) => o.name).join(", ") : ""
|
|
1918
|
+
]);
|
|
1919
|
+
process.stdout.write(`${formatTable(rows, headers)}
|
|
1920
|
+
`);
|
|
1921
|
+
})
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
889
1924
|
|
|
890
|
-
// src/
|
|
891
|
-
import {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
if (
|
|
895
|
-
|
|
896
|
-
block_id: blockId
|
|
897
|
-
});
|
|
898
|
-
const blocks = rawBlocks.filter(isFullBlock);
|
|
899
|
-
const SKIP_RECURSE = /* @__PURE__ */ new Set(["child_page", "child_database"]);
|
|
900
|
-
const nodes = [];
|
|
901
|
-
for (let i = 0; i < blocks.length; i += MAX_CONCURRENT_REQUESTS) {
|
|
902
|
-
const batch = blocks.slice(i, i + MAX_CONCURRENT_REQUESTS);
|
|
903
|
-
const batchNodes = await Promise.all(
|
|
904
|
-
batch.map(async (block) => {
|
|
905
|
-
const children = block.has_children && !SKIP_RECURSE.has(block.type) ? await fetchBlockTree(client, block.id, depth + 1, maxDepth) : [];
|
|
906
|
-
return { block, children };
|
|
907
|
-
})
|
|
908
|
-
);
|
|
909
|
-
nodes.push(...batchNodes);
|
|
1925
|
+
// src/commands/ls.ts
|
|
1926
|
+
import { isFullPageOrDataSource } from "@notionhq/client";
|
|
1927
|
+
import { Command as Command12 } from "commander";
|
|
1928
|
+
function getTitle(item) {
|
|
1929
|
+
if (item.object === "data_source") {
|
|
1930
|
+
return item.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
910
1931
|
}
|
|
911
|
-
|
|
1932
|
+
const titleProp = Object.values(item.properties).find(
|
|
1933
|
+
(p) => p.type === "title"
|
|
1934
|
+
);
|
|
1935
|
+
if (titleProp?.type === "title") {
|
|
1936
|
+
return titleProp.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
1937
|
+
}
|
|
1938
|
+
return "(untitled)";
|
|
912
1939
|
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1940
|
+
function displayType(item) {
|
|
1941
|
+
return item.object === "data_source" ? "database" : item.object;
|
|
1942
|
+
}
|
|
1943
|
+
function lsCommand() {
|
|
1944
|
+
const cmd = new Command12("ls");
|
|
1945
|
+
cmd.description("list accessible Notion pages and databases").option(
|
|
1946
|
+
"--type <type>",
|
|
1947
|
+
"filter by object type (page or database)",
|
|
1948
|
+
(val) => {
|
|
1949
|
+
if (val !== "page" && val !== "database") {
|
|
1950
|
+
throw new Error('--type must be "page" or "database"');
|
|
1951
|
+
}
|
|
1952
|
+
return val;
|
|
1953
|
+
}
|
|
1954
|
+
).option(
|
|
1955
|
+
"--cursor <cursor>",
|
|
1956
|
+
"start from this pagination cursor (from a previous --next hint)"
|
|
1957
|
+
).option("--json", "force JSON output").action(
|
|
1958
|
+
withErrorHandling(
|
|
1959
|
+
async (opts) => {
|
|
1960
|
+
if (opts.json) {
|
|
1961
|
+
setOutputMode("json");
|
|
1962
|
+
}
|
|
1963
|
+
const { token, source } = await resolveToken();
|
|
1964
|
+
reportTokenSource(source);
|
|
1965
|
+
const notion = createNotionClient(token);
|
|
1966
|
+
const response = await notion.search({
|
|
1967
|
+
start_cursor: opts.cursor,
|
|
1968
|
+
page_size: 20
|
|
1969
|
+
});
|
|
1970
|
+
let items = response.results.filter(
|
|
1971
|
+
(r) => isFullPageOrDataSource(r)
|
|
1972
|
+
);
|
|
1973
|
+
if (opts.type) {
|
|
1974
|
+
const filterType = opts.type;
|
|
1975
|
+
items = items.filter(
|
|
1976
|
+
(r) => filterType === "database" ? r.object === "data_source" : r.object === filterType
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
if (items.length === 0) {
|
|
1980
|
+
process.stdout.write("No accessible content found\n");
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
const headers = ["TYPE", "TITLE", "ID", "MODIFIED"];
|
|
1984
|
+
const rows = items.map((item) => [
|
|
1985
|
+
displayType(item),
|
|
1986
|
+
getTitle(item),
|
|
1987
|
+
item.id,
|
|
1988
|
+
item.last_edited_time.split("T")[0]
|
|
1989
|
+
]);
|
|
1990
|
+
printOutput(items, headers, rows);
|
|
1991
|
+
if (response.has_more && response.next_cursor) {
|
|
1992
|
+
process.stderr.write(
|
|
1993
|
+
`
|
|
1994
|
+
--next: notion ls --cursor ${response.next_cursor}
|
|
1995
|
+
`
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
)
|
|
2000
|
+
);
|
|
2001
|
+
return cmd;
|
|
917
2002
|
}
|
|
918
2003
|
|
|
2004
|
+
// src/commands/open.ts
|
|
2005
|
+
import { exec } from "child_process";
|
|
2006
|
+
import { promisify } from "util";
|
|
2007
|
+
import { Command as Command13 } from "commander";
|
|
2008
|
+
var execAsync = promisify(exec);
|
|
2009
|
+
function openCommand() {
|
|
2010
|
+
const cmd = new Command13("open");
|
|
2011
|
+
cmd.description("open a Notion page in the default browser").argument("<id/url>", "Notion page ID or URL").action(
|
|
2012
|
+
withErrorHandling(async (idOrUrl) => {
|
|
2013
|
+
const id = parseNotionId(idOrUrl);
|
|
2014
|
+
const url = `https://www.notion.so/${id}`;
|
|
2015
|
+
const platform = process.platform;
|
|
2016
|
+
const opener = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
2017
|
+
await execAsync(`${opener} "${url}"`);
|
|
2018
|
+
process.stdout.write(`Opening ${url}
|
|
2019
|
+
`);
|
|
2020
|
+
})
|
|
2021
|
+
);
|
|
2022
|
+
return cmd;
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// src/commands/profile/list.ts
|
|
2026
|
+
import { Command as Command14 } from "commander";
|
|
2027
|
+
function profileListCommand() {
|
|
2028
|
+
const cmd = new Command14("list");
|
|
2029
|
+
cmd.description("list all authentication profiles").action(
|
|
2030
|
+
withErrorHandling(async () => {
|
|
2031
|
+
const config = await readGlobalConfig();
|
|
2032
|
+
const profiles = config.profiles ?? {};
|
|
2033
|
+
const profileNames = Object.keys(profiles);
|
|
2034
|
+
if (profileNames.length === 0) {
|
|
2035
|
+
process.stdout.write(
|
|
2036
|
+
"No profiles configured. Run `notion auth login` to get started.\n"
|
|
2037
|
+
);
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
for (const name of profileNames) {
|
|
2041
|
+
const profile = profiles[name];
|
|
2042
|
+
const isActive = config.active_profile === name;
|
|
2043
|
+
const marker = isActive ? bold("* ") : " ";
|
|
2044
|
+
const activeLabel = isActive ? " (active)" : "";
|
|
2045
|
+
const workspaceInfo = profile.workspace_name ? dim(` \u2014 ${profile.workspace_name}`) : "";
|
|
2046
|
+
process.stdout.write(
|
|
2047
|
+
`${marker}${name}${activeLabel}${workspaceInfo}
|
|
2048
|
+
`
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
})
|
|
2052
|
+
);
|
|
2053
|
+
return cmd;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// src/commands/profile/use.ts
|
|
2057
|
+
import { Command as Command15 } from "commander";
|
|
2058
|
+
function profileUseCommand() {
|
|
2059
|
+
const cmd = new Command15("use");
|
|
2060
|
+
cmd.description("switch the active profile").argument("<name>", "profile name to activate").action(
|
|
2061
|
+
withErrorHandling(async (name) => {
|
|
2062
|
+
const config = await readGlobalConfig();
|
|
2063
|
+
const profiles = config.profiles ?? {};
|
|
2064
|
+
if (!profiles[name]) {
|
|
2065
|
+
throw new CliError(
|
|
2066
|
+
ErrorCodes.AUTH_PROFILE_NOT_FOUND,
|
|
2067
|
+
`Profile "${name}" not found.`,
|
|
2068
|
+
`Run "notion auth list" to see available profiles`
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
await writeGlobalConfig({
|
|
2072
|
+
...config,
|
|
2073
|
+
active_profile: name
|
|
2074
|
+
});
|
|
2075
|
+
stderrWrite(success(`Switched to profile "${name}".`));
|
|
2076
|
+
})
|
|
2077
|
+
);
|
|
2078
|
+
return cmd;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/commands/read.ts
|
|
2082
|
+
import { Command as Command16 } from "commander";
|
|
2083
|
+
|
|
919
2084
|
// src/blocks/rich-text.ts
|
|
920
2085
|
function richTextToMd(richText) {
|
|
921
2086
|
return richText.map(segmentToMd).join("");
|
|
@@ -942,7 +2107,8 @@ function applyAnnotations(text, annotations) {
|
|
|
942
2107
|
|
|
943
2108
|
// src/blocks/converters.ts
|
|
944
2109
|
function indentChildren(childrenMd) {
|
|
945
|
-
return childrenMd.split("\n").filter(Boolean).map((line) =>
|
|
2110
|
+
return `${childrenMd.split("\n").filter(Boolean).map((line) => ` ${line}`).join("\n")}
|
|
2111
|
+
`;
|
|
946
2112
|
}
|
|
947
2113
|
var converters = {
|
|
948
2114
|
paragraph(block) {
|
|
@@ -1053,376 +2219,47 @@ ${content}
|
|
|
1053
2219
|
const b = block;
|
|
1054
2220
|
return `### ${b.child_database.title}
|
|
1055
2221
|
`;
|
|
1056
|
-
},
|
|
1057
|
-
link_preview(block) {
|
|
1058
|
-
const b = block;
|
|
1059
|
-
return `[${b.link_preview.url}](${b.link_preview.url})
|
|
1060
|
-
`;
|
|
1061
|
-
}
|
|
1062
|
-
};
|
|
1063
|
-
function blockToMd(block, ctx) {
|
|
1064
|
-
const converter = converters[block.type];
|
|
1065
|
-
if (converter) {
|
|
1066
|
-
return converter(block, ctx);
|
|
1067
|
-
}
|
|
1068
|
-
return `<!-- unsupported block: ${block.type} -->
|
|
1069
|
-
`;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// src/blocks/properties.ts
|
|
1073
|
-
function formatPropertyValue(name, prop) {
|
|
1074
|
-
switch (prop.type) {
|
|
1075
|
-
case "title":
|
|
1076
|
-
return prop.title.map((rt) => rt.plain_text).join("");
|
|
1077
|
-
case "rich_text":
|
|
1078
|
-
return prop.rich_text.map((rt) => rt.plain_text).join("");
|
|
1079
|
-
case "number":
|
|
1080
|
-
return prop.number !== null ? String(prop.number) : "";
|
|
1081
|
-
case "select":
|
|
1082
|
-
return prop.select?.name ?? "";
|
|
1083
|
-
case "status":
|
|
1084
|
-
return prop.status?.name ?? "";
|
|
1085
|
-
case "multi_select":
|
|
1086
|
-
return prop.multi_select.map((s) => s.name).join(", ");
|
|
1087
|
-
case "date":
|
|
1088
|
-
if (!prop.date) return "";
|
|
1089
|
-
return prop.date.end ? `${prop.date.start} \u2192 ${prop.date.end}` : prop.date.start;
|
|
1090
|
-
case "checkbox":
|
|
1091
|
-
return prop.checkbox ? "true" : "false";
|
|
1092
|
-
case "url":
|
|
1093
|
-
return prop.url ?? "";
|
|
1094
|
-
case "email":
|
|
1095
|
-
return prop.email ?? "";
|
|
1096
|
-
case "phone_number":
|
|
1097
|
-
return prop.phone_number ?? "";
|
|
1098
|
-
case "people":
|
|
1099
|
-
return prop.people.map((p) => "name" in p && p.name ? p.name : p.id).join(", ");
|
|
1100
|
-
case "relation":
|
|
1101
|
-
return prop.relation.map((r) => r.id).join(", ");
|
|
1102
|
-
case "formula": {
|
|
1103
|
-
const f = prop.formula;
|
|
1104
|
-
if (f.type === "string") return f.string ?? "";
|
|
1105
|
-
if (f.type === "number") return f.number !== null ? String(f.number) : "";
|
|
1106
|
-
if (f.type === "boolean") return String(f.boolean);
|
|
1107
|
-
if (f.type === "date") return f.date?.start ?? "";
|
|
1108
|
-
return "";
|
|
1109
|
-
}
|
|
1110
|
-
case "rollup": {
|
|
1111
|
-
const r = prop.rollup;
|
|
1112
|
-
if (r.type === "number") return r.number !== null ? String(r.number) : "";
|
|
1113
|
-
if (r.type === "date") return r.date?.start ?? "";
|
|
1114
|
-
if (r.type === "array") return `[${r.array.length} items]`;
|
|
1115
|
-
return "";
|
|
1116
|
-
}
|
|
1117
|
-
case "created_time":
|
|
1118
|
-
return prop.created_time;
|
|
1119
|
-
case "last_edited_time":
|
|
1120
|
-
return prop.last_edited_time;
|
|
1121
|
-
case "created_by":
|
|
1122
|
-
return "name" in prop.created_by ? prop.created_by.name ?? prop.created_by.id : prop.created_by.id;
|
|
1123
|
-
case "last_edited_by":
|
|
1124
|
-
return "name" in prop.last_edited_by ? prop.last_edited_by.name ?? prop.last_edited_by.id : prop.last_edited_by.id;
|
|
1125
|
-
case "files":
|
|
1126
|
-
return prop.files.map((f) => {
|
|
1127
|
-
if (f.type === "external") return f.external.url;
|
|
1128
|
-
return f.name;
|
|
1129
|
-
}).join(", ");
|
|
1130
|
-
case "unique_id":
|
|
1131
|
-
return prop.unique_id.prefix ? `${prop.unique_id.prefix}-${prop.unique_id.number}` : String(prop.unique_id.number ?? "");
|
|
1132
|
-
default:
|
|
1133
|
-
return "";
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// src/blocks/render.ts
|
|
1138
|
-
function buildPropertiesHeader(page) {
|
|
1139
|
-
const lines = ["---"];
|
|
1140
|
-
for (const [name, prop] of Object.entries(page.properties)) {
|
|
1141
|
-
const value = formatPropertyValue(name, prop);
|
|
1142
|
-
if (value) {
|
|
1143
|
-
lines.push(`${name}: ${value}`);
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
lines.push("---", "");
|
|
1147
|
-
return lines.join("\n");
|
|
1148
|
-
}
|
|
1149
|
-
function renderBlockTree(blocks) {
|
|
1150
|
-
const parts = [];
|
|
1151
|
-
let listCounter = 0;
|
|
1152
|
-
for (const node of blocks) {
|
|
1153
|
-
if (node.block.type === "numbered_list_item") {
|
|
1154
|
-
listCounter++;
|
|
1155
|
-
} else {
|
|
1156
|
-
listCounter = 0;
|
|
1157
|
-
}
|
|
1158
|
-
const childrenMd = node.children.length > 0 ? renderBlockTree(node.children) : "";
|
|
1159
|
-
const md = blockToMd(node.block, {
|
|
1160
|
-
listNumber: node.block.type === "numbered_list_item" ? listCounter : void 0,
|
|
1161
|
-
childrenMd: childrenMd || void 0
|
|
1162
|
-
});
|
|
1163
|
-
parts.push(md);
|
|
1164
|
-
}
|
|
1165
|
-
return parts.join("");
|
|
1166
|
-
}
|
|
1167
|
-
function renderPageMarkdown({ page, blocks }) {
|
|
1168
|
-
const header = buildPropertiesHeader(page);
|
|
1169
|
-
const content = renderBlockTree(blocks);
|
|
1170
|
-
return header + content;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
// src/output/markdown.ts
|
|
1174
|
-
import { Chalk as Chalk2 } from "chalk";
|
|
1175
|
-
var c = new Chalk2({ level: 3 });
|
|
1176
|
-
function renderMarkdown(md) {
|
|
1177
|
-
if (!isatty()) return md;
|
|
1178
|
-
const lines = md.split("\n");
|
|
1179
|
-
const out = [];
|
|
1180
|
-
let inFence = false;
|
|
1181
|
-
let fenceLang = "";
|
|
1182
|
-
let fenceLines = [];
|
|
1183
|
-
for (const line of lines) {
|
|
1184
|
-
const fenceMatch = line.match(/^```(\w*)$/);
|
|
1185
|
-
if (fenceMatch && !inFence) {
|
|
1186
|
-
inFence = true;
|
|
1187
|
-
fenceLang = fenceMatch[1] ?? "";
|
|
1188
|
-
fenceLines = [];
|
|
1189
|
-
continue;
|
|
1190
|
-
}
|
|
1191
|
-
if (line === "```" && inFence) {
|
|
1192
|
-
inFence = false;
|
|
1193
|
-
const header = fenceLang ? c.dim(`[${fenceLang}]`) : "";
|
|
1194
|
-
if (header) out.push(header);
|
|
1195
|
-
for (const fl of fenceLines) {
|
|
1196
|
-
out.push(c.green(" " + fl));
|
|
1197
|
-
}
|
|
1198
|
-
out.push("");
|
|
1199
|
-
continue;
|
|
1200
|
-
}
|
|
1201
|
-
if (inFence) {
|
|
1202
|
-
fenceLines.push(line);
|
|
1203
|
-
continue;
|
|
1204
|
-
}
|
|
1205
|
-
if (line === "---") {
|
|
1206
|
-
out.push(c.dim("\u2500".repeat(40)));
|
|
1207
|
-
continue;
|
|
1208
|
-
}
|
|
1209
|
-
if (/^<!--.*-->$/.test(line.trim())) {
|
|
1210
|
-
continue;
|
|
1211
|
-
}
|
|
1212
|
-
const h1 = line.match(/^# (.+)/);
|
|
1213
|
-
if (h1) {
|
|
1214
|
-
out.push("\n" + c.bold.cyan(h1[1]));
|
|
1215
|
-
continue;
|
|
1216
|
-
}
|
|
1217
|
-
const h2 = line.match(/^## (.+)/);
|
|
1218
|
-
if (h2) {
|
|
1219
|
-
out.push("\n" + c.bold.blue(h2[1]));
|
|
1220
|
-
continue;
|
|
1221
|
-
}
|
|
1222
|
-
const h3 = line.match(/^### (.+)/);
|
|
1223
|
-
if (h3) {
|
|
1224
|
-
out.push("\n" + c.bold(h3[1]));
|
|
1225
|
-
continue;
|
|
1226
|
-
}
|
|
1227
|
-
const h4 = line.match(/^#### (.+)/);
|
|
1228
|
-
if (h4) {
|
|
1229
|
-
out.push(c.bold.underline(h4[1]));
|
|
1230
|
-
continue;
|
|
1231
|
-
}
|
|
1232
|
-
if (line.startsWith("> ")) {
|
|
1233
|
-
out.push(c.yellow("\u258E ") + renderInline(line.slice(2)));
|
|
1234
|
-
continue;
|
|
1235
|
-
}
|
|
1236
|
-
if (line === "---") {
|
|
1237
|
-
out.push(c.dim("\u2500".repeat(40)));
|
|
1238
|
-
continue;
|
|
1239
|
-
}
|
|
1240
|
-
const propMatch = line.match(/^([A-Za-z_][A-Za-z0-9_ ]*): (.+)$/);
|
|
1241
|
-
if (propMatch) {
|
|
1242
|
-
out.push(c.dim(propMatch[1] + ": ") + c.white(propMatch[2]));
|
|
1243
|
-
continue;
|
|
1244
|
-
}
|
|
1245
|
-
const bulletMatch = line.match(/^(\s*)- (\[[ x]\] )?(.+)/);
|
|
1246
|
-
if (bulletMatch) {
|
|
1247
|
-
const indent = bulletMatch[1] ?? "";
|
|
1248
|
-
const checkbox = bulletMatch[2];
|
|
1249
|
-
const text = bulletMatch[3] ?? "";
|
|
1250
|
-
if (checkbox) {
|
|
1251
|
-
const checked = checkbox.trim() === "[x]";
|
|
1252
|
-
const box = checked ? c.green("\u2611") : c.dim("\u2610");
|
|
1253
|
-
out.push(indent + box + " " + renderInline(text));
|
|
1254
|
-
} else {
|
|
1255
|
-
out.push(indent + c.cyan("\u2022") + " " + renderInline(text));
|
|
1256
|
-
}
|
|
1257
|
-
continue;
|
|
1258
|
-
}
|
|
1259
|
-
const numMatch = line.match(/^(\s*)(\d+)\. (.+)/);
|
|
1260
|
-
if (numMatch) {
|
|
1261
|
-
const indent = numMatch[1] ?? "";
|
|
1262
|
-
const num = numMatch[2] ?? "";
|
|
1263
|
-
const text = numMatch[3] ?? "";
|
|
1264
|
-
out.push(indent + c.cyan(num + ".") + " " + renderInline(text));
|
|
1265
|
-
continue;
|
|
1266
|
-
}
|
|
1267
|
-
out.push(renderInline(line));
|
|
1268
|
-
}
|
|
1269
|
-
return out.join("\n") + "\n";
|
|
1270
|
-
}
|
|
1271
|
-
function renderInline(text) {
|
|
1272
|
-
const codeSpans = [];
|
|
1273
|
-
let result = text.replace(/`([^`]+)`/g, (_, code) => {
|
|
1274
|
-
codeSpans.push(c.green(code));
|
|
1275
|
-
return `\0CODE${codeSpans.length - 1}\0`;
|
|
1276
|
-
});
|
|
1277
|
-
result = result.replace(/!\[([^\]]*)\]\([^)]+\)/g, (_, alt) => alt ? c.dim(`[image: ${alt}]`) : c.dim("[image]")).replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, t, url) => c.cyan.underline(t) + c.dim(` (${url})`)).replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => c.bold.italic(t)).replace(/\*\*(.+?)\*\*/g, (_, t) => c.bold(t)).replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, (_, t) => c.italic(t)).replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, (_, t) => c.italic(t)).replace(/~~(.+?)~~/g, (_, t) => c.strikethrough(t));
|
|
1278
|
-
result = result.replace(/\x00CODE(\d+)\x00/g, (_, i) => codeSpans[Number(i)] ?? "");
|
|
1279
|
-
return result;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
// src/commands/read.ts
|
|
1283
|
-
function readCommand() {
|
|
1284
|
-
return new Command11("read").description("Read a Notion page as markdown").argument("<id>", "Notion page ID or URL").option("--json", "Output raw JSON instead of markdown").option("--md", "Output raw markdown (no terminal styling)").action(
|
|
1285
|
-
withErrorHandling(async (id, options) => {
|
|
1286
|
-
const { token } = await resolveToken();
|
|
1287
|
-
const client = createNotionClient(token);
|
|
1288
|
-
const pageId = parseNotionId(id);
|
|
1289
|
-
const pageWithBlocks = await fetchPageWithBlocks(client, pageId);
|
|
1290
|
-
if (options.json) {
|
|
1291
|
-
process.stdout.write(JSON.stringify(pageWithBlocks, null, 2) + "\n");
|
|
1292
|
-
} else {
|
|
1293
|
-
const markdown = renderPageMarkdown(pageWithBlocks);
|
|
1294
|
-
if (options.md || !isatty()) {
|
|
1295
|
-
process.stdout.write(markdown);
|
|
1296
|
-
} else {
|
|
1297
|
-
process.stdout.write(renderMarkdown(markdown));
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
})
|
|
1301
|
-
);
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// src/commands/db/schema.ts
|
|
1305
|
-
import { Command as Command12 } from "commander";
|
|
1306
|
-
|
|
1307
|
-
// src/services/database.service.ts
|
|
1308
|
-
import { isFullPage as isFullPage2 } from "@notionhq/client";
|
|
1309
|
-
async function fetchDatabaseSchema(client, dbId) {
|
|
1310
|
-
const ds = await client.dataSources.retrieve({ data_source_id: dbId });
|
|
1311
|
-
const title = "title" in ds ? ds.title.map((rt) => rt.plain_text).join("") || dbId : dbId;
|
|
1312
|
-
const properties = {};
|
|
1313
|
-
if ("properties" in ds) {
|
|
1314
|
-
for (const [name, prop] of Object.entries(ds.properties)) {
|
|
1315
|
-
const config = {
|
|
1316
|
-
id: prop.id,
|
|
1317
|
-
name,
|
|
1318
|
-
type: prop.type
|
|
1319
|
-
};
|
|
1320
|
-
if (prop.type === "select" && "select" in prop) {
|
|
1321
|
-
config.options = prop.select.options;
|
|
1322
|
-
} else if (prop.type === "status" && "status" in prop) {
|
|
1323
|
-
config.options = prop.status.options;
|
|
1324
|
-
} else if (prop.type === "multi_select" && "multi_select" in prop) {
|
|
1325
|
-
config.options = prop.multi_select.options;
|
|
1326
|
-
}
|
|
1327
|
-
properties[name] = config;
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
return { id: dbId, title, properties };
|
|
1331
|
-
}
|
|
1332
|
-
async function queryDatabase(client, dbId, opts = {}) {
|
|
1333
|
-
const rawPages = await paginateResults(
|
|
1334
|
-
(cursor) => client.dataSources.query({
|
|
1335
|
-
data_source_id: dbId,
|
|
1336
|
-
filter: opts.filter,
|
|
1337
|
-
sorts: opts.sorts,
|
|
1338
|
-
start_cursor: cursor,
|
|
1339
|
-
page_size: 100
|
|
1340
|
-
})
|
|
1341
|
-
);
|
|
1342
|
-
return rawPages.filter(isFullPage2).map((page) => {
|
|
1343
|
-
const propValues = {};
|
|
1344
|
-
for (const [name, prop] of Object.entries(page.properties)) {
|
|
1345
|
-
if (opts.columns && !opts.columns.includes(name)) continue;
|
|
1346
|
-
propValues[name] = displayPropertyValue(prop);
|
|
1347
|
-
}
|
|
1348
|
-
return { id: page.id, properties: propValues, raw: page };
|
|
1349
|
-
});
|
|
1350
|
-
}
|
|
1351
|
-
function buildFilter(filterStrings, schema) {
|
|
1352
|
-
if (!filterStrings.length) return void 0;
|
|
1353
|
-
const filters = filterStrings.map((raw) => {
|
|
1354
|
-
const eqIdx = raw.indexOf("=");
|
|
1355
|
-
if (eqIdx === -1) {
|
|
1356
|
-
throw new CliError(
|
|
1357
|
-
ErrorCodes.INVALID_ARG,
|
|
1358
|
-
`Invalid filter syntax: "${raw}"`,
|
|
1359
|
-
'Use format: --filter "PropertyName=Value"'
|
|
1360
|
-
);
|
|
1361
|
-
}
|
|
1362
|
-
const propName = raw.slice(0, eqIdx).trim();
|
|
1363
|
-
const value = raw.slice(eqIdx + 1).trim();
|
|
1364
|
-
const propConfig = schema.properties[propName];
|
|
1365
|
-
if (!propConfig) {
|
|
1366
|
-
const available = Object.keys(schema.properties).join(", ");
|
|
1367
|
-
throw new CliError(
|
|
1368
|
-
ErrorCodes.INVALID_ARG,
|
|
1369
|
-
`Property "${propName}" not found`,
|
|
1370
|
-
`Available properties: ${available}`
|
|
1371
|
-
);
|
|
1372
|
-
}
|
|
1373
|
-
return buildPropertyFilter(propName, propConfig.type, value);
|
|
1374
|
-
});
|
|
1375
|
-
return filters.length === 1 ? filters[0] : { and: filters };
|
|
1376
|
-
}
|
|
1377
|
-
function buildPropertyFilter(property, type, value) {
|
|
1378
|
-
switch (type) {
|
|
1379
|
-
case "select":
|
|
1380
|
-
return { property, select: { equals: value } };
|
|
1381
|
-
case "status":
|
|
1382
|
-
return { property, status: { equals: value } };
|
|
1383
|
-
case "multi_select":
|
|
1384
|
-
return { property, multi_select: { contains: value } };
|
|
1385
|
-
case "checkbox":
|
|
1386
|
-
return { property, checkbox: { equals: value.toLowerCase() === "true" } };
|
|
1387
|
-
case "number":
|
|
1388
|
-
return { property, number: { equals: Number(value) } };
|
|
1389
|
-
case "title":
|
|
1390
|
-
return { property, title: { contains: value } };
|
|
1391
|
-
case "rich_text":
|
|
1392
|
-
return { property, rich_text: { contains: value } };
|
|
1393
|
-
case "url":
|
|
1394
|
-
return { property, url: { contains: value } };
|
|
1395
|
-
case "email":
|
|
1396
|
-
return { property, email: { contains: value } };
|
|
1397
|
-
default:
|
|
1398
|
-
throw new CliError(
|
|
1399
|
-
ErrorCodes.INVALID_ARG,
|
|
1400
|
-
`Filtering by property type "${type}" is not supported`
|
|
1401
|
-
);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
function buildSorts(sortStrings) {
|
|
1405
|
-
return sortStrings.map((raw) => {
|
|
1406
|
-
const colonIdx = raw.lastIndexOf(":");
|
|
1407
|
-
if (colonIdx === -1) {
|
|
1408
|
-
return { property: raw.trim(), direction: "ascending" };
|
|
1409
|
-
}
|
|
1410
|
-
const property = raw.slice(0, colonIdx).trim();
|
|
1411
|
-
const dir = raw.slice(colonIdx + 1).trim().toLowerCase();
|
|
1412
|
-
return {
|
|
1413
|
-
property,
|
|
1414
|
-
direction: dir === "desc" || dir === "descending" ? "descending" : "ascending"
|
|
1415
|
-
};
|
|
1416
|
-
});
|
|
2222
|
+
},
|
|
2223
|
+
link_preview(block) {
|
|
2224
|
+
const b = block;
|
|
2225
|
+
return `[${b.link_preview.url}](${b.link_preview.url})
|
|
2226
|
+
`;
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
function blockToMd(block, ctx) {
|
|
2230
|
+
const converter = converters[block.type];
|
|
2231
|
+
if (converter) {
|
|
2232
|
+
return converter(block, ctx);
|
|
2233
|
+
}
|
|
2234
|
+
return `<!-- unsupported block: ${block.type} -->
|
|
2235
|
+
`;
|
|
1417
2236
|
}
|
|
1418
|
-
|
|
2237
|
+
|
|
2238
|
+
// src/blocks/properties.ts
|
|
2239
|
+
function formatFormula(f) {
|
|
2240
|
+
if (f.type === "string") return f.string ?? "";
|
|
2241
|
+
if (f.type === "number") return f.number !== null ? String(f.number) : "";
|
|
2242
|
+
if (f.type === "boolean") return String(f.boolean);
|
|
2243
|
+
if (f.type === "date") return f.date?.start ?? "";
|
|
2244
|
+
return "";
|
|
2245
|
+
}
|
|
2246
|
+
function formatRollup(r) {
|
|
2247
|
+
if (r.type === "number") return r.number !== null ? String(r.number) : "";
|
|
2248
|
+
if (r.type === "date") return r.date?.start ?? "";
|
|
2249
|
+
if (r.type === "array") return `[${r.array.length} items]`;
|
|
2250
|
+
return "";
|
|
2251
|
+
}
|
|
2252
|
+
function formatUser(p) {
|
|
2253
|
+
return "name" in p && p.name ? p.name : p.id;
|
|
2254
|
+
}
|
|
2255
|
+
function formatPropertyValue(_name, prop) {
|
|
1419
2256
|
switch (prop.type) {
|
|
1420
2257
|
case "title":
|
|
1421
|
-
return prop.title.map((
|
|
2258
|
+
return prop.title.map((rt) => rt.plain_text).join("");
|
|
1422
2259
|
case "rich_text":
|
|
1423
|
-
return prop.rich_text.map((
|
|
2260
|
+
return prop.rich_text.map((rt) => rt.plain_text).join("");
|
|
1424
2261
|
case "number":
|
|
1425
|
-
return prop.number !== null
|
|
2262
|
+
return prop.number !== null ? String(prop.number) : "";
|
|
1426
2263
|
case "select":
|
|
1427
2264
|
return prop.select?.name ?? "";
|
|
1428
2265
|
case "status":
|
|
@@ -1430,9 +2267,10 @@ function displayPropertyValue(prop) {
|
|
|
1430
2267
|
case "multi_select":
|
|
1431
2268
|
return prop.multi_select.map((s) => s.name).join(", ");
|
|
1432
2269
|
case "date":
|
|
1433
|
-
|
|
2270
|
+
if (!prop.date) return "";
|
|
2271
|
+
return prop.date.end ? `${prop.date.start} \u2192 ${prop.date.end}` : prop.date.start;
|
|
1434
2272
|
case "checkbox":
|
|
1435
|
-
return prop.checkbox ? "
|
|
2273
|
+
return prop.checkbox ? "true" : "false";
|
|
1436
2274
|
case "url":
|
|
1437
2275
|
return prop.url ?? "";
|
|
1438
2276
|
case "email":
|
|
@@ -1440,22 +2278,23 @@ function displayPropertyValue(prop) {
|
|
|
1440
2278
|
case "phone_number":
|
|
1441
2279
|
return prop.phone_number ?? "";
|
|
1442
2280
|
case "people":
|
|
1443
|
-
return prop.people.map(
|
|
2281
|
+
return prop.people.map(formatUser).join(", ");
|
|
1444
2282
|
case "relation":
|
|
1445
|
-
return prop.relation.
|
|
1446
|
-
case "formula":
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
return f.number !== null && f.number !== void 0 ? String(f.number) : "";
|
|
1451
|
-
if (f.type === "boolean") return f.boolean ? "true" : "false";
|
|
1452
|
-
if (f.type === "date") return f.date?.start ?? "";
|
|
1453
|
-
return "";
|
|
1454
|
-
}
|
|
2283
|
+
return prop.relation.map((r) => r.id).join(", ");
|
|
2284
|
+
case "formula":
|
|
2285
|
+
return formatFormula(prop.formula);
|
|
2286
|
+
case "rollup":
|
|
2287
|
+
return formatRollup(prop.rollup);
|
|
1455
2288
|
case "created_time":
|
|
1456
2289
|
return prop.created_time;
|
|
1457
2290
|
case "last_edited_time":
|
|
1458
2291
|
return prop.last_edited_time;
|
|
2292
|
+
case "created_by":
|
|
2293
|
+
return "name" in prop.created_by ? prop.created_by.name ?? prop.created_by.id : prop.created_by.id;
|
|
2294
|
+
case "last_edited_by":
|
|
2295
|
+
return "name" in prop.last_edited_by ? prop.last_edited_by.name ?? prop.last_edited_by.id : prop.last_edited_by.id;
|
|
2296
|
+
case "files":
|
|
2297
|
+
return prop.files.map((f) => f.type === "external" ? f.external.url : f.name).join(", ");
|
|
1459
2298
|
case "unique_id":
|
|
1460
2299
|
return prop.unique_id.prefix ? `${prop.unique_id.prefix}-${prop.unique_id.number}` : String(prop.unique_id.number ?? "");
|
|
1461
2300
|
default:
|
|
@@ -1463,95 +2302,354 @@ function displayPropertyValue(prop) {
|
|
|
1463
2302
|
}
|
|
1464
2303
|
}
|
|
1465
2304
|
|
|
1466
|
-
// src/
|
|
1467
|
-
function
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
2305
|
+
// src/blocks/render.ts
|
|
2306
|
+
function buildPropertiesHeader(page) {
|
|
2307
|
+
const lines = ["---"];
|
|
2308
|
+
for (const [name, prop] of Object.entries(page.properties)) {
|
|
2309
|
+
const value = formatPropertyValue(name, prop);
|
|
2310
|
+
if (value) {
|
|
2311
|
+
lines.push(`${name}: ${value}`);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
lines.push("---", "");
|
|
2315
|
+
return lines.join("\n");
|
|
2316
|
+
}
|
|
2317
|
+
function renderBlockTree(blocks) {
|
|
2318
|
+
const parts = [];
|
|
2319
|
+
let listCounter = 0;
|
|
2320
|
+
for (const node of blocks) {
|
|
2321
|
+
if (node.block.type === "numbered_list_item") {
|
|
2322
|
+
listCounter++;
|
|
2323
|
+
} else {
|
|
2324
|
+
listCounter = 0;
|
|
2325
|
+
}
|
|
2326
|
+
const childrenMd = node.children.length > 0 ? renderBlockTree(node.children) : "";
|
|
2327
|
+
const md = blockToMd(node.block, {
|
|
2328
|
+
listNumber: node.block.type === "numbered_list_item" ? listCounter : void 0,
|
|
2329
|
+
childrenMd: childrenMd || void 0
|
|
2330
|
+
});
|
|
2331
|
+
parts.push(md);
|
|
2332
|
+
}
|
|
2333
|
+
return parts.join("");
|
|
2334
|
+
}
|
|
2335
|
+
function renderPageMarkdown({ page, blocks }) {
|
|
2336
|
+
const header = buildPropertiesHeader(page);
|
|
2337
|
+
const content = renderBlockTree(blocks);
|
|
2338
|
+
return header + content;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
// src/output/markdown.ts
|
|
2342
|
+
import { Chalk as Chalk2 } from "chalk";
|
|
2343
|
+
var c = new Chalk2({ level: 3 });
|
|
2344
|
+
function handleFenceLine(line, state, out) {
|
|
2345
|
+
const fenceMatch = line.match(/^```(\w*)$/);
|
|
2346
|
+
if (fenceMatch && !state.inFence) {
|
|
2347
|
+
state.inFence = true;
|
|
2348
|
+
state.fenceLang = fenceMatch[1] ?? "";
|
|
2349
|
+
state.fenceLines = [];
|
|
2350
|
+
return true;
|
|
2351
|
+
}
|
|
2352
|
+
if (line === "```" && state.inFence) {
|
|
2353
|
+
state.inFence = false;
|
|
2354
|
+
const header = state.fenceLang ? c.dim(`[${state.fenceLang}]`) : "";
|
|
2355
|
+
if (header) out.push(header);
|
|
2356
|
+
for (const fl of state.fenceLines) {
|
|
2357
|
+
out.push(c.green(` ${fl}`));
|
|
2358
|
+
}
|
|
2359
|
+
out.push("");
|
|
2360
|
+
return true;
|
|
2361
|
+
}
|
|
2362
|
+
return false;
|
|
2363
|
+
}
|
|
2364
|
+
function handleHeading(line, out) {
|
|
2365
|
+
const h1 = line.match(/^# (.+)/);
|
|
2366
|
+
if (h1) {
|
|
2367
|
+
out.push(`
|
|
2368
|
+
${c.bold.cyan(h1[1])}`);
|
|
2369
|
+
return true;
|
|
2370
|
+
}
|
|
2371
|
+
const h2 = line.match(/^## (.+)/);
|
|
2372
|
+
if (h2) {
|
|
2373
|
+
out.push(`
|
|
2374
|
+
${c.bold.blue(h2[1])}`);
|
|
2375
|
+
return true;
|
|
2376
|
+
}
|
|
2377
|
+
const h3 = line.match(/^### (.+)/);
|
|
2378
|
+
if (h3) {
|
|
2379
|
+
out.push(`
|
|
2380
|
+
${c.bold(h3[1])}`);
|
|
2381
|
+
return true;
|
|
2382
|
+
}
|
|
2383
|
+
const h4 = line.match(/^#### (.+)/);
|
|
2384
|
+
if (h4) {
|
|
2385
|
+
out.push(c.bold.underline(h4[1]));
|
|
2386
|
+
return true;
|
|
2387
|
+
}
|
|
2388
|
+
return false;
|
|
2389
|
+
}
|
|
2390
|
+
function handleListLine(line, out) {
|
|
2391
|
+
const bulletMatch = line.match(/^(\s*)- (\[[ x]\] )?(.+)/);
|
|
2392
|
+
if (bulletMatch) {
|
|
2393
|
+
const indent = bulletMatch[1] ?? "";
|
|
2394
|
+
const checkbox = bulletMatch[2];
|
|
2395
|
+
const text = bulletMatch[3] ?? "";
|
|
2396
|
+
if (checkbox) {
|
|
2397
|
+
const checked = checkbox.trim() === "[x]";
|
|
2398
|
+
const box = checked ? c.green("\u2611") : c.dim("\u2610");
|
|
2399
|
+
out.push(`${indent + box} ${renderInline(text)}`);
|
|
2400
|
+
} else {
|
|
2401
|
+
out.push(`${indent + c.cyan("\u2022")} ${renderInline(text)}`);
|
|
2402
|
+
}
|
|
2403
|
+
return true;
|
|
2404
|
+
}
|
|
2405
|
+
const numMatch = line.match(/^(\s*)(\d+)\. (.+)/);
|
|
2406
|
+
if (numMatch) {
|
|
2407
|
+
const indent = numMatch[1] ?? "";
|
|
2408
|
+
const num = numMatch[2] ?? "";
|
|
2409
|
+
const text = numMatch[3] ?? "";
|
|
2410
|
+
out.push(`${indent + c.cyan(`${num}.`)} ${renderInline(text)}`);
|
|
2411
|
+
return true;
|
|
2412
|
+
}
|
|
2413
|
+
return false;
|
|
2414
|
+
}
|
|
2415
|
+
function renderMarkdown(md) {
|
|
2416
|
+
if (!isatty()) return md;
|
|
2417
|
+
const lines = md.split("\n");
|
|
2418
|
+
const out = [];
|
|
2419
|
+
const fence = { inFence: false, fenceLang: "", fenceLines: [] };
|
|
2420
|
+
for (const line of lines) {
|
|
2421
|
+
if (handleFenceLine(line, fence, out)) continue;
|
|
2422
|
+
if (fence.inFence) {
|
|
2423
|
+
fence.fenceLines.push(line);
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2426
|
+
if (line === "---") {
|
|
2427
|
+
out.push(c.dim("\u2500".repeat(40)));
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
if (/^<!--.*-->$/.test(line.trim())) continue;
|
|
2431
|
+
if (handleHeading(line, out)) continue;
|
|
2432
|
+
if (line.startsWith("> ")) {
|
|
2433
|
+
out.push(c.yellow("\u258E ") + renderInline(line.slice(2)));
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
const propMatch = line.match(/^([A-Za-z_][A-Za-z0-9_ ]*): (.+)$/);
|
|
2437
|
+
if (propMatch) {
|
|
2438
|
+
out.push(c.dim(`${propMatch[1]}: `) + c.white(propMatch[2]));
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
if (handleListLine(line, out)) continue;
|
|
2442
|
+
out.push(renderInline(line));
|
|
2443
|
+
}
|
|
2444
|
+
return `${out.join("\n")}
|
|
2445
|
+
`;
|
|
2446
|
+
}
|
|
2447
|
+
function renderInline(text) {
|
|
2448
|
+
const codeSpans = [];
|
|
2449
|
+
let result = text.replace(/`([^`]+)`/g, (_, code) => {
|
|
2450
|
+
codeSpans.push(c.green(code));
|
|
2451
|
+
return `\0CODE${codeSpans.length - 1}\0`;
|
|
2452
|
+
});
|
|
2453
|
+
result = result.replace(
|
|
2454
|
+
/!\[([^\]]*)\]\([^)]+\)/g,
|
|
2455
|
+
(_, alt) => alt ? c.dim(`[image: ${alt}]`) : c.dim("[image]")
|
|
2456
|
+
).replace(
|
|
2457
|
+
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
2458
|
+
(_, t, url) => c.cyan.underline(t) + c.dim(` (${url})`)
|
|
2459
|
+
).replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => c.bold.italic(t)).replace(/\*\*(.+?)\*\*/g, (_, t) => c.bold(t)).replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, (_, t) => c.italic(t)).replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, (_, t) => c.italic(t)).replace(/~~(.+?)~~/g, (_, t) => c.strikethrough(t));
|
|
2460
|
+
result = result.replace(
|
|
2461
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: sentinel chars are intentional
|
|
2462
|
+
/\x00CODE(\d+)\x00/g,
|
|
2463
|
+
(_, i) => codeSpans[Number(i)] ?? ""
|
|
1486
2464
|
);
|
|
2465
|
+
return result;
|
|
1487
2466
|
}
|
|
1488
2467
|
|
|
1489
|
-
// src/
|
|
1490
|
-
import {
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
return Math.min(Math.max(header, maxData), 40);
|
|
2468
|
+
// src/services/page.service.ts
|
|
2469
|
+
import {
|
|
2470
|
+
collectPaginatedAPI,
|
|
2471
|
+
isFullBlock
|
|
2472
|
+
} from "@notionhq/client";
|
|
2473
|
+
var MAX_CONCURRENT_REQUESTS = 3;
|
|
2474
|
+
async function fetchBlockTree(client, blockId, depth, maxDepth) {
|
|
2475
|
+
if (depth >= maxDepth) return [];
|
|
2476
|
+
const rawBlocks = await collectPaginatedAPI(client.blocks.children.list, {
|
|
2477
|
+
block_id: blockId
|
|
1500
2478
|
});
|
|
1501
|
-
const
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
2479
|
+
const blocks = rawBlocks.filter(isFullBlock);
|
|
2480
|
+
const SKIP_RECURSE = /* @__PURE__ */ new Set(["child_page", "child_database"]);
|
|
2481
|
+
const nodes = [];
|
|
2482
|
+
for (let i = 0; i < blocks.length; i += MAX_CONCURRENT_REQUESTS) {
|
|
2483
|
+
const batch = blocks.slice(i, i + MAX_CONCURRENT_REQUESTS);
|
|
2484
|
+
const batchNodes = await Promise.all(
|
|
2485
|
+
batch.map(async (block) => {
|
|
2486
|
+
const children = block.has_children && !SKIP_RECURSE.has(block.type) ? await fetchBlockTree(client, block.id, depth + 1, maxDepth) : [];
|
|
2487
|
+
return { block, children };
|
|
2488
|
+
})
|
|
2489
|
+
);
|
|
2490
|
+
nodes.push(...batchNodes);
|
|
1511
2491
|
}
|
|
1512
|
-
return
|
|
2492
|
+
return nodes;
|
|
1513
2493
|
}
|
|
1514
|
-
function
|
|
1515
|
-
|
|
2494
|
+
async function fetchPageWithBlocks(client, pageId) {
|
|
2495
|
+
const page = await client.pages.retrieve({
|
|
2496
|
+
page_id: pageId
|
|
2497
|
+
});
|
|
2498
|
+
const blocks = await fetchBlockTree(client, pageId, 0, 10);
|
|
2499
|
+
return { page, blocks };
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// src/commands/read.ts
|
|
2503
|
+
function readCommand() {
|
|
2504
|
+
return new Command16("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(
|
|
1516
2505
|
withErrorHandling(
|
|
1517
2506
|
async (id, options) => {
|
|
1518
2507
|
const { token } = await resolveToken();
|
|
1519
2508
|
const client = createNotionClient(token);
|
|
1520
|
-
const
|
|
1521
|
-
const
|
|
1522
|
-
const columns = options.columns ? options.columns.split(",").map((c2) => c2.trim()) : void 0;
|
|
1523
|
-
const filter = options.filter.length ? buildFilter(options.filter, schema) : void 0;
|
|
1524
|
-
const sorts = options.sort.length ? buildSorts(options.sort) : void 0;
|
|
1525
|
-
const entries = await queryDatabase(client, dbId, { filter, sorts, columns });
|
|
2509
|
+
const pageId = parseNotionId(id);
|
|
2510
|
+
const pageWithBlocks = await fetchPageWithBlocks(client, pageId);
|
|
1526
2511
|
if (options.json) {
|
|
1527
|
-
process.stdout.write(
|
|
1528
|
-
|
|
2512
|
+
process.stdout.write(
|
|
2513
|
+
`${JSON.stringify(pageWithBlocks, null, 2)}
|
|
2514
|
+
`
|
|
2515
|
+
);
|
|
2516
|
+
} else {
|
|
2517
|
+
const markdown = renderPageMarkdown(pageWithBlocks);
|
|
2518
|
+
if (options.md || !isatty()) {
|
|
2519
|
+
process.stdout.write(markdown);
|
|
2520
|
+
} else {
|
|
2521
|
+
process.stdout.write(renderMarkdown(markdown));
|
|
2522
|
+
}
|
|
1529
2523
|
}
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
2524
|
+
}
|
|
2525
|
+
)
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// src/commands/search.ts
|
|
2530
|
+
import { isFullPageOrDataSource as isFullPageOrDataSource2 } from "@notionhq/client";
|
|
2531
|
+
import { Command as Command17 } from "commander";
|
|
2532
|
+
function getTitle2(item) {
|
|
2533
|
+
if (item.object === "data_source") {
|
|
2534
|
+
return item.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
2535
|
+
}
|
|
2536
|
+
const titleProp = Object.values(item.properties).find(
|
|
2537
|
+
(p) => p.type === "title"
|
|
2538
|
+
);
|
|
2539
|
+
if (titleProp?.type === "title") {
|
|
2540
|
+
return titleProp.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
2541
|
+
}
|
|
2542
|
+
return "(untitled)";
|
|
2543
|
+
}
|
|
2544
|
+
function toSdkFilterValue(type) {
|
|
2545
|
+
return type === "database" ? "data_source" : "page";
|
|
2546
|
+
}
|
|
2547
|
+
function displayType2(item) {
|
|
2548
|
+
return item.object === "data_source" ? "database" : item.object;
|
|
2549
|
+
}
|
|
2550
|
+
function searchCommand() {
|
|
2551
|
+
const cmd = new Command17("search");
|
|
2552
|
+
cmd.description("search Notion workspace by keyword").argument("<query>", "search keyword").option(
|
|
2553
|
+
"--type <type>",
|
|
2554
|
+
"filter by object type (page or database)",
|
|
2555
|
+
(val) => {
|
|
2556
|
+
if (val !== "page" && val !== "database") {
|
|
2557
|
+
throw new Error('--type must be "page" or "database"');
|
|
2558
|
+
}
|
|
2559
|
+
return val;
|
|
2560
|
+
}
|
|
2561
|
+
).option(
|
|
2562
|
+
"--cursor <cursor>",
|
|
2563
|
+
"start from this pagination cursor (from a previous --next hint)"
|
|
2564
|
+
).option("--json", "force JSON output").action(
|
|
2565
|
+
withErrorHandling(
|
|
2566
|
+
async (query, opts) => {
|
|
2567
|
+
if (opts.json) {
|
|
2568
|
+
setOutputMode("json");
|
|
1533
2569
|
}
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
const
|
|
1537
|
-
|
|
2570
|
+
const { token, source } = await resolveToken();
|
|
2571
|
+
reportTokenSource(source);
|
|
2572
|
+
const notion = createNotionClient(token);
|
|
2573
|
+
const response = await notion.search({
|
|
2574
|
+
query,
|
|
2575
|
+
filter: opts.type ? { property: "object", value: toSdkFilterValue(opts.type) } : void 0,
|
|
2576
|
+
start_cursor: opts.cursor,
|
|
2577
|
+
page_size: 20
|
|
2578
|
+
});
|
|
2579
|
+
const fullResults = response.results.filter(
|
|
2580
|
+
(r) => isFullPageOrDataSource2(r)
|
|
1538
2581
|
);
|
|
1539
|
-
|
|
1540
|
-
|
|
2582
|
+
if (fullResults.length === 0) {
|
|
2583
|
+
process.stdout.write(`No results found for "${query}"
|
|
1541
2584
|
`);
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
const headers = ["TYPE", "TITLE", "ID", "MODIFIED"];
|
|
2588
|
+
const rows = fullResults.map((item) => [
|
|
2589
|
+
displayType2(item),
|
|
2590
|
+
getTitle2(item),
|
|
2591
|
+
item.id,
|
|
2592
|
+
item.last_edited_time.split("T")[0]
|
|
2593
|
+
]);
|
|
2594
|
+
printOutput(fullResults, headers, rows);
|
|
2595
|
+
if (response.has_more && response.next_cursor) {
|
|
2596
|
+
process.stderr.write(
|
|
2597
|
+
`
|
|
2598
|
+
--next: notion search "${query}" --cursor ${response.next_cursor}
|
|
2599
|
+
`
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
1542
2602
|
}
|
|
1543
2603
|
)
|
|
1544
2604
|
);
|
|
2605
|
+
return cmd;
|
|
1545
2606
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
2607
|
+
|
|
2608
|
+
// src/commands/users.ts
|
|
2609
|
+
import { Command as Command18 } from "commander";
|
|
2610
|
+
function getEmailOrWorkspace(user) {
|
|
2611
|
+
if (user.type === "person") {
|
|
2612
|
+
return user.person.email ?? "\u2014";
|
|
2613
|
+
}
|
|
2614
|
+
if (user.type === "bot") {
|
|
2615
|
+
const bot = user.bot;
|
|
2616
|
+
return "workspace_name" in bot && bot.workspace_name ? bot.workspace_name : "\u2014";
|
|
2617
|
+
}
|
|
2618
|
+
return "\u2014";
|
|
2619
|
+
}
|
|
2620
|
+
function usersCommand() {
|
|
2621
|
+
const cmd = new Command18("users");
|
|
2622
|
+
cmd.description("list all users in the workspace").option("--json", "output as JSON").action(
|
|
2623
|
+
withErrorHandling(async (opts) => {
|
|
2624
|
+
if (opts.json) setOutputMode("json");
|
|
2625
|
+
const { token, source } = await resolveToken();
|
|
2626
|
+
reportTokenSource(source);
|
|
2627
|
+
const notion = createNotionClient(token);
|
|
2628
|
+
const allUsers = await paginateResults(
|
|
2629
|
+
(cursor) => notion.users.list({ start_cursor: cursor })
|
|
2630
|
+
);
|
|
2631
|
+
const users = allUsers.filter(
|
|
2632
|
+
(u) => u.name !== void 0
|
|
2633
|
+
);
|
|
2634
|
+
const rows = users.map((user) => [
|
|
2635
|
+
user.type,
|
|
2636
|
+
user.name ?? "(unnamed)",
|
|
2637
|
+
getEmailOrWorkspace(user),
|
|
2638
|
+
user.id
|
|
2639
|
+
]);
|
|
2640
|
+
printOutput(users, ["TYPE", "NAME", "EMAIL / WORKSPACE", "ID"], rows);
|
|
2641
|
+
})
|
|
2642
|
+
);
|
|
2643
|
+
return cmd;
|
|
1548
2644
|
}
|
|
1549
2645
|
|
|
1550
2646
|
// src/cli.ts
|
|
1551
2647
|
var __filename = fileURLToPath(import.meta.url);
|
|
1552
2648
|
var __dirname = dirname(__filename);
|
|
1553
|
-
var pkg = JSON.parse(
|
|
1554
|
-
|
|
2649
|
+
var pkg = JSON.parse(
|
|
2650
|
+
readFileSync(join3(__dirname, "../package.json"), "utf-8")
|
|
2651
|
+
);
|
|
2652
|
+
var program = new Command19();
|
|
1555
2653
|
program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
|
|
1556
2654
|
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
2655
|
program.configureOutput({
|
|
@@ -1572,19 +2670,31 @@ program.hook("preAction", (thisCommand) => {
|
|
|
1572
2670
|
setOutputMode("md");
|
|
1573
2671
|
}
|
|
1574
2672
|
});
|
|
1575
|
-
|
|
1576
|
-
|
|
2673
|
+
var authCmd = new Command19("auth").description("manage Notion authentication");
|
|
2674
|
+
authCmd.action(authDefaultAction(authCmd));
|
|
2675
|
+
authCmd.addCommand(loginCommand());
|
|
2676
|
+
authCmd.addCommand(logoutCommand());
|
|
2677
|
+
authCmd.addCommand(statusCommand());
|
|
2678
|
+
authCmd.addCommand(profileListCommand());
|
|
2679
|
+
authCmd.addCommand(profileUseCommand());
|
|
2680
|
+
program.addCommand(authCmd);
|
|
2681
|
+
program.addCommand(initCommand(), { hidden: true });
|
|
2682
|
+
var profileCmd = new Command19("profile").description(
|
|
2683
|
+
"manage authentication profiles"
|
|
2684
|
+
);
|
|
1577
2685
|
profileCmd.addCommand(profileListCommand());
|
|
1578
2686
|
profileCmd.addCommand(profileUseCommand());
|
|
1579
|
-
|
|
1580
|
-
program.addCommand(profileCmd);
|
|
2687
|
+
program.addCommand(profileCmd, { hidden: true });
|
|
1581
2688
|
program.addCommand(searchCommand());
|
|
1582
2689
|
program.addCommand(lsCommand());
|
|
1583
2690
|
program.addCommand(openCommand());
|
|
1584
2691
|
program.addCommand(usersCommand());
|
|
1585
2692
|
program.addCommand(commentsCommand());
|
|
1586
2693
|
program.addCommand(readCommand());
|
|
1587
|
-
|
|
2694
|
+
program.addCommand(commentAddCommand());
|
|
2695
|
+
program.addCommand(appendCommand());
|
|
2696
|
+
program.addCommand(createPageCommand());
|
|
2697
|
+
var dbCmd = new Command19("db").description("Database operations");
|
|
1588
2698
|
dbCmd.addCommand(dbSchemaCommand());
|
|
1589
2699
|
dbCmd.addCommand(dbQueryCommand());
|
|
1590
2700
|
program.addCommand(dbCmd);
|