@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 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 Command21 } from "commander";
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 when using --range (always true for full-page replace)"
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
- if (opts.allowDeletingContent && !opts.range) {
1759
- process.stderr.write(
1760
- "Warning: --allow-deleting-content has no effect without --range (full-page replace always allows deletion).\n"
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 markdown via -m/--message or pipe non-empty content through stdin"
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 markdown via -m/--message or pipe it through stdin"
1833
+ "Pass content via -m/--message for full replacement, --find/--replace for targeted edits, or pipe content through stdin"
1780
1834
  );
1781
1835
  }
1782
- const pageId = parseNotionId(idOrUrl);
1783
- const uuid = toUuid(pageId);
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
- } else {
1791
- await replaceMarkdown(client, uuid, markdown);
1792
- }
1793
- } catch (error2) {
1794
- if (opts.range && isNotionValidationError(error2)) {
1795
- throw new CliError(
1796
- ErrorCodes.INVALID_ARG,
1797
- `Selector not found: "${opts.range}". ${error2.message}`,
1798
- SELECTOR_HINT,
1799
- error2
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
- throw error2;
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/users.ts
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 Command20("users");
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 Command21();
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 Command21("auth").description("manage Notion authentication");
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 Command21("profile").description(
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
- var dbCmd = new Command21("db").description("Database operations");
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);