@andrzejchm/notion-cli 0.6.0 → 0.7.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 +10 -0
- package/dist/cli.js +262 -31
- package/dist/cli.js.map +1 -1
- package/docs/FEATURE-PARITY.md +189 -0
- package/docs/README.agents.md +42 -0
- package/docs/skills/using-notion-cli/SKILL.md +28 -18
- package/docs/testing-setup.md +250 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -95,7 +95,10 @@ notion ls
|
|
|
95
95
|
| `notion comments <id\|url>` | Read page comments |
|
|
96
96
|
| `notion comment <id\|url> -m <text>` | Add a comment to a page |
|
|
97
97
|
| `notion append <id\|url> -m <markdown>` | Append markdown blocks to a page |
|
|
98
|
+
| `notion edit-page <id\|url> --find <old> --replace <new>` | Search-and-replace text on a page |
|
|
99
|
+
| `notion edit-page <id\|url> -m <markdown>` | Replace entire page content |
|
|
98
100
|
| `notion create-page --parent <id\|url> --title <title>` | Create a new page, prints URL |
|
|
101
|
+
| `notion update <id\|url> --prop "Name=Value"` | Update properties on a page |
|
|
99
102
|
| `notion completion bash\|zsh\|fish` | Install shell tab completion |
|
|
100
103
|
|
|
101
104
|
### `notion db query` flags
|
|
@@ -188,6 +191,7 @@ Write commands require additional capabilities — enable in your integration se
|
|
|
188
191
|
|---------|----------------------|
|
|
189
192
|
| `notion append` | Read content, Insert content |
|
|
190
193
|
| `notion create-page` | Read content, Insert content |
|
|
194
|
+
| `notion update` | Read content, Update content |
|
|
191
195
|
| `notion comment` | Read content, Insert content, Read comments, Insert comments |
|
|
192
196
|
|
|
193
197
|
---
|
|
@@ -208,6 +212,12 @@ Write commands require additional capabilities — enable in your integration se
|
|
|
208
212
|
|
|
209
213
|
---
|
|
210
214
|
|
|
215
|
+
## Roadmap & Feature Parity
|
|
216
|
+
|
|
217
|
+
See [docs/FEATURE-PARITY.md](docs/FEATURE-PARITY.md) for a detailed comparison of this CLI's capabilities against the official Notion MCP server, with prioritized gaps and planned additions.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
211
221
|
## License
|
|
212
222
|
|
|
213
223
|
MIT © [Andrzej Chmielewski](https://github.com/andrzejchm)
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
5
|
import { dirname, join as join3 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
-
import { Command as
|
|
7
|
+
import { Command as Command22 } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/commands/append.ts
|
|
10
10
|
import { Command } from "commander";
|
|
@@ -598,6 +598,30 @@ function buildContentRange(content) {
|
|
|
598
598
|
}
|
|
599
599
|
return content;
|
|
600
600
|
}
|
|
601
|
+
async function searchAndReplace(client, pageId, updates, options) {
|
|
602
|
+
await client.pages.updateMarkdown({
|
|
603
|
+
page_id: pageId,
|
|
604
|
+
type: "update_content",
|
|
605
|
+
update_content: {
|
|
606
|
+
content_updates: updates.map((u) => ({
|
|
607
|
+
old_str: u.oldStr,
|
|
608
|
+
new_str: u.newStr,
|
|
609
|
+
...options?.replaceAll && { replace_all_matches: true }
|
|
610
|
+
})),
|
|
611
|
+
...options?.allowDeletingContent && { allow_deleting_content: true }
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async function replacePageContent(client, pageId, newContent, options) {
|
|
616
|
+
await client.pages.updateMarkdown({
|
|
617
|
+
page_id: pageId,
|
|
618
|
+
type: "replace_content",
|
|
619
|
+
replace_content: {
|
|
620
|
+
new_str: newContent,
|
|
621
|
+
...options?.allowDeletingContent && { allow_deleting_content: true }
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
601
625
|
async function replaceMarkdown(client, pageId, newMarkdown, options) {
|
|
602
626
|
const current = await client.pages.retrieveMarkdown({ page_id: pageId });
|
|
603
627
|
const currentContent = current.markdown.trim();
|
|
@@ -1737,6 +1761,10 @@ function dbSchemaCommand() {
|
|
|
1737
1761
|
|
|
1738
1762
|
// src/commands/edit-page.ts
|
|
1739
1763
|
import { Command as Command11 } from "commander";
|
|
1764
|
+
function collect2(val, acc) {
|
|
1765
|
+
acc.push(val);
|
|
1766
|
+
return acc;
|
|
1767
|
+
}
|
|
1740
1768
|
function editPageCommand() {
|
|
1741
1769
|
const cmd = new Command11("edit-page");
|
|
1742
1770
|
cmd.description(
|
|
@@ -1745,20 +1773,46 @@ function editPageCommand() {
|
|
|
1745
1773
|
"-m, --message <markdown>",
|
|
1746
1774
|
"new markdown content for the page body"
|
|
1747
1775
|
).option(
|
|
1776
|
+
"--find <text>",
|
|
1777
|
+
"text to find (repeatable, pair with --replace)",
|
|
1778
|
+
collect2,
|
|
1779
|
+
[]
|
|
1780
|
+
).option(
|
|
1781
|
+
"--replace <text>",
|
|
1782
|
+
"replacement text (repeatable, pair with --find)",
|
|
1783
|
+
collect2,
|
|
1784
|
+
[]
|
|
1785
|
+
).option("--all", "replace all matches of each --find pattern").option(
|
|
1748
1786
|
"--range <selector>",
|
|
1749
|
-
'ellipsis selector to replace only a section, e.g. "## My Section...last line"'
|
|
1787
|
+
'[deprecated] ellipsis selector to replace only a section, e.g. "## My Section...last line"'
|
|
1750
1788
|
).option(
|
|
1751
1789
|
"--allow-deleting-content",
|
|
1752
|
-
"allow deletion
|
|
1790
|
+
"allow deletion of child pages/databases"
|
|
1753
1791
|
).action(
|
|
1754
1792
|
withErrorHandling(async (idOrUrl, opts) => {
|
|
1755
1793
|
const { token, source } = await resolveToken();
|
|
1756
1794
|
reportTokenSource(source);
|
|
1757
1795
|
const client = createNotionClient(token);
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
)
|
|
1796
|
+
const pageId = parseNotionId(idOrUrl);
|
|
1797
|
+
const uuid = toUuid(pageId);
|
|
1798
|
+
if (opts.find.length > 0) {
|
|
1799
|
+
if (opts.find.length !== opts.replace.length) {
|
|
1800
|
+
throw new CliError(
|
|
1801
|
+
ErrorCodes.INVALID_ARG,
|
|
1802
|
+
`Mismatched --find/--replace: got ${opts.find.length} --find and ${opts.replace.length} --replace flags.`,
|
|
1803
|
+
"Provide the same number of --find and --replace flags, paired by position."
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
const updates = opts.find.map((oldStr, i) => ({
|
|
1807
|
+
oldStr,
|
|
1808
|
+
newStr: opts.replace[i]
|
|
1809
|
+
}));
|
|
1810
|
+
await searchAndReplace(client, uuid, updates, {
|
|
1811
|
+
replaceAll: opts.all ?? false,
|
|
1812
|
+
allowDeletingContent: opts.allowDeletingContent ?? false
|
|
1813
|
+
});
|
|
1814
|
+
process.stdout.write("Page content updated.\n");
|
|
1815
|
+
return;
|
|
1762
1816
|
}
|
|
1763
1817
|
let markdown = "";
|
|
1764
1818
|
if (opts.message) {
|
|
@@ -1769,38 +1823,39 @@ function editPageCommand() {
|
|
|
1769
1823
|
throw new CliError(
|
|
1770
1824
|
ErrorCodes.INVALID_ARG,
|
|
1771
1825
|
"No content provided (stdin was empty).",
|
|
1772
|
-
"Pass
|
|
1826
|
+
"Pass content via -m/--message for full replacement, --find/--replace for targeted edits, or pipe content through stdin"
|
|
1773
1827
|
);
|
|
1774
1828
|
}
|
|
1775
1829
|
} else {
|
|
1776
1830
|
throw new CliError(
|
|
1777
1831
|
ErrorCodes.INVALID_ARG,
|
|
1778
1832
|
"No content provided.",
|
|
1779
|
-
"Pass
|
|
1833
|
+
"Pass content via -m/--message for full replacement, --find/--replace for targeted edits, or pipe content through stdin"
|
|
1780
1834
|
);
|
|
1781
1835
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
try {
|
|
1785
|
-
if (opts.range) {
|
|
1836
|
+
if (opts.range) {
|
|
1837
|
+
try {
|
|
1786
1838
|
await replaceMarkdown(client, uuid, markdown, {
|
|
1787
1839
|
range: opts.range,
|
|
1788
1840
|
allowDeletingContent: opts.allowDeletingContent ?? false
|
|
1789
1841
|
});
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
);
|
|
1842
|
+
} catch (error2) {
|
|
1843
|
+
if (isNotionValidationError(error2)) {
|
|
1844
|
+
throw new CliError(
|
|
1845
|
+
ErrorCodes.INVALID_ARG,
|
|
1846
|
+
`Selector not found: "${opts.range}". ${error2.message}`,
|
|
1847
|
+
SELECTOR_HINT,
|
|
1848
|
+
error2
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
throw error2;
|
|
1801
1852
|
}
|
|
1802
|
-
|
|
1853
|
+
process.stdout.write("Page content replaced.\n");
|
|
1854
|
+
return;
|
|
1803
1855
|
}
|
|
1856
|
+
await replacePageContent(client, uuid, markdown, {
|
|
1857
|
+
allowDeletingContent: opts.allowDeletingContent ?? false
|
|
1858
|
+
});
|
|
1804
1859
|
process.stdout.write("Page content replaced.\n");
|
|
1805
1860
|
})
|
|
1806
1861
|
);
|
|
@@ -2333,8 +2388,183 @@ function searchCommand() {
|
|
|
2333
2388
|
return cmd;
|
|
2334
2389
|
}
|
|
2335
2390
|
|
|
2336
|
-
// src/commands/
|
|
2391
|
+
// src/commands/update.ts
|
|
2337
2392
|
import { Command as Command20 } from "commander";
|
|
2393
|
+
|
|
2394
|
+
// src/services/update.service.ts
|
|
2395
|
+
var UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
2396
|
+
"relation",
|
|
2397
|
+
"formula",
|
|
2398
|
+
"rollup",
|
|
2399
|
+
"created_time",
|
|
2400
|
+
"created_by",
|
|
2401
|
+
"last_edited_time",
|
|
2402
|
+
"last_edited_by",
|
|
2403
|
+
"files",
|
|
2404
|
+
"unique_id",
|
|
2405
|
+
"verification",
|
|
2406
|
+
"button"
|
|
2407
|
+
]);
|
|
2408
|
+
function buildPropertyUpdate(propName, propType, value) {
|
|
2409
|
+
if (UNSUPPORTED_TYPES.has(propType)) {
|
|
2410
|
+
throw new CliError(
|
|
2411
|
+
ErrorCodes.INVALID_ARG,
|
|
2412
|
+
`Property "${propName}" has type "${propType}" which cannot be set via the CLI.`,
|
|
2413
|
+
"Supported types: title, rich_text, select, status, multi_select, number, checkbox, url, email, phone_number, date"
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
if (value === "") {
|
|
2417
|
+
return null;
|
|
2418
|
+
}
|
|
2419
|
+
switch (propType) {
|
|
2420
|
+
case "title":
|
|
2421
|
+
return { title: [{ type: "text", text: { content: value } }] };
|
|
2422
|
+
case "rich_text":
|
|
2423
|
+
return { rich_text: [{ type: "text", text: { content: value } }] };
|
|
2424
|
+
case "select":
|
|
2425
|
+
return { select: { name: value } };
|
|
2426
|
+
case "status":
|
|
2427
|
+
return { status: { name: value } };
|
|
2428
|
+
case "multi_select":
|
|
2429
|
+
return {
|
|
2430
|
+
multi_select: value.split(",").map((v) => v.trim()).filter(Boolean).map((v) => ({ name: v }))
|
|
2431
|
+
};
|
|
2432
|
+
case "number": {
|
|
2433
|
+
const n = Number(value);
|
|
2434
|
+
if (Number.isNaN(n)) {
|
|
2435
|
+
throw new CliError(
|
|
2436
|
+
ErrorCodes.INVALID_ARG,
|
|
2437
|
+
`Invalid number value "${value}" for property "${propName}".`,
|
|
2438
|
+
'Provide a numeric value, e.g. --prop "Count=42"'
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
return { number: n };
|
|
2442
|
+
}
|
|
2443
|
+
case "checkbox": {
|
|
2444
|
+
const lower = value.toLowerCase();
|
|
2445
|
+
return { checkbox: lower === "true" || lower === "yes" };
|
|
2446
|
+
}
|
|
2447
|
+
case "url":
|
|
2448
|
+
return { url: value };
|
|
2449
|
+
case "email":
|
|
2450
|
+
return { email: value };
|
|
2451
|
+
case "phone_number":
|
|
2452
|
+
return { phone_number: value };
|
|
2453
|
+
case "date": {
|
|
2454
|
+
const parts = value.split(",");
|
|
2455
|
+
const start = parts[0].trim();
|
|
2456
|
+
const end = parts[1]?.trim();
|
|
2457
|
+
return { date: end ? { start, end } : { start } };
|
|
2458
|
+
}
|
|
2459
|
+
default:
|
|
2460
|
+
throw new CliError(
|
|
2461
|
+
ErrorCodes.INVALID_ARG,
|
|
2462
|
+
`Property "${propName}" has unsupported type "${propType}".`,
|
|
2463
|
+
"Supported types: title, rich_text, select, status, multi_select, number, checkbox, url, email, phone_number, date"
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
function buildPropertiesPayload(propStrings, page) {
|
|
2468
|
+
const result = {};
|
|
2469
|
+
for (const propString of propStrings) {
|
|
2470
|
+
const eqIdx = propString.indexOf("=");
|
|
2471
|
+
if (eqIdx === -1) {
|
|
2472
|
+
throw new CliError(
|
|
2473
|
+
ErrorCodes.INVALID_ARG,
|
|
2474
|
+
`Invalid --prop value: "${propString}". Expected format: "PropertyName=Value".`,
|
|
2475
|
+
'Example: --prop "Status=Done"'
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
const propName = propString.slice(0, eqIdx).trim();
|
|
2479
|
+
const value = propString.slice(eqIdx + 1);
|
|
2480
|
+
const schemaProp = page.properties[propName];
|
|
2481
|
+
if (!schemaProp) {
|
|
2482
|
+
const available = Object.keys(page.properties).join(", ");
|
|
2483
|
+
throw new CliError(
|
|
2484
|
+
ErrorCodes.INVALID_ARG,
|
|
2485
|
+
`Property "${propName}" not found on this page.`,
|
|
2486
|
+
`Available properties: ${available}`
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
const propType = schemaProp.type;
|
|
2490
|
+
const payload = buildPropertyUpdate(propName, propType, value);
|
|
2491
|
+
result[propName] = payload;
|
|
2492
|
+
}
|
|
2493
|
+
return result;
|
|
2494
|
+
}
|
|
2495
|
+
async function updatePageProperties(client, pageId, properties) {
|
|
2496
|
+
const response = await client.pages.update({
|
|
2497
|
+
page_id: pageId,
|
|
2498
|
+
properties
|
|
2499
|
+
});
|
|
2500
|
+
return response;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// src/commands/update.ts
|
|
2504
|
+
function collectProps(val, acc) {
|
|
2505
|
+
acc.push(val);
|
|
2506
|
+
return acc;
|
|
2507
|
+
}
|
|
2508
|
+
function updateCommand() {
|
|
2509
|
+
const cmd = new Command20("update");
|
|
2510
|
+
cmd.description("update properties on a Notion page").argument("<id/url>", "Notion page ID or URL").option(
|
|
2511
|
+
"--prop <property=value>",
|
|
2512
|
+
"set a property value (repeatable)",
|
|
2513
|
+
collectProps,
|
|
2514
|
+
[]
|
|
2515
|
+
).option("--title <title>", "set the page title").action(
|
|
2516
|
+
withErrorHandling(async (idOrUrl, opts) => {
|
|
2517
|
+
if (opts.title === void 0 && opts.prop.length === 0) {
|
|
2518
|
+
throw new CliError(
|
|
2519
|
+
ErrorCodes.INVALID_ARG,
|
|
2520
|
+
"No properties to update.",
|
|
2521
|
+
'Provide at least one --prop "Name=Value" or --title "New Title"'
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
const { token, source } = await resolveToken();
|
|
2525
|
+
reportTokenSource(source);
|
|
2526
|
+
const client = createNotionClient(token);
|
|
2527
|
+
const id = parseNotionId(idOrUrl);
|
|
2528
|
+
const uuid = toUuid(id);
|
|
2529
|
+
const page = await client.pages.retrieve({
|
|
2530
|
+
page_id: uuid
|
|
2531
|
+
});
|
|
2532
|
+
const properties = buildPropertiesPayload(opts.prop, page);
|
|
2533
|
+
if (opts.title !== void 0) {
|
|
2534
|
+
const titleEntry = Object.entries(page.properties).find(
|
|
2535
|
+
([, prop]) => prop.type === "title"
|
|
2536
|
+
);
|
|
2537
|
+
if (!titleEntry) {
|
|
2538
|
+
throw new CliError(
|
|
2539
|
+
ErrorCodes.INVALID_ARG,
|
|
2540
|
+
"This page has no title property.",
|
|
2541
|
+
"Use --prop to set properties by name instead"
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
const [titlePropName] = titleEntry;
|
|
2545
|
+
properties[titlePropName] = {
|
|
2546
|
+
title: [{ type: "text", text: { content: opts.title } }]
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
const updatedPage = await updatePageProperties(
|
|
2550
|
+
client,
|
|
2551
|
+
uuid,
|
|
2552
|
+
properties
|
|
2553
|
+
);
|
|
2554
|
+
const mode = getOutputMode();
|
|
2555
|
+
if (mode === "json") {
|
|
2556
|
+
process.stdout.write(`${formatJSON(updatedPage)}
|
|
2557
|
+
`);
|
|
2558
|
+
} else {
|
|
2559
|
+
process.stdout.write("Page updated.\n");
|
|
2560
|
+
}
|
|
2561
|
+
})
|
|
2562
|
+
);
|
|
2563
|
+
return cmd;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// src/commands/users.ts
|
|
2567
|
+
import { Command as Command21 } from "commander";
|
|
2338
2568
|
function getEmailOrWorkspace(user) {
|
|
2339
2569
|
if (user.type === "person") {
|
|
2340
2570
|
return user.person.email ?? "\u2014";
|
|
@@ -2346,7 +2576,7 @@ function getEmailOrWorkspace(user) {
|
|
|
2346
2576
|
return "\u2014";
|
|
2347
2577
|
}
|
|
2348
2578
|
function usersCommand() {
|
|
2349
|
-
const cmd = new
|
|
2579
|
+
const cmd = new Command21("users");
|
|
2350
2580
|
cmd.description("list all users in the workspace").option("--json", "output as JSON").action(
|
|
2351
2581
|
withErrorHandling(async (opts) => {
|
|
2352
2582
|
if (opts.json) setOutputMode("json");
|
|
@@ -2377,7 +2607,7 @@ var __dirname = dirname(__filename);
|
|
|
2377
2607
|
var pkg = JSON.parse(
|
|
2378
2608
|
readFileSync(join3(__dirname, "../package.json"), "utf-8")
|
|
2379
2609
|
);
|
|
2380
|
-
var program = new
|
|
2610
|
+
var program = new Command22();
|
|
2381
2611
|
program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
|
|
2382
2612
|
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");
|
|
2383
2613
|
program.configureOutput({
|
|
@@ -2398,7 +2628,7 @@ program.hook("preAction", (thisCommand) => {
|
|
|
2398
2628
|
setOutputMode("md");
|
|
2399
2629
|
}
|
|
2400
2630
|
});
|
|
2401
|
-
var authCmd = new
|
|
2631
|
+
var authCmd = new Command22("auth").description("manage Notion authentication");
|
|
2402
2632
|
authCmd.action(authDefaultAction(authCmd));
|
|
2403
2633
|
authCmd.addCommand(loginCommand());
|
|
2404
2634
|
authCmd.addCommand(logoutCommand());
|
|
@@ -2408,7 +2638,7 @@ authCmd.addCommand(profileUseCommand());
|
|
|
2408
2638
|
authCmd.addCommand(profileRemoveCommand());
|
|
2409
2639
|
program.addCommand(authCmd);
|
|
2410
2640
|
program.addCommand(initCommand(), { hidden: true });
|
|
2411
|
-
var profileCmd = new
|
|
2641
|
+
var profileCmd = new Command22("profile").description(
|
|
2412
2642
|
"manage authentication profiles"
|
|
2413
2643
|
);
|
|
2414
2644
|
profileCmd.addCommand(profileListCommand());
|
|
@@ -2425,7 +2655,8 @@ program.addCommand(commentAddCommand());
|
|
|
2425
2655
|
program.addCommand(appendCommand());
|
|
2426
2656
|
program.addCommand(createPageCommand());
|
|
2427
2657
|
program.addCommand(editPageCommand());
|
|
2428
|
-
|
|
2658
|
+
program.addCommand(updateCommand());
|
|
2659
|
+
var dbCmd = new Command22("db").description("Database operations");
|
|
2429
2660
|
dbCmd.addCommand(dbSchemaCommand());
|
|
2430
2661
|
dbCmd.addCommand(dbQueryCommand());
|
|
2431
2662
|
program.addCommand(dbCmd);
|