@eightstate/escli 0.5.0 → 0.7.1
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/dist/commands/notion/block/trash.js +71 -0
- package/dist/commands/notion/comments/add.js +48 -0
- package/dist/commands/notion/comments/get.js +42 -0
- package/dist/commands/notion/comments/list.js +55 -0
- package/dist/commands/notion/comments/reply.js +45 -0
- package/dist/commands/notion/comments/thread.js +87 -0
- package/dist/commands/notion/db/create.js +555 -0
- package/dist/commands/notion/db/query.js +451 -0
- package/dist/commands/notion/db/row/create.js +74 -0
- package/dist/commands/notion/db/row/update.js +165 -0
- package/dist/commands/notion/ds/create.js +73 -0
- package/dist/commands/notion/enroll.js +302 -0
- package/dist/commands/notion/index.js +24 -0
- package/dist/commands/notion/page/edit.js +73 -0
- package/dist/commands/notion/page/move.js +59 -0
- package/dist/commands/notion/page/read.js +60 -0
- package/dist/commands/notion/page/replace-content.js +80 -0
- package/dist/commands/notion/page/replace-text.js +80 -0
- package/dist/commands/notion/page/replace.js +63 -0
- package/dist/commands/notion/page/trash.js +79 -0
- package/dist/commands/notion/search.js +207 -0
- package/dist/commands/notion/upload/attach.js +105 -0
- package/dist/commands/notion/upload/index.js +129 -0
- package/dist/commands/notion/upload/list.js +78 -0
- package/dist/commands/notion/upload/status.js +76 -0
- package/dist/commands/notion/view/create.js +78 -0
- package/dist/commands/notion/whoami.js +96 -0
- package/dist/commands/research.js +11 -0
- package/dist/entry.js +16 -9
- package/dist/io/render-kv.js +12 -0
- package/dist/io/render-labeled.js +16 -0
- package/dist/io/render-table.js +19 -0
- package/dist/io/render-trailer.js +7 -0
- package/dist/lib/manifest.js +3 -2
- package/dist/lib/notion/comments/shared.js +366 -0
- package/dist/lib/notion/db-row/common.js +367 -0
- package/dist/lib/notion/manifest-pass.js +4 -0
- package/dist/lib/notion/page/content-common.js +473 -0
- package/dist/lib/notion/trash-move/support.js +300 -0
- package/dist/lib/notion/upload/shared.js +372 -0
- package/dist/lib/registry.js +118 -25
- package/dist/services/notion.js +274 -0
- package/dist/services/research.js +31 -10
- package/oclif.manifest.json +4084 -1
- package/package.json +22 -17
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { notionPagesMove } from '../../../services/notion.js';
|
|
3
|
+
import { botName, exactOneDestination, formatParent, notionErrors, NotionWriteCommand, pageRef, parentRef, parseNotionId, recordValue, renderReceipt, requireYes, } from '../../../lib/notion/trash-move/support.js';
|
|
4
|
+
export default class NotionPageMove extends NotionWriteCommand {
|
|
5
|
+
static errors = notionErrors();
|
|
6
|
+
static summary = 'Move a Notion page to a new parent page or data source';
|
|
7
|
+
static description = 'Move a page under another page or into a database data source. Use --to-data-source with a data_source_id, not a database_id.';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> notion page move 195de922-1179-449f-ab80-75a27c979105 --to-page 0f6b6d9e-36d4-49ff-a4dc-037b65fd08c2 --yes',
|
|
10
|
+
'<%= config.bin %> notion page move 195de922-1179-449f-ab80-75a27c979105 --to-data-source 1c7b35e6-e67f-8096-bf3f-000ba938459e --yes',
|
|
11
|
+
];
|
|
12
|
+
static aliases = [];
|
|
13
|
+
static flags = {
|
|
14
|
+
'to-page': Flags.string({ description: 'Destination parent page ID or URL.' }),
|
|
15
|
+
'to-data-source': Flags.string({ description: 'Destination database data source ID.' }),
|
|
16
|
+
yes: Flags.boolean({ description: 'Confirm the workflow-changing write without prompting.', default: false }),
|
|
17
|
+
raw: Flags.boolean({ description: 'Emit the verbatim Notion response body.', default: false }),
|
|
18
|
+
};
|
|
19
|
+
static args = {
|
|
20
|
+
page: Args.string({ description: 'Page ID or Notion URL.', required: true }),
|
|
21
|
+
};
|
|
22
|
+
static enableJsonFlag = true;
|
|
23
|
+
static strict = true;
|
|
24
|
+
async execute() {
|
|
25
|
+
const { args, flags } = await this.parse(NotionPageMove);
|
|
26
|
+
this.outputFlags = flags;
|
|
27
|
+
requireYes(flags.yes);
|
|
28
|
+
const pageId = parseNotionId(args.page);
|
|
29
|
+
const to = exactOneDestination(flags['to-page'], flags['to-data-source']);
|
|
30
|
+
const result = await notionPagesMove(pageId, to);
|
|
31
|
+
const raw = this.rememberResult(result);
|
|
32
|
+
const rawRecord = recordValue(raw);
|
|
33
|
+
const returnedParent = parentRef(rawRecord?.parent);
|
|
34
|
+
const bot = botName(raw);
|
|
35
|
+
return {
|
|
36
|
+
action: 'page.move',
|
|
37
|
+
page: pageRef(raw, pageId),
|
|
38
|
+
from: parentRef(rawRecord?.previous_parent ?? rawRecord?.from) ?? { type: 'unknown' },
|
|
39
|
+
to: { ...to, ...returnedParent },
|
|
40
|
+
destructive: false,
|
|
41
|
+
workflow_risk: 'Page location changed; database properties may now govern this page.',
|
|
42
|
+
...(bot ? { bot } : {}),
|
|
43
|
+
notion_version: '2026-03-11',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
render(data) {
|
|
47
|
+
return renderReceipt([
|
|
48
|
+
['action', data.action],
|
|
49
|
+
['page', `${data.page.title} (${data.page.id})`],
|
|
50
|
+
['from', formatParent(data.from)],
|
|
51
|
+
['to', formatParent(data.to)],
|
|
52
|
+
['destructive', false],
|
|
53
|
+
['workflow_risk', 'page location changed; database properties may now govern this page'],
|
|
54
|
+
['bot', data.bot ?? ''],
|
|
55
|
+
['notion_version', data.notion_version],
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=move.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { ErrorCode } from '@eightstate/contracts/errors';
|
|
3
|
+
import { writeStdout } from '../../../io/io.js';
|
|
4
|
+
import { notionFetchPage } from '../../../services/notion.js';
|
|
5
|
+
import { PAGE_CONTENT_ERROR_CODES, PageContentReadDataSchema, PageRefArg, emitRaw, normalizeMarkdownBody, pageIdFromRef, previewMarkdown, renderRead, withoutRaw, RawBypassCommand, } from '../../../lib/notion/page/content-common.js';
|
|
6
|
+
export { PageContentReadDataSchema };
|
|
7
|
+
export default class NotionPageRead extends RawBypassCommand {
|
|
8
|
+
static errors = [...PAGE_CONTENT_ERROR_CODES, ErrorCode.UsageUnknownFlag];
|
|
9
|
+
static summary = 'Read page body markdown';
|
|
10
|
+
static description = 'Read a Notion page body as native enhanced markdown with a compact metadata trailer.';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> notion page read 362e1f4a-7d8c-81b0-9f6e-f2c9a7b630dc',
|
|
13
|
+
'<%= config.bin %> notion page read https://notion.so/Q2-Planning-362e1f4a7d8c81b09f6ef2c9a7b630dc --raw',
|
|
14
|
+
'<%= config.bin %> --json notion page read 362e1f4a-7d8c-81b0-9f6e-f2c9a7b630dc',
|
|
15
|
+
];
|
|
16
|
+
static aliases = [];
|
|
17
|
+
static flags = {
|
|
18
|
+
raw: Flags.boolean({ description: 'Emit the verbatim Notion response body.', default: false }),
|
|
19
|
+
};
|
|
20
|
+
static args = {
|
|
21
|
+
page: Args.string(PageRefArg),
|
|
22
|
+
};
|
|
23
|
+
static enableJsonFlag = true;
|
|
24
|
+
static strict = true;
|
|
25
|
+
async execute() {
|
|
26
|
+
const { args, flags } = await this.parse(NotionPageRead);
|
|
27
|
+
const pageId = pageIdFromRef(args.page);
|
|
28
|
+
const result = await notionFetchPage(pageId);
|
|
29
|
+
if (flags.raw) {
|
|
30
|
+
await emitRaw(result.raw);
|
|
31
|
+
this.humanOutputHandled = true;
|
|
32
|
+
return {
|
|
33
|
+
surface: 'page-content',
|
|
34
|
+
command: 'read',
|
|
35
|
+
page: { id: pageId },
|
|
36
|
+
content: { format: 'notion-enhanced-markdown', chars_total: 0, preview: '', notion_truncated: false, unknown_block_ids: [] },
|
|
37
|
+
raw: result.raw,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const body = normalizeMarkdownBody(result, pageId);
|
|
41
|
+
const data = {
|
|
42
|
+
surface: 'page-content',
|
|
43
|
+
command: 'read',
|
|
44
|
+
page: { id: body.id, title: body.title, url: body.url },
|
|
45
|
+
content: {
|
|
46
|
+
format: 'notion-enhanced-markdown',
|
|
47
|
+
chars_total: body.markdown.length,
|
|
48
|
+
preview: previewMarkdown(body.markdown),
|
|
49
|
+
notion_truncated: body.truncated,
|
|
50
|
+
unknown_block_ids: body.unknown_block_ids,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
if (flags.json)
|
|
54
|
+
return withoutRaw(data);
|
|
55
|
+
await writeStdout(`${renderRead(data)}\n`);
|
|
56
|
+
this.humanOutputHandled = true;
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=read.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { notionPagePatchMarkdown } from '../../../services/notion.js';
|
|
3
|
+
import { boolValue, deltaEstimate, estimateText, formatEstimate, markdownPreview, notionErrors, NotionWriteCommand, pageRef, parseNotionId, readMarkdownInput, recordValue, renderReceipt, requireYes, stringValue, unknownBlockIds, } from '../../../lib/notion/trash-move/support.js';
|
|
4
|
+
export default class NotionPageReplaceContent extends NotionWriteCommand {
|
|
5
|
+
static errors = notionErrors();
|
|
6
|
+
static summary = 'Replace a Notion page body with markdown';
|
|
7
|
+
static description = 'Replace page markdown with Notion replace_content. If child pages/databases would be deleted, re-run with --allow-deleting-content after review.';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> notion page replace-content 195de922-1179-449f-ab80-75a27c979105 --file replacement.md --yes',
|
|
10
|
+
'<%= config.bin %> notion page replace-content 195de922-1179-449f-ab80-75a27c979105 --text "# Replacement" --allow-deleting-content --yes',
|
|
11
|
+
];
|
|
12
|
+
static aliases = [];
|
|
13
|
+
static flags = {
|
|
14
|
+
file: Flags.string({ description: 'Markdown file to use as the new page body.' }),
|
|
15
|
+
text: Flags.string({ description: 'Markdown text to use as the new page body.' }),
|
|
16
|
+
'allow-deleting-content': Flags.boolean({ description: 'Allow deletion of child pages/databases if Notion requires it.', default: false }),
|
|
17
|
+
yes: Flags.boolean({ description: 'Confirm the destructive write without prompting.', default: false }),
|
|
18
|
+
raw: Flags.boolean({ description: 'Emit the verbatim Notion response body.', default: false }),
|
|
19
|
+
};
|
|
20
|
+
static args = {
|
|
21
|
+
page: Args.string({ description: 'Page ID or Notion URL.', required: true }),
|
|
22
|
+
};
|
|
23
|
+
static enableJsonFlag = true;
|
|
24
|
+
static strict = true;
|
|
25
|
+
async execute() {
|
|
26
|
+
const { args, flags } = await this.parse(NotionPageReplaceContent);
|
|
27
|
+
this.outputFlags = flags;
|
|
28
|
+
requireYes(flags.yes);
|
|
29
|
+
const pageId = parseNotionId(args.page);
|
|
30
|
+
const input = await readMarkdownInput(flags.file, flags.text);
|
|
31
|
+
// Canonical replace_content body — matches notion page replace and the
|
|
32
|
+
// page-content test contract. allow_deleting_content stays top-level and
|
|
33
|
+
// is only emitted when true.
|
|
34
|
+
const body = {
|
|
35
|
+
type: 'replace_content',
|
|
36
|
+
replace_content: { markdown: input.markdown },
|
|
37
|
+
...(flags['allow-deleting-content'] ? { allow_deleting_content: true } : {}),
|
|
38
|
+
};
|
|
39
|
+
const result = await notionPagePatchMarkdown(pageId, body);
|
|
40
|
+
const raw = this.rememberResult(result);
|
|
41
|
+
const rawRecord = recordValue(raw);
|
|
42
|
+
const markdown = stringValue(rawRecord?.markdown) ?? '';
|
|
43
|
+
const before = estimateText(stringValue(rawRecord?.before_markdown) ?? stringValue(rawRecord?.previous_markdown) ?? '');
|
|
44
|
+
const after = estimateText(input.markdown);
|
|
45
|
+
const delta = deltaEstimate(before, after);
|
|
46
|
+
const truncated = boolValue(rawRecord?.truncated) ?? false;
|
|
47
|
+
return {
|
|
48
|
+
action: 'page.replace-content',
|
|
49
|
+
page: pageRef(raw, pageId),
|
|
50
|
+
edit: {
|
|
51
|
+
mode: 'replace_content',
|
|
52
|
+
source: input.source,
|
|
53
|
+
allow_deleting_content: flags['allow-deleting-content'],
|
|
54
|
+
before_estimate: before,
|
|
55
|
+
after_estimate: after,
|
|
56
|
+
delta,
|
|
57
|
+
},
|
|
58
|
+
result: { markdown_preview: markdownPreview(markdown), truncated, unknown_block_ids: unknownBlockIds(raw) },
|
|
59
|
+
...(flags['allow-deleting-content'] ? { warning: 'Full body replacement completed with allow_deleting_content=true. Review in Notion.' } : {}),
|
|
60
|
+
notion_version: '2026-03-11',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
render(data) {
|
|
64
|
+
return renderReceipt([
|
|
65
|
+
['action', data.action],
|
|
66
|
+
['page', `${data.page.title} (${data.page.id})`],
|
|
67
|
+
['mode', data.edit.mode],
|
|
68
|
+
['source', data.edit.source],
|
|
69
|
+
['allow_deleting_content', data.edit.allow_deleting_content],
|
|
70
|
+
['before_estimate', formatEstimate(data.edit.before_estimate)],
|
|
71
|
+
['after_estimate', formatEstimate(data.edit.after_estimate)],
|
|
72
|
+
['delta', formatEstimate(data.edit.delta)],
|
|
73
|
+
['markdown_preview', `\n${data.result.markdown_preview}`],
|
|
74
|
+
['truncated', data.result.truncated],
|
|
75
|
+
['unknown_block_ids', data.result.unknown_block_ids],
|
|
76
|
+
...(data.warning ? [['warning', data.warning.toLowerCase()]] : []),
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=replace-content.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { notionPagePatchMarkdown } from '../../../services/notion.js';
|
|
3
|
+
import { boolValue, markdownPreview, notionErrors, NotionWriteCommand, numberValue, pageRef, parseNotionId, recordValue, renderReceipt, requireYes, stringValue, unknownBlockIds, } from '../../../lib/notion/trash-move/support.js';
|
|
4
|
+
export default class NotionPageReplaceText extends NotionWriteCommand {
|
|
5
|
+
static errors = notionErrors();
|
|
6
|
+
static summary = 'Search and replace exact markdown in a Notion page';
|
|
7
|
+
static description = 'Patch page markdown with Notion update_content. --allow-deleting-content is only sent when explicitly supplied.';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> notion page replace-text 195de922-1179-449f-ab80-75a27c979105 --old "draft" --new "final" --yes',
|
|
10
|
+
'<%= config.bin %> notion page replace-text 195de922-1179-449f-ab80-75a27c979105 --old "old" --new "new" --replace-all --yes',
|
|
11
|
+
];
|
|
12
|
+
static aliases = [];
|
|
13
|
+
static flags = {
|
|
14
|
+
old: Flags.string({ description: 'Exact markdown to replace.', required: true }),
|
|
15
|
+
new: Flags.string({ description: 'Replacement markdown.', required: true }),
|
|
16
|
+
'replace-all': Flags.boolean({ description: 'Replace all exact matches.', default: false }),
|
|
17
|
+
'allow-deleting-content': Flags.boolean({ description: 'Allow deletion of child pages/databases if Notion requires it.', default: false }),
|
|
18
|
+
yes: Flags.boolean({ description: 'Confirm the write without prompting.', default: false }),
|
|
19
|
+
raw: Flags.boolean({ description: 'Emit the verbatim Notion response body.', default: false }),
|
|
20
|
+
};
|
|
21
|
+
static args = {
|
|
22
|
+
page: Args.string({ description: 'Page ID or Notion URL.', required: true }),
|
|
23
|
+
};
|
|
24
|
+
static enableJsonFlag = true;
|
|
25
|
+
static strict = true;
|
|
26
|
+
async execute() {
|
|
27
|
+
const { args, flags } = await this.parse(NotionPageReplaceText);
|
|
28
|
+
this.outputFlags = flags;
|
|
29
|
+
requireYes(flags.yes);
|
|
30
|
+
const pageId = parseNotionId(args.page);
|
|
31
|
+
// Canonical update_content body — matches notion page edit and the
|
|
32
|
+
// page-content test contract. allow_deleting_content stays top-level and
|
|
33
|
+
// is only emitted when true; never nested inside update_content.
|
|
34
|
+
const body = {
|
|
35
|
+
type: 'update_content',
|
|
36
|
+
update_content: {
|
|
37
|
+
content_updates: [{ old_str: flags.old, new_str: flags.new, replace_all_matches: flags['replace-all'] }],
|
|
38
|
+
},
|
|
39
|
+
...(flags['allow-deleting-content'] ? { allow_deleting_content: true } : {}),
|
|
40
|
+
};
|
|
41
|
+
const result = await notionPagePatchMarkdown(pageId, body);
|
|
42
|
+
const raw = this.rememberResult(result);
|
|
43
|
+
const rawRecord = recordValue(raw);
|
|
44
|
+
const markdown = stringValue(rawRecord?.markdown) ?? '';
|
|
45
|
+
const replaced = numberValue(rawRecord?.matches_replaced) ?? numberValue(recordValue(rawRecord?.edit)?.matches_replaced) ?? (flags['replace-all'] ? 0 : 1);
|
|
46
|
+
const truncated = boolValue(rawRecord?.truncated) ?? false;
|
|
47
|
+
return {
|
|
48
|
+
action: 'page.replace-text',
|
|
49
|
+
page: pageRef(raw, pageId),
|
|
50
|
+
edit: {
|
|
51
|
+
mode: 'update_content',
|
|
52
|
+
matches_replaced: replaced,
|
|
53
|
+
replace_all_matches: flags['replace-all'],
|
|
54
|
+
allow_deleting_content: flags['allow-deleting-content'],
|
|
55
|
+
old_preview: markdownPreview(flags.old),
|
|
56
|
+
new_preview: markdownPreview(flags.new),
|
|
57
|
+
},
|
|
58
|
+
result: { markdown_preview: markdownPreview(markdown), truncated, unknown_block_ids: unknownBlockIds(raw) },
|
|
59
|
+
...(flags.new.length < flags.old.length ? { warning: 'Content text was removed. Child page/database deletion was not allowed.' } : {}),
|
|
60
|
+
notion_version: '2026-03-11',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
render(data) {
|
|
64
|
+
return renderReceipt([
|
|
65
|
+
['action', data.action],
|
|
66
|
+
['page', `${data.page.title} (${data.page.id})`],
|
|
67
|
+
['mode', data.edit.mode],
|
|
68
|
+
['matches_replaced', data.edit.matches_replaced],
|
|
69
|
+
['replace_all_matches', data.edit.replace_all_matches],
|
|
70
|
+
['allow_deleting_content', data.edit.allow_deleting_content],
|
|
71
|
+
['old_preview', data.edit.old_preview],
|
|
72
|
+
['new_preview', data.edit.new_preview],
|
|
73
|
+
['markdown_preview', `\n${data.result.markdown_preview}`],
|
|
74
|
+
['truncated', data.result.truncated],
|
|
75
|
+
['unknown_block_ids', data.result.unknown_block_ids],
|
|
76
|
+
...(data.warning ? [['warning', data.warning.toLowerCase()]] : []),
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=replace-text.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { ErrorCode } from '@eightstate/contracts/errors';
|
|
3
|
+
import { writeStdout } from '../../../io/io.js';
|
|
4
|
+
import { notionPagePatchMarkdown } from '../../../services/notion.js';
|
|
5
|
+
import { PAGE_CONTENT_ERROR_CODES, PageContentReplaceDataSchema, PageRefArg, buildReplaceBody, emitRaw, extractPageInfo, pageIdFromRef, previewMarkdown, readMarkdownSource, renderReplace, withoutRaw, RawBypassCommand, } from '../../../lib/notion/page/content-common.js';
|
|
6
|
+
export { PageContentReplaceDataSchema };
|
|
7
|
+
export default class NotionPageReplace extends RawBypassCommand {
|
|
8
|
+
static errors = [...PAGE_CONTENT_ERROR_CODES, ErrorCode.UsageUnknownFlag];
|
|
9
|
+
static summary = 'Replace page body markdown';
|
|
10
|
+
static description = 'Replace a Notion page body through Notion replace_content using native enhanced markdown.';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> notion page replace 362e1f4a-7d8c-81b0-9f6e-f2c9a7b630dc --file q2-planning-replacement.md',
|
|
13
|
+
'<%= config.bin %> notion page replace 362e1f4a-7d8c-81b0-9f6e-f2c9a7b630dc --markdown "# Q2 Planning"',
|
|
14
|
+
'<%= config.bin %> notion page replace 362e1f4a-7d8c-81b0-9f6e-f2c9a7b630dc --stdin --raw',
|
|
15
|
+
];
|
|
16
|
+
static aliases = [];
|
|
17
|
+
static flags = {
|
|
18
|
+
markdown: Flags.string({ description: 'Enhanced markdown content.' }),
|
|
19
|
+
file: Flags.string({ description: 'Read enhanced markdown content from a file.' }),
|
|
20
|
+
stdin: Flags.boolean({ description: 'Read enhanced markdown content from stdin.', default: false }),
|
|
21
|
+
'allow-deleting-content': Flags.boolean({ description: 'Allow deletion of nested child pages/databases if Notion detects it.', default: false }),
|
|
22
|
+
raw: Flags.boolean({ description: 'Emit the verbatim Notion response body.', default: false }),
|
|
23
|
+
};
|
|
24
|
+
static args = {
|
|
25
|
+
page: Args.string(PageRefArg),
|
|
26
|
+
};
|
|
27
|
+
static enableJsonFlag = true;
|
|
28
|
+
static strict = true;
|
|
29
|
+
async execute() {
|
|
30
|
+
const { args, flags } = await this.parse(NotionPageReplace);
|
|
31
|
+
const pageId = pageIdFromRef(args.page);
|
|
32
|
+
const markdown = await readMarkdownSource(flags);
|
|
33
|
+
const result = await notionPagePatchMarkdown(pageId, buildReplaceBody(markdown, flags['allow-deleting-content']));
|
|
34
|
+
if (flags.raw) {
|
|
35
|
+
await emitRaw(result.raw);
|
|
36
|
+
this.humanOutputHandled = true;
|
|
37
|
+
return {
|
|
38
|
+
surface: 'page-content',
|
|
39
|
+
command: 'replace',
|
|
40
|
+
page: { id: pageId },
|
|
41
|
+
status: 'changed',
|
|
42
|
+
allow_deleting_content: flags['allow-deleting-content'],
|
|
43
|
+
submitted: { format: 'notion-enhanced-markdown', chars: markdown.length, preview: previewMarkdown(markdown, 20, 1200) },
|
|
44
|
+
raw: result.raw,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const page = extractPageInfo(result.data, pageId);
|
|
48
|
+
const data = {
|
|
49
|
+
surface: 'page-content',
|
|
50
|
+
command: 'replace',
|
|
51
|
+
page,
|
|
52
|
+
status: 'changed',
|
|
53
|
+
allow_deleting_content: flags['allow-deleting-content'],
|
|
54
|
+
submitted: { format: 'notion-enhanced-markdown', chars: markdown.length, preview: previewMarkdown(markdown, 20, 1200) },
|
|
55
|
+
};
|
|
56
|
+
if (flags.json)
|
|
57
|
+
return withoutRaw(data);
|
|
58
|
+
await writeStdout(`${renderReplace(data)}\n`);
|
|
59
|
+
this.humanOutputHandled = true;
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=replace.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { notionPagesTrash } from '../../../services/notion.js';
|
|
3
|
+
import { botName, formatParentCompact, notionErrors, NotionWriteCommand, pageRef, parentRef, parseNotionId, renderReceipt, requireYes, } from '../../../lib/notion/trash-move/support.js';
|
|
4
|
+
export default class NotionPageTrash extends NotionWriteCommand {
|
|
5
|
+
static errors = notionErrors();
|
|
6
|
+
static summary = 'Move a Notion page to Trash, or restore it with --restore';
|
|
7
|
+
static description = 'Soft-trash or restore a Notion page through the enrolled Notion bot. Requires --yes in non-interactive mode.';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> notion page trash 195de922-1179-449f-ab80-75a27c979105 --yes',
|
|
10
|
+
'<%= config.bin %> notion page trash 195de922-1179-449f-ab80-75a27c979105 --restore --yes',
|
|
11
|
+
'<%= config.bin %> notion page trash https://www.notion.so/Old-launch-notes-195de9221179449fab8075a27c979105 --raw --yes',
|
|
12
|
+
];
|
|
13
|
+
static aliases = [];
|
|
14
|
+
static flags = {
|
|
15
|
+
restore: Flags.boolean({ description: 'Restore the page instead of trashing it.', default: false }),
|
|
16
|
+
yes: Flags.boolean({ description: 'Confirm the destructive write without prompting.', default: false }),
|
|
17
|
+
raw: Flags.boolean({ description: 'Emit the verbatim Notion response body.', default: false }),
|
|
18
|
+
};
|
|
19
|
+
static args = {
|
|
20
|
+
page: Args.string({ description: 'Page ID or Notion URL.', required: true }),
|
|
21
|
+
};
|
|
22
|
+
static enableJsonFlag = true;
|
|
23
|
+
static strict = true;
|
|
24
|
+
async execute() {
|
|
25
|
+
const { args, flags } = await this.parse(NotionPageTrash);
|
|
26
|
+
this.outputFlags = flags;
|
|
27
|
+
requireYes(flags.yes);
|
|
28
|
+
const pageId = parseNotionId(args.page);
|
|
29
|
+
const result = await notionPagesTrash(pageId, !flags.restore);
|
|
30
|
+
const raw = this.rememberResult(result);
|
|
31
|
+
const page = pageRef(raw, pageId);
|
|
32
|
+
const bot = botName(raw);
|
|
33
|
+
if (flags.restore) {
|
|
34
|
+
return {
|
|
35
|
+
action: 'page.restore',
|
|
36
|
+
page,
|
|
37
|
+
state: { before: 'in_trash', after: 'active' },
|
|
38
|
+
restored_to: parentRef(result.data && typeof result.data === 'object' ? result.data.parent : undefined),
|
|
39
|
+
next: 'Verify the restored page appears in the expected parent.',
|
|
40
|
+
...(bot ? { bot } : {}),
|
|
41
|
+
notion_version: '2026-03-11',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
action: 'page.trash',
|
|
46
|
+
page,
|
|
47
|
+
state: { before: 'active', after: 'in_trash' },
|
|
48
|
+
destructive: true,
|
|
49
|
+
reversible: true,
|
|
50
|
+
undo: `escli notion page trash ${page.id} --restore --yes`,
|
|
51
|
+
...(bot ? { bot } : {}),
|
|
52
|
+
notion_version: '2026-03-11',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
render(data) {
|
|
56
|
+
if (data.action === 'page.restore') {
|
|
57
|
+
return renderReceipt([
|
|
58
|
+
['action', data.action],
|
|
59
|
+
['page', `${data.page.title} (${data.page.id})`],
|
|
60
|
+
['state', `${data.state.before} → ${data.state.after}`],
|
|
61
|
+
['restored_to', formatParentCompact(data.restored_to)],
|
|
62
|
+
['next', 'verify the page appears in the expected parent'],
|
|
63
|
+
['bot', data.bot ?? ''],
|
|
64
|
+
['notion_version', data.notion_version],
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
return renderReceipt([
|
|
68
|
+
['action', data.action],
|
|
69
|
+
['page', `${data.page.title} (${data.page.id})`],
|
|
70
|
+
['state', `${data.state.before} → ${data.state.after}`],
|
|
71
|
+
['destructive', true],
|
|
72
|
+
['reversible', true],
|
|
73
|
+
['undo', data.undo ?? ''],
|
|
74
|
+
['bot', data.bot ?? ''],
|
|
75
|
+
['notion_version', data.notion_version],
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=trash.js.map
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { ErrorCode } from '@eightstate/contracts/errors';
|
|
4
|
+
import { BaseCommand } from '../../base-command.js';
|
|
5
|
+
import { renderTable } from '../../io/render-table.js';
|
|
6
|
+
import { renderTrailer } from '../../io/render-trailer.js';
|
|
7
|
+
import { writeStdout, writeStderr } from '../../io/io.js';
|
|
8
|
+
import { NOTION_VERSION, notionSearch } from '../../services/notion.js';
|
|
9
|
+
export const NotionSearchResultSchema = z.object({
|
|
10
|
+
rank: z.number().int().positive(),
|
|
11
|
+
kind: z.enum(['page', 'data_source']),
|
|
12
|
+
id: z.string(),
|
|
13
|
+
title: z.string().optional(),
|
|
14
|
+
name: z.string().optional(),
|
|
15
|
+
context: z.string(),
|
|
16
|
+
last_edited: z.string(),
|
|
17
|
+
match: z.string(),
|
|
18
|
+
next: z.string(),
|
|
19
|
+
});
|
|
20
|
+
export const NotionSearchDataSchema = z.object({
|
|
21
|
+
query: z.string(),
|
|
22
|
+
scope: z.literal('bot-visible'),
|
|
23
|
+
trash: z.enum(['hidden', 'included']),
|
|
24
|
+
results: z.array(NotionSearchResultSchema),
|
|
25
|
+
warnings: z.array(z.string()),
|
|
26
|
+
next: z.string().nullable(),
|
|
27
|
+
notion_version: z.literal(NOTION_VERSION),
|
|
28
|
+
});
|
|
29
|
+
let cachedVisibleScopes;
|
|
30
|
+
export default class NotionSearch extends BaseCommand {
|
|
31
|
+
static errors = [ErrorCode.UsageRequired, ErrorCode.UsageInvalid, ErrorCode.AuthRequired, ErrorCode.GateInvalidResponse, ErrorCode.ApiError, ErrorCode.NotionUnauthorized, ErrorCode.NotionRestricted, ErrorCode.NotionNotFound, ErrorCode.NotionRateLimited, ErrorCode.NotionValidation, ErrorCode.NotionConflict, ErrorCode.NotionTimeoutUncertain, ErrorCode.NotionEnrollmentRequired, ErrorCode.NetworkError, ErrorCode.NetworkTimeout, ErrorCode.ServiceUnavailable];
|
|
32
|
+
static summary = 'Find bot-visible Notion page/data-source targets';
|
|
33
|
+
static description = 'Search Notion pages and data sources visible to the enrolled bot.';
|
|
34
|
+
static examples = ['<%= config.bin %> notion search roadmap', '<%= config.bin %> notion search "roadmap" --next 10d7e4d2-9a41-4c73-9d2d-4d3d860d5a91', '<%= config.bin %> notion search "roadmap" --raw'];
|
|
35
|
+
static aliases = ['notion s'];
|
|
36
|
+
static flags = {
|
|
37
|
+
next: Flags.string({ description: "Continue the same search using Notion's opaque pagination token." }),
|
|
38
|
+
raw: Flags.boolean({ description: 'Emit the verbatim Notion response body.', default: false }),
|
|
39
|
+
'include-trash': Flags.boolean({ description: 'Include results where Notion reports in_trash=true.', default: false }),
|
|
40
|
+
};
|
|
41
|
+
static args = {
|
|
42
|
+
query: Args.string({ description: 'Search query.', required: true }),
|
|
43
|
+
};
|
|
44
|
+
static enableJsonFlag = true;
|
|
45
|
+
static strict = true;
|
|
46
|
+
async execute() {
|
|
47
|
+
const { args, flags } = await this.parse(NotionSearch);
|
|
48
|
+
const response = await notionSearch(args.query, defined({ start_cursor: flags.next, include_trash: flags['include-trash'] || undefined }));
|
|
49
|
+
if (flags.raw) {
|
|
50
|
+
await writeStdout(`${JSON.stringify(response.raw)}\n`);
|
|
51
|
+
this.humanOutputHandled = true;
|
|
52
|
+
return response.raw;
|
|
53
|
+
}
|
|
54
|
+
const data = shapeSearchData(args.query, response.data, response.meta.next, flags.next, flags['include-trash']);
|
|
55
|
+
if (!flags.json) {
|
|
56
|
+
await writeStdout(`${renderNotionSearch(data, flags.next)}\n`);
|
|
57
|
+
this.humanOutputHandled = true;
|
|
58
|
+
if (data.results.length === 0 && !flags.quiet)
|
|
59
|
+
await writeZeroResultHint(args.query);
|
|
60
|
+
}
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function renderNotionSearch(data, nextToken) {
|
|
65
|
+
const lines = [`notion.search query=${data.query} scope=${data.scope} trash=${data.trash}${nextToken ? ` next=${nextToken}` : ''}`];
|
|
66
|
+
lines.push(renderTable({
|
|
67
|
+
header: ['#', 'kind', 'title/name', 'id', 'context', 'edited', 'match', 'next'],
|
|
68
|
+
rows: data.results.map((result) => [result.rank, result.kind, result.title ?? result.name ?? '', result.id, result.context, result.last_edited, result.match, actionLabel(result)]),
|
|
69
|
+
}));
|
|
70
|
+
if (data.results.length === 0)
|
|
71
|
+
lines.push('0 results.');
|
|
72
|
+
const trailer = renderTrailer({ next: data.next });
|
|
73
|
+
if (trailer)
|
|
74
|
+
lines.push(trailer);
|
|
75
|
+
const incompleteWarning = data.warnings.find((warning) => warning.startsWith('Notion says this search is incomplete:'));
|
|
76
|
+
if (incompleteWarning)
|
|
77
|
+
lines.push(`warning: ${incompleteWarning}`);
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
}
|
|
80
|
+
function shapeSearchData(query, body, metaNext, nextToken, includeTrash) {
|
|
81
|
+
const response = recordValue(body);
|
|
82
|
+
const results = (Array.isArray(response?.results) ? response.results : [])
|
|
83
|
+
.filter((result) => includeTrash || recordValue(result)?.in_trash !== true)
|
|
84
|
+
.map((result, index) => shapeResult(result, index, nextToken))
|
|
85
|
+
.filter((result) => result !== undefined);
|
|
86
|
+
const warnings = ['Search is bot-visible and not exhaustive workspace RAG.'];
|
|
87
|
+
if (response?.request_status?.type === 'incomplete') {
|
|
88
|
+
const reason = stringValue(response.request_status.incomplete_reason) ?? 'unknown';
|
|
89
|
+
warnings.push(`Notion says this search is incomplete: ${reason}. Narrow the query; do not treat this as exhaustive.`);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
query,
|
|
93
|
+
scope: 'bot-visible',
|
|
94
|
+
trash: includeTrash ? 'included' : 'hidden',
|
|
95
|
+
results,
|
|
96
|
+
warnings,
|
|
97
|
+
next: metaNext ?? stringValue(response?.next_cursor) ?? null,
|
|
98
|
+
notion_version: NOTION_VERSION,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function shapeResult(value, index, nextToken) {
|
|
102
|
+
const result = recordValue(value);
|
|
103
|
+
const id = stringValue(result?.id);
|
|
104
|
+
const object = stringValue(result?.object);
|
|
105
|
+
if (!result || !id || (object !== 'page' && object !== 'data_source'))
|
|
106
|
+
return undefined;
|
|
107
|
+
const title = titleForResult(result, object);
|
|
108
|
+
const kind = object;
|
|
109
|
+
return {
|
|
110
|
+
rank: index + 1 + (nextToken ? 0 : 0),
|
|
111
|
+
kind,
|
|
112
|
+
id,
|
|
113
|
+
...(kind === 'page' ? { title } : { name: title }),
|
|
114
|
+
context: contextForResult(result),
|
|
115
|
+
last_edited: editedForResult(result),
|
|
116
|
+
match: matchForResult(title),
|
|
117
|
+
next: kind === 'page' ? `escli notion fetch ${id}` : `escli notion ds query ${id}`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function titleForResult(result, object) {
|
|
121
|
+
if (object === 'data_source')
|
|
122
|
+
return plainText(result.title) || 'Untitled';
|
|
123
|
+
const properties = recordValue(result.properties);
|
|
124
|
+
const titleProperty = Object.values(properties ?? {}).find((property) => recordValue(property)?.type === 'title');
|
|
125
|
+
return plainText(recordValue(titleProperty)?.title) || plainText(result.title) || 'Untitled';
|
|
126
|
+
}
|
|
127
|
+
function contextForResult(result) {
|
|
128
|
+
const object = stringValue(result.object);
|
|
129
|
+
if (object === 'data_source') {
|
|
130
|
+
const parent = recordValue(result.database_parent);
|
|
131
|
+
const parentId = stringValue(parent?.database_id);
|
|
132
|
+
const props = Object.keys(recordValue(result.properties) ?? {}).filter((name) => name !== 'Name').slice(0, 4);
|
|
133
|
+
const propCount = Object.keys(recordValue(result.properties) ?? {}).filter((name) => name !== 'Name').length;
|
|
134
|
+
const propsSuffix = props.length > 0 ? `; props: ${props.join(', ')}${propCount > props.length ? ` +${propCount - props.length}` : ''}` : '';
|
|
135
|
+
return `db: ${parentId ?? 'unknown'}${propsSuffix}`;
|
|
136
|
+
}
|
|
137
|
+
const parent = recordValue(result.parent);
|
|
138
|
+
if (!parent)
|
|
139
|
+
return 'parent: unknown';
|
|
140
|
+
const type = stringValue(parent.type);
|
|
141
|
+
const id = type ? stringValue(parent[type]) : undefined;
|
|
142
|
+
if (type === 'workspace')
|
|
143
|
+
return 'parent: workspace';
|
|
144
|
+
if (type === 'page_id')
|
|
145
|
+
return `parent: ${id ?? 'unknown'}`;
|
|
146
|
+
if (type === 'database_id')
|
|
147
|
+
return `db: ${id ?? 'unknown'}`;
|
|
148
|
+
if (type === 'data_source_id')
|
|
149
|
+
return `data_source: ${id ?? 'unknown'}`;
|
|
150
|
+
return `parent: ${id ?? 'unknown'}`;
|
|
151
|
+
}
|
|
152
|
+
function editedForResult(result) {
|
|
153
|
+
const edited = stringValue(result.last_edited_time);
|
|
154
|
+
const editedBy = recordValue(result.last_edited_by);
|
|
155
|
+
const name = stringValue(editedBy?.name) ?? stringValue(editedBy?.id);
|
|
156
|
+
const timestamp = edited ? formatEditedTime(edited) : 'unknown';
|
|
157
|
+
return name ? `${timestamp} by ${name}` : timestamp;
|
|
158
|
+
}
|
|
159
|
+
function formatEditedTime(value) {
|
|
160
|
+
return value.replace(/:\d\d\.\d{3}Z$/u, 'Z');
|
|
161
|
+
}
|
|
162
|
+
function matchForResult(title) {
|
|
163
|
+
return title === 'Untitled' ? 'metadata match' : 'title/name match';
|
|
164
|
+
}
|
|
165
|
+
function actionLabel(result) {
|
|
166
|
+
return result.kind === 'page' ? `fetch ${result.id}` : `ds query ${result.id}`;
|
|
167
|
+
}
|
|
168
|
+
async function writeZeroResultHint(query) {
|
|
169
|
+
const scopes = await visibleScopes();
|
|
170
|
+
if (scopes.length === 0)
|
|
171
|
+
return;
|
|
172
|
+
await writeStderr(`no matches for "${query}" — bot can see: ${scopes.join(', ')}. Try one of these terms or share the target with the Notion bot.\n`);
|
|
173
|
+
}
|
|
174
|
+
async function visibleScopes() {
|
|
175
|
+
if (cachedVisibleScopes)
|
|
176
|
+
return cachedVisibleScopes;
|
|
177
|
+
const response = await notionSearch('', { page_size: 5 });
|
|
178
|
+
const root = recordValue(response.data);
|
|
179
|
+
const results = Array.isArray(root?.results) ? root.results : [];
|
|
180
|
+
cachedVisibleScopes = results.map((result) => titleForScope(result)).filter((name) => Boolean(name)).slice(0, 5);
|
|
181
|
+
return cachedVisibleScopes;
|
|
182
|
+
}
|
|
183
|
+
function titleForScope(value) {
|
|
184
|
+
const result = recordValue(value);
|
|
185
|
+
if (!result || result.in_trash === true)
|
|
186
|
+
return undefined;
|
|
187
|
+
const object = stringValue(result.object);
|
|
188
|
+
const title = titleForResult(result, object ?? '');
|
|
189
|
+
return title === 'Untitled' ? undefined : title;
|
|
190
|
+
}
|
|
191
|
+
function plainText(value) {
|
|
192
|
+
if (typeof value === 'string')
|
|
193
|
+
return value;
|
|
194
|
+
if (!Array.isArray(value))
|
|
195
|
+
return '';
|
|
196
|
+
return value.map((part) => stringValue(recordValue(part)?.plain_text) ?? stringValue(recordValue(recordValue(part)?.text)?.content) ?? '').join('').trim();
|
|
197
|
+
}
|
|
198
|
+
function defined(input) {
|
|
199
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
200
|
+
}
|
|
201
|
+
function recordValue(value) {
|
|
202
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
203
|
+
}
|
|
204
|
+
function stringValue(value) {
|
|
205
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=search.js.map
|