@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.
Files changed (45) hide show
  1. package/dist/commands/notion/block/trash.js +71 -0
  2. package/dist/commands/notion/comments/add.js +48 -0
  3. package/dist/commands/notion/comments/get.js +42 -0
  4. package/dist/commands/notion/comments/list.js +55 -0
  5. package/dist/commands/notion/comments/reply.js +45 -0
  6. package/dist/commands/notion/comments/thread.js +87 -0
  7. package/dist/commands/notion/db/create.js +555 -0
  8. package/dist/commands/notion/db/query.js +451 -0
  9. package/dist/commands/notion/db/row/create.js +74 -0
  10. package/dist/commands/notion/db/row/update.js +165 -0
  11. package/dist/commands/notion/ds/create.js +73 -0
  12. package/dist/commands/notion/enroll.js +302 -0
  13. package/dist/commands/notion/index.js +24 -0
  14. package/dist/commands/notion/page/edit.js +73 -0
  15. package/dist/commands/notion/page/move.js +59 -0
  16. package/dist/commands/notion/page/read.js +60 -0
  17. package/dist/commands/notion/page/replace-content.js +80 -0
  18. package/dist/commands/notion/page/replace-text.js +80 -0
  19. package/dist/commands/notion/page/replace.js +63 -0
  20. package/dist/commands/notion/page/trash.js +79 -0
  21. package/dist/commands/notion/search.js +207 -0
  22. package/dist/commands/notion/upload/attach.js +105 -0
  23. package/dist/commands/notion/upload/index.js +129 -0
  24. package/dist/commands/notion/upload/list.js +78 -0
  25. package/dist/commands/notion/upload/status.js +76 -0
  26. package/dist/commands/notion/view/create.js +78 -0
  27. package/dist/commands/notion/whoami.js +96 -0
  28. package/dist/commands/research.js +11 -0
  29. package/dist/entry.js +16 -9
  30. package/dist/io/render-kv.js +12 -0
  31. package/dist/io/render-labeled.js +16 -0
  32. package/dist/io/render-table.js +19 -0
  33. package/dist/io/render-trailer.js +7 -0
  34. package/dist/lib/manifest.js +3 -2
  35. package/dist/lib/notion/comments/shared.js +366 -0
  36. package/dist/lib/notion/db-row/common.js +367 -0
  37. package/dist/lib/notion/manifest-pass.js +4 -0
  38. package/dist/lib/notion/page/content-common.js +473 -0
  39. package/dist/lib/notion/trash-move/support.js +300 -0
  40. package/dist/lib/notion/upload/shared.js +372 -0
  41. package/dist/lib/registry.js +118 -25
  42. package/dist/services/notion.js +274 -0
  43. package/dist/services/research.js +31 -10
  44. package/oclif.manifest.json +4084 -1
  45. package/package.json +22 -17
@@ -0,0 +1,105 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base-command.js';
3
+ import { writeStdout } from '../../../io/io.js';
4
+ import { proxy } from '../../../services/notion.js';
5
+ import { attachUploadedFiles, createdBlockId, emitNotionFailure, emitNotionSuccess, emitRaw, fileUploadErrors, FileUploadAttachReceiptSchema, nextForTarget, normalizeUpload, parseAttachTarget, rawFlag, renderAttachReceipt, requireUploaded, targetLabel, } from '../../../lib/notion/upload/shared.js';
6
+ import { ErrorCode } from '@eightstate/contracts/errors';
7
+ import { ExitCodes } from '@eightstate/contracts/exit-codes';
8
+ import { EscliError } from '../../../lib/escli-error.js';
9
+ import { hasManifestFlag } from '../../../lib/notion/manifest-pass.js';
10
+ export { FileUploadAttachReceiptSchema as NotionUploadAttachDataSchema };
11
+ export default class NotionUploadAttach extends BaseCommand {
12
+ static errors = fileUploadErrors;
13
+ static summary = 'Attach existing Notion file_upload ids';
14
+ static description = 'Validates existing file_upload ids and attaches them to a block, page files property, page cover, or page icon.';
15
+ static examples = [
16
+ '<%= config.bin %> notion file-upload attach fu_4383 --attach-to block:blk_6c42 --as pdf',
17
+ '<%= config.bin %> notion file-upload attach fu_1 fu_2 --attach property --page pg_123 --property Attachments',
18
+ ];
19
+ static aliases = ['notion upload attach'];
20
+ static flags = {
21
+ name: Flags.string({ description: 'Display name. Allowed only with one file_upload id.' }),
22
+ 'attach-to': Flags.string({ description: 'Compact target: block:<id>, property:<page-id>:<property>, cover:<page-id>, or icon:<page-id>.' }),
23
+ attach: Flags.option({ description: 'Attachment target kind.', options: ['block', 'property', 'page'] })(),
24
+ parent: Flags.string({ description: 'Block/page id for block attachment.' }),
25
+ as: Flags.option({ description: 'Block attachment type.', options: ['file', 'image', 'pdf', 'video', 'audio'], default: 'file' })(),
26
+ page: Flags.string({ description: 'Page id for property/page attachment.' }),
27
+ property: Flags.string({ description: 'Files property name or id.' }),
28
+ slot: Flags.option({ description: 'Page file slot.', options: ['cover', 'icon'] })(),
29
+ caption: Flags.string({ description: 'Caption for block attachments.' }),
30
+ 'replace-all': Flags.boolean({ description: 'Replace property files instead of appending.', default: false }),
31
+ yes: Flags.boolean({ description: 'Accept non-interactive replacement confirmations.', default: false }),
32
+ raw: rawFlag,
33
+ };
34
+ static args = {
35
+ 'file-upload-id': Args.string({ description: 'Notion file_upload id(s).', required: true, multiple: true }),
36
+ };
37
+ static enableJsonFlag = true;
38
+ static strict = true;
39
+ async run() {
40
+ if (hasManifestFlag(this.argv)) {
41
+ await super.run();
42
+ return;
43
+ }
44
+ const start = Date.now();
45
+ let json = false;
46
+ try {
47
+ const { data, raw } = await this.perform();
48
+ if (raw)
49
+ return;
50
+ json = data.json;
51
+ if (json)
52
+ await emitNotionSuccess(this.id ?? 'notion file-upload attach', this.config.version, data.receipt, Date.now() - start);
53
+ else
54
+ await writeStdout(`${renderAttachReceipt(data.receipt)}\n`);
55
+ }
56
+ catch (error) {
57
+ await emitNotionFailure(this.id ?? 'notion file-upload attach', this.config.version, error, Date.now() - start, json);
58
+ }
59
+ }
60
+ async execute() {
61
+ return (await this.perform()).data.receipt;
62
+ }
63
+ async perform() {
64
+ const { args, flags } = await this.parse(NotionUploadAttach);
65
+ const ids = args['file-upload-id'];
66
+ if (flags.name && ids.length !== 1)
67
+ throw new EscliError('--name is only valid with one file_upload id', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
68
+ const target = parseAttachTarget(flags);
69
+ if (target.kind === 'none')
70
+ throw new EscliError('--attach or --attach-to is required', { code: ErrorCode.UsageRequired, exitCode: ExitCodes.Usage });
71
+ if (target.kind === 'property' && target.replace_all && !flags.yes)
72
+ throw new EscliError('replace-all would overwrite existing files property; rerun with --yes', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
73
+ if (target.kind === 'page' && ids.length !== 1)
74
+ throw new EscliError('page cover/icon accepts exactly one file_upload id', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
75
+ const rawChain = [];
76
+ const uploads = [];
77
+ for (const id of ids) {
78
+ const result = await proxy('GET', `/v1/file_uploads/${encodeURIComponent(id)}`);
79
+ rawChain.push(result.raw);
80
+ const upload = normalizeUpload(result.data);
81
+ upload.id ??= id;
82
+ requireUploaded(upload);
83
+ uploads.push({ id, name: flags.name ?? upload.name ?? upload.filename });
84
+ }
85
+ const attached = await attachUploadedFiles(target, uploads);
86
+ if (attached)
87
+ rawChain.push(attached.raw);
88
+ const blockId = target.kind === 'block' ? createdBlockId(attached?.data) : null;
89
+ if (flags.raw) {
90
+ await emitRaw(rawChain);
91
+ return { data: { receipt: { operation: 'file-upload.attach', attached: ids.length, target: targetLabel(target) ?? '', created_block_id: blockId, file_upload_ids: ids, file_upload_id: ids[0], next: null }, json: Boolean(flags.json) }, raw: true };
92
+ }
93
+ const receipt = FileUploadAttachReceiptSchema.parse({
94
+ operation: 'file-upload.attach',
95
+ attached: ids.length,
96
+ target: targetLabel(target),
97
+ created_block_id: blockId,
98
+ file_upload_ids: ids,
99
+ file_upload_id: ids[0],
100
+ next: nextForTarget(target, blockId),
101
+ });
102
+ return { data: { receipt, json: Boolean(flags.json) }, raw: false };
103
+ }
104
+ }
105
+ //# sourceMappingURL=attach.js.map
@@ -0,0 +1,129 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base-command.js';
3
+ import { writeStdout, writeStderr } from '../../../io/io.js';
4
+ import { notionFileUploadComplete, notionFileUploadCreate, notionFileUploadSend } from '../../../services/notion.js';
5
+ import { attachUploadedFiles, decideMode, emitRaw, fileUploadErrors, fileUploadId, FileUploadReceiptSchema, emitNotionFailure, emitNotionSuccess, nextForTarget, parseAttachTarget, rawFlag, readLocalFile, renderUploadReceipt, targetLabel, } from '../../../lib/notion/upload/shared.js';
6
+ import { ErrorCode } from '@eightstate/contracts/errors';
7
+ import { ExitCodes } from '@eightstate/contracts/exit-codes';
8
+ import { EscliError } from '../../../lib/escli-error.js';
9
+ import { hasManifestFlag } from '../../../lib/notion/manifest-pass.js';
10
+ const MIB = 1024 * 1024;
11
+ export { FileUploadReceiptSchema as NotionUploadDataSchema };
12
+ export default class NotionUpload extends BaseCommand {
13
+ static errors = fileUploadErrors;
14
+ static summary = 'Upload local files to Notion file_upload storage';
15
+ static description = 'Creates Notion file_upload objects, sends bytes, completes multipart uploads, and optionally attaches them.';
16
+ static examples = [
17
+ '<%= config.bin %> notion file-upload upload ./diagram.png',
18
+ '<%= config.bin %> notion file-upload upload ./demo.mov --attach-to block:blk_123 --as video',
19
+ '<%= config.bin %> notion file-upload upload ./diagram.png --attach property --page pg_123 --property Attachments',
20
+ ];
21
+ static aliases = ['notion upload'];
22
+ static flags = {
23
+ mode: Flags.option({ description: 'Upload mode.', options: ['auto', 'single', 'multi'], default: 'auto' })(),
24
+ name: Flags.string({ description: 'Display name. Allowed only with one file.' }),
25
+ 'part-size-mib': Flags.integer({ description: 'Multipart part size in MiB.', default: 10 }),
26
+ 'parallel-parts': Flags.integer({ description: 'Multipart parallelism hint.', default: 2 }),
27
+ 'attach-to': Flags.string({ description: 'Compact target: block:<id>, property:<page-id>:<property>, cover:<page-id>, or icon:<page-id>.' }),
28
+ attach: Flags.option({ description: 'Attachment target kind.', options: ['block', 'property', 'page'] })(),
29
+ parent: Flags.string({ description: 'Block/page id for block attachment.' }),
30
+ as: Flags.option({ description: 'Block attachment type.', options: ['file', 'image', 'pdf', 'video', 'audio'], default: 'file' })(),
31
+ page: Flags.string({ description: 'Page id for property/page attachment.' }),
32
+ property: Flags.string({ description: 'Files property name or id.' }),
33
+ slot: Flags.option({ description: 'Page file slot.', options: ['cover', 'icon'] })(),
34
+ caption: Flags.string({ description: 'Caption for block attachments.' }),
35
+ 'replace-all': Flags.boolean({ description: 'Replace property files instead of appending.', default: false }),
36
+ yes: Flags.boolean({ description: 'Accept non-interactive replacement / large batch confirmations.', default: false }),
37
+ raw: rawFlag,
38
+ };
39
+ static args = {
40
+ path: Args.string({ description: 'Local file path(s).', required: true, multiple: true }),
41
+ };
42
+ static enableJsonFlag = true;
43
+ static strict = true;
44
+ async run() {
45
+ if (hasManifestFlag(this.argv)) {
46
+ await super.run();
47
+ return;
48
+ }
49
+ const start = Date.now();
50
+ let json = false;
51
+ try {
52
+ const { data, raw } = await this.perform();
53
+ if (raw)
54
+ return;
55
+ json = data.json;
56
+ if (json)
57
+ await emitNotionSuccess(this.id ?? 'notion file-upload upload', this.config.version, data.receipt, Date.now() - start);
58
+ else {
59
+ await writeStdout(`${renderUploadReceipt(data.receipt)}\n`);
60
+ if (!data.quiet && data.unattachedWarning)
61
+ await writeStderr(`${data.unattachedWarning}\n`);
62
+ }
63
+ }
64
+ catch (error) {
65
+ await emitNotionFailure(this.id ?? 'notion file-upload upload', this.config.version, error, Date.now() - start, json);
66
+ }
67
+ }
68
+ async execute() {
69
+ return (await this.perform()).data.receipt;
70
+ }
71
+ async perform() {
72
+ const { args, flags } = await this.parse(NotionUpload);
73
+ const paths = args.path;
74
+ if (flags.name && paths.length !== 1)
75
+ throw new EscliError('--name is only valid with one file', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
76
+ if (flags['part-size-mib'] < 5 || flags['part-size-mib'] > 20)
77
+ throw new EscliError('--part-size-mib must be between 5 and 20', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
78
+ if (flags['parallel-parts'] < 1 || flags['parallel-parts'] > 3)
79
+ throw new EscliError('--parallel-parts must be between 1 and 3', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
80
+ const target = parseAttachTarget(flags);
81
+ if (target.kind === 'property' && target.replace_all && !flags.yes)
82
+ throw new EscliError('replace-all would overwrite existing files property; rerun with --yes', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
83
+ if (target.kind === 'page' && paths.length !== 1)
84
+ throw new EscliError('page cover/icon accepts exactly one file_upload id', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
85
+ const files = await Promise.all(paths.map((path) => readLocalFile(path)));
86
+ const uploaded = [];
87
+ const rawChain = [];
88
+ for (const file of files) {
89
+ const name = flags.name ?? file.name;
90
+ const mode = decideMode(file.size, flags.mode);
91
+ const parts = mode === 'multi_part' ? Math.max(1, Math.ceil(file.size / (flags['part-size-mib'] * MIB))) : undefined;
92
+ const created = await notionFileUploadCreate({ mode, filename: name, name, ...(parts ? { number_of_parts: parts } : {}) });
93
+ rawChain.push(created.raw);
94
+ const id = fileUploadId(created.data);
95
+ if (mode === 'single_part') {
96
+ const sent = await notionFileUploadSend(id, { file: file.bytes, filename: name });
97
+ rawChain.push(sent.raw);
98
+ }
99
+ else {
100
+ for (let index = 0; index < (parts ?? 1); index += 1) {
101
+ const start = index * flags['part-size-mib'] * MIB;
102
+ const end = Math.min(file.bytes.length, start + flags['part-size-mib'] * MIB);
103
+ const sent = await notionFileUploadSend(id, { file: file.bytes.subarray(start, end), filename: name });
104
+ rawChain.push(sent.raw);
105
+ }
106
+ const completed = await notionFileUploadComplete(id);
107
+ rawChain.push(completed.raw);
108
+ }
109
+ uploaded.push({ name, file_upload_id: id, status: 'uploaded', mode, size_bytes: file.size, parts, persistent: target.kind !== 'none' });
110
+ }
111
+ const attachResult = await attachUploadedFiles(target, uploaded.map((file) => ({ id: file.file_upload_id, name: file.name })));
112
+ if (attachResult)
113
+ rawChain.push(attachResult.raw);
114
+ if (flags.raw) {
115
+ await emitRaw(rawChain);
116
+ return { data: { receipt: { operation: 'file-upload.upload', uploaded: uploaded.length, attached: target.kind === 'none' ? 0 : uploaded.length, target: targetLabel(target), files: uploaded, next: null }, json: Boolean(flags.json), quiet: flags.quiet }, raw: true };
117
+ }
118
+ const receipt = FileUploadReceiptSchema.parse({
119
+ operation: 'file-upload.upload',
120
+ uploaded: uploaded.length,
121
+ attached: target.kind === 'none' ? 0 : uploaded.length,
122
+ target: targetLabel(target),
123
+ files: uploaded,
124
+ next: target.kind === 'none' ? `escli notion file-upload attach ${uploaded.map((file) => file.file_upload_id).join(' ')} --attach ...` : nextForTarget(target, null),
125
+ });
126
+ return { data: { receipt, json: Boolean(flags.json), quiet: flags.quiet, unattachedWarning: target.kind === 'none' ? `unattached uploads expire unless attached: escli notion file-upload attach ${uploaded.map((file) => file.file_upload_id).join(' ')} --attach ...` : undefined }, raw: false };
127
+ }
128
+ }
129
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,78 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base-command.js';
3
+ import { writeStdout } from '../../../io/io.js';
4
+ import { proxy } from '../../../services/notion.js';
5
+ import { emitNotionFailure, emitNotionSuccess, emitRaw, fileUploadErrors, FileUploadListReceiptSchema, listResults, rawFlag, renderListReceipt, uploadState, } from '../../../lib/notion/upload/shared.js';
6
+ import { ErrorCode } from '@eightstate/contracts/errors';
7
+ import { ExitCodes } from '@eightstate/contracts/exit-codes';
8
+ import { EscliError } from '../../../lib/escli-error.js';
9
+ import { hasManifestFlag } from '../../../lib/notion/manifest-pass.js';
10
+ export { FileUploadListReceiptSchema as NotionUploadListDataSchema };
11
+ export default class NotionUploadList extends BaseCommand {
12
+ static errors = fileUploadErrors;
13
+ static summary = 'List recent Notion file_uploads';
14
+ static description = 'Emits one bounded recent page of bot-created Notion file uploads. This command is intentionally not paginated.';
15
+ static examples = ['<%= config.bin %> notion file-upload list', '<%= config.bin %> notion file-upload list --status uploaded --page-size 20'];
16
+ static aliases = ['notion upload list', 'notion file-upload ls'];
17
+ static flags = {
18
+ status: Flags.option({ description: 'Filter by upload status.', options: ['pending', 'uploaded', 'expired', 'failed'] })(),
19
+ 'created-after': Flags.string({ description: 'Filter to uploads created after an ISO-8601 timestamp.' }),
20
+ 'page-size': Flags.integer({ description: 'Bounded recent page size.', default: 20 }),
21
+ raw: rawFlag,
22
+ };
23
+ static args = {};
24
+ static enableJsonFlag = true;
25
+ static strict = true;
26
+ async run() {
27
+ if (hasManifestFlag(this.argv)) {
28
+ await super.run();
29
+ return;
30
+ }
31
+ const start = Date.now();
32
+ let json = false;
33
+ try {
34
+ const { data, raw } = await this.perform();
35
+ if (raw)
36
+ return;
37
+ json = data.json;
38
+ if (json)
39
+ await emitNotionSuccess(this.id ?? 'notion file-upload list', this.config.version, data.receipt, Date.now() - start);
40
+ else
41
+ await writeStdout(`${renderListReceipt(data.receipt)}\n`);
42
+ }
43
+ catch (error) {
44
+ await emitNotionFailure(this.id ?? 'notion file-upload list', this.config.version, error, Date.now() - start, json);
45
+ }
46
+ }
47
+ async execute() {
48
+ return (await this.perform()).data.receipt;
49
+ }
50
+ async perform() {
51
+ const { flags } = await this.parse(NotionUploadList);
52
+ if (flags['page-size'] < 1 || flags['page-size'] > 100)
53
+ throw new EscliError('--page-size must be between 1 and 100', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
54
+ const result = await proxy('GET', '/v1/file_uploads', { query: { status: flags.status, created_after: flags['created-after'], page_size: flags['page-size'] } });
55
+ if (flags.raw) {
56
+ await emitRaw(result.raw);
57
+ return { data: { receipt: buildReceipt(result.data, flags), json: Boolean(flags.json) }, raw: true };
58
+ }
59
+ const receipt = FileUploadListReceiptSchema.parse(buildReceipt(result.data, flags));
60
+ return { data: { receipt, json: Boolean(flags.json) }, raw: false };
61
+ }
62
+ }
63
+ function buildReceipt(raw, flags) {
64
+ const uploads = listResults(raw).map((upload) => ({ id: upload.id ?? '', name: upload.name ?? upload.filename, status: upload.status ?? 'unknown', expiry_time: upload.expiry_time, state: uploadState(upload) }));
65
+ return {
66
+ operation: 'file-upload.list',
67
+ count: uploads.length,
68
+ filter: { status: flags.status, created_after: flags['created-after'], page_size: flags['page-size'] },
69
+ counts_on_page: {
70
+ attach_now: uploads.filter((upload) => upload.state === 'attach_now').length,
71
+ persistent: uploads.filter((upload) => upload.state === 'persistent').length,
72
+ pending: uploads.filter((upload) => upload.state === 'pending').length,
73
+ dead: uploads.filter((upload) => upload.state === 'dead').length,
74
+ },
75
+ uploads,
76
+ };
77
+ }
78
+ //# sourceMappingURL=list.js.map
@@ -0,0 +1,76 @@
1
+ import { Args } from '@oclif/core';
2
+ import { hasManifestFlag } from '../../../lib/notion/manifest-pass.js';
3
+ import { BaseCommand } from '../../../base-command.js';
4
+ import { writeStdout } from '../../../io/io.js';
5
+ import { proxy } from '../../../services/notion.js';
6
+ import { emitNotionFailure, emitNotionSuccess, emitRaw, fileUploadErrors, FileUploadStatusReceiptSchema, normalizeUpload, rawFlag, renderStatusReceipt, uploadState, } from '../../../lib/notion/upload/shared.js';
7
+ export { FileUploadStatusReceiptSchema as NotionUploadStatusDataSchema };
8
+ export default class NotionUploadStatus extends BaseCommand {
9
+ static errors = fileUploadErrors;
10
+ static summary = 'Check Notion file_upload status';
11
+ static description = 'Checks whether supplied file_upload ids are attachable, persistent, pending, or dead.';
12
+ static examples = ['<%= config.bin %> notion file-upload status fu_9bb1 fu_4383', '<%= config.bin %> --json notion file-upload status fu_9bb1'];
13
+ static aliases = ['notion upload status'];
14
+ static flags = { raw: rawFlag };
15
+ static args = {
16
+ 'file-upload-id': Args.string({ description: 'Notion file_upload id(s).', required: true, multiple: true }),
17
+ };
18
+ static enableJsonFlag = true;
19
+ static strict = true;
20
+ async run() {
21
+ if (hasManifestFlag(this.argv)) {
22
+ await super.run();
23
+ return;
24
+ }
25
+ const start = Date.now();
26
+ let json = false;
27
+ try {
28
+ const { data, raw } = await this.perform();
29
+ if (raw)
30
+ return;
31
+ json = data.json;
32
+ if (json)
33
+ await emitNotionSuccess(this.id ?? 'notion file-upload status', this.config.version, data.receipt, Date.now() - start);
34
+ else
35
+ await writeStdout(`${renderStatusReceipt(data.receipt)}\n`);
36
+ }
37
+ catch (error) {
38
+ await emitNotionFailure(this.id ?? 'notion file-upload status', this.config.version, error, Date.now() - start, json);
39
+ }
40
+ }
41
+ async execute() {
42
+ return (await this.perform()).data.receipt;
43
+ }
44
+ async perform() {
45
+ const { args, flags } = await this.parse(NotionUploadStatus);
46
+ const ids = args['file-upload-id'];
47
+ const rawChain = [];
48
+ const uploads = [];
49
+ for (const id of ids) {
50
+ const result = await proxy('GET', `/v1/file_uploads/${encodeURIComponent(id)}`);
51
+ rawChain.push(result.raw);
52
+ const upload = normalizeUpload(result.data);
53
+ uploads.push({ id: upload.id ?? id, name: upload.name ?? upload.filename, status: upload.status ?? 'unknown', expiry_time: upload.expiry_time, state: uploadState(upload) });
54
+ }
55
+ if (flags.raw) {
56
+ await emitRaw(rawChain);
57
+ return { data: { receipt: buildReceipt(uploads), json: Boolean(flags.json) }, raw: true };
58
+ }
59
+ const receipt = FileUploadStatusReceiptSchema.parse(buildReceipt(uploads));
60
+ return { data: { receipt, json: Boolean(flags.json) }, raw: false };
61
+ }
62
+ }
63
+ function buildReceipt(uploads) {
64
+ return {
65
+ operation: 'file-upload.status',
66
+ checked: uploads.length,
67
+ states: {
68
+ attach_now: uploads.filter((upload) => upload.state === 'attach_now').length,
69
+ persistent: uploads.filter((upload) => upload.state === 'persistent').length,
70
+ pending: uploads.filter((upload) => upload.state === 'pending').length,
71
+ dead: uploads.filter((upload) => upload.state === 'dead').length,
72
+ },
73
+ uploads,
74
+ };
75
+ }
76
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1,78 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { hasManifestFlag } from '../../../lib/notion/manifest-pass.js';
3
+ import { BaseCommand } from '../../../base-command.js';
4
+ import { writeStdout } from '../../../io/io.js';
5
+ import { proxy } from '../../../services/notion.js';
6
+ import { dryRunData, emitRawOrReturn, maybeDryRunWarning, normalizeViewCreateResponse, notionCreateErrors, parseOptionalJsonInput, parsePosition, parseSortFlags, parseViewType, renderViewCreate, viewLocation, } from '../db/create.js';
7
+ export default class NotionViewCreate extends BaseCommand {
8
+ static errors = notionCreateErrors;
9
+ static summary = 'Create a Notion database view';
10
+ static description = 'Create one database view over an existing data source, placed in a database, dashboard, or linked page location.';
11
+ static examples = [
12
+ '<%= config.bin %> notion view create "Renewals this quarter" --data-source 74b1dd01-5e4d-4f64-a67e-e60ce75bb901 --in-db 362e7f31-7d53-4f6a-a0f6-7a7a7b68b0dc --type table --filter renewals-q.filter.json --sort Renewal:asc',
13
+ '<%= config.bin %> notion view create "Renewals this quarter" --data-source 74b1dd01-5e4d-4f64-a67e-e60ce75bb901 --in-db 362e7f31-7d53-4f6a-a0f6-7a7a7b68b0dc --type table --dry-run',
14
+ '<%= config.bin %> --json notion view create "Renewals this quarter" --data-source 74b1dd01-5e4d-4f64-a67e-e60ce75bb901 --in-db 362e7f31-7d53-4f6a-a0f6-7a7a7b68b0dc --type table',
15
+ ];
16
+ static aliases = ['notion views create', 'n view create', 'n views create'];
17
+ static flags = {
18
+ 'data-source': Flags.string({ description: 'Data source ID.', required: true }),
19
+ 'in-db': Flags.string({ description: 'Place as a top-level database tab.' }),
20
+ 'in-dashboard': Flags.string({ description: 'Place as a dashboard widget under this view ID.' }),
21
+ 'linked-on': Flags.string({ description: 'Place as a linked database on this page ID.' }),
22
+ type: Flags.string({ description: 'View type.', required: true, options: ['table', 'board', 'list', 'calendar', 'timeline', 'gallery', 'form', 'chart', 'map'] }),
23
+ filter: Flags.string({ description: 'Filter JSON path or inline JSON.' }),
24
+ sort: Flags.string({ description: 'Sort as <property:asc|property:desc>.', multiple: true }),
25
+ config: Flags.string({ description: 'Configuration JSON path or inline JSON.' }),
26
+ position: Flags.string({ description: 'View position: start, end, or after:<view_id>.' }),
27
+ raw: Flags.boolean({ description: 'Emit verbatim Notion response body.', default: false }),
28
+ 'dry-run': Flags.boolean({ description: 'Validate inputs and print the create payload without writing to Notion.', default: false }),
29
+ };
30
+ static args = {
31
+ name: Args.string({ description: 'View name.', required: true }),
32
+ };
33
+ static enableJsonFlag = true;
34
+ static strict = true;
35
+ async run() {
36
+ if (hasManifestFlag(this.argv)) {
37
+ await super.run();
38
+ return;
39
+ }
40
+ if (this.argv.includes('--raw')) {
41
+ await this.execute();
42
+ return;
43
+ }
44
+ await super.run();
45
+ }
46
+ async execute() {
47
+ const { args, flags } = await this.parse(NotionViewCreate);
48
+ const request = {
49
+ name: args.name,
50
+ data_source_id: flags['data-source'],
51
+ type: parseViewType(flags.type),
52
+ location: viewLocation(flags),
53
+ filter: await parseOptionalJsonInput(flags.filter, 'filter'),
54
+ sorts: parseSortFlags(flags.sort),
55
+ config: await parseOptionalJsonInput(flags.config, 'config'),
56
+ position: parsePosition(flags.position),
57
+ };
58
+ if (flags['dry-run']) {
59
+ await maybeDryRunWarning(flags, 'view create');
60
+ const data = { ...dryRunData('view', request), query_shape: { sorts: [] } };
61
+ if (!flags.json) {
62
+ await writeStdout(`${renderViewCreate(data)}\n`);
63
+ this.humanOutputHandled = true;
64
+ }
65
+ return data;
66
+ }
67
+ const result = await proxy('POST', '/v1/views', { body: request });
68
+ const payload = await emitRawOrReturn(result, flags.raw);
69
+ const data = normalizeViewCreateResponse(payload, request);
70
+ if (!flags.json && !flags.raw) {
71
+ await writeStdout(`${renderViewCreate(data)}\n`);
72
+ this.humanOutputHandled = true;
73
+ }
74
+ return { ...data, raw: result.raw };
75
+ }
76
+ }
77
+ export { renderViewCreate };
78
+ //# sourceMappingURL=create.js.map
@@ -0,0 +1,96 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { ErrorCode } from '@eightstate/contracts/errors';
3
+ import { BaseCommand } from '../../base-command.js';
4
+ import { renderKv } from '../../io/render-kv.js';
5
+ import { writeStdout } from '../../io/io.js';
6
+ import { notionCommentsList, notionFetchPage, proxy, whoami } from '../../services/notion.js';
7
+ import { hasManifestFlag } from '../../lib/notion/manifest-pass.js';
8
+ import { formatBot, formatWhoamiCapabilities, normalizeRecord, recordValue, runNotionCommand, stringValue, successEnvelope, validateVerify, } from './enroll.js';
9
+ const VERIFY_OPTIONS = ['identity', 'read', 'comments', 'users', 'all'];
10
+ export default class NotionWhoami extends BaseCommand {
11
+ static errors = [ErrorCode.UsageRequired, ErrorCode.UsageInvalid, ErrorCode.UsageUnknownFlag, ErrorCode.ValidationFailed, ErrorCode.AuthRequired, ErrorCode.NotionEnrollmentRequired, ErrorCode.NotionUnauthorized, ErrorCode.NotionRestricted, ErrorCode.NotionNotFound, ErrorCode.NotionRateLimited, ErrorCode.NotionValidation, ErrorCode.NotionConflict, ErrorCode.NotionTimeoutUncertain, ErrorCode.GateInvalidResponse, ErrorCode.NetworkError, ErrorCode.NetworkTimeout, ErrorCode.ServiceUnavailable, ErrorCode.ApiError, ErrorCode.Internal];
12
+ static summary = 'Show the enrolled Notion bot identity and readiness';
13
+ static description = 'Show which Notion bot/workspace this Clerk user is enrolled to use, plus non-mutating capability verification status.';
14
+ static examples = ['<%= config.bin %> notion whoami', '<%= config.bin %> notion whoami --probe-page 362e --verify all', '<%= config.bin %> --json notion whoami --verify identity'];
15
+ static aliases = [];
16
+ static flags = {
17
+ 'probe-page': Flags.string({ description: 'Page/block ID or URL used for non-mutating read/comment probes.' }),
18
+ verify: Flags.string({ description: 'Verification depth.', options: [...VERIFY_OPTIONS], default: 'identity' }),
19
+ raw: Flags.boolean({ description: 'Emit verbatim ordered Notion response body chain.', default: false }),
20
+ };
21
+ static args = {};
22
+ static enableJsonFlag = true;
23
+ static strict = true;
24
+ outputHandled = false;
25
+ async run() {
26
+ if (hasManifestFlag(this.argv)) {
27
+ await super.run();
28
+ return;
29
+ }
30
+ await runNotionCommand(this);
31
+ }
32
+ async execute() {
33
+ const { flags } = await this.parse(NotionWhoami);
34
+ validateVerify(flags.verify, flags['probe-page']);
35
+ const result = await whoami();
36
+ const data = normalizeRecord(result.data);
37
+ const rawChain = [result.raw];
38
+ await runRequestedProbes(flags.verify, flags['probe-page'], rawChain);
39
+ if (flags.raw) {
40
+ await writeStdout(`${JSON.stringify(rawChain)}\n`);
41
+ this.outputHandled = true;
42
+ return data;
43
+ }
44
+ if (flags.json) {
45
+ await writeStdout(`${JSON.stringify(successEnvelope(data))}\n`);
46
+ this.outputHandled = true;
47
+ return data;
48
+ }
49
+ await writeStdout(`${renderWhoami(data)}\n`);
50
+ this.outputHandled = true;
51
+ return data;
52
+ }
53
+ }
54
+ export function renderWhoami(data) {
55
+ const identity = recordValue(data.notion_identity) ?? recordValue(data.identity) ?? recordValue(data.bot) ?? data;
56
+ const clerkUser = recordValue(data.clerk_user);
57
+ const custody = recordValue(data.custody);
58
+ const capabilities = recordValue(data.capabilities);
59
+ const rows = [
60
+ ['notion identity', stringValue(data.status) ?? 'ready'],
61
+ ['bot', formatWhoamiBot(identity)],
62
+ ['attribution', stringValue(identity.attribution) === 'bot' || !stringValue(identity.attribution) ? `edits appear as bot "${stringValue(identity.name) ?? 'unknown'}", not the human user` : String(identity.attribution)],
63
+ ['capabilities', formatWhoamiCapabilities(capabilities)],
64
+ ['custody', formatWhoamiCustody(custody, clerkUser, data)],
65
+ ['next', stringValue(data.next) ?? 'use --probe-page <id> --verify all before write/comment work'],
66
+ ];
67
+ return renderKv(rows);
68
+ }
69
+ function formatWhoamiBot(identity) {
70
+ const workspace = stringValue(identity.workspace_name);
71
+ const bot = formatBot(identity);
72
+ return workspace ? `${bot} in ${workspace}` : bot;
73
+ }
74
+ async function runRequestedProbes(verify, probePage, rawChain) {
75
+ if ((verify === 'read' || verify === 'all') && probePage) {
76
+ const readResult = await notionFetchPage(probePage);
77
+ rawChain.push(readResult.raw);
78
+ }
79
+ if ((verify === 'comments' || verify === 'all') && probePage) {
80
+ const commentsResult = await notionCommentsList(probePage);
81
+ rawChain.push(commentsResult.raw);
82
+ }
83
+ if (verify === 'users' || verify === 'all') {
84
+ const usersResult = await proxy('GET', '/v1/users');
85
+ rawChain.push(usersResult.raw);
86
+ }
87
+ }
88
+ function formatWhoamiCustody(custody, clerkUser, data) {
89
+ const clerkId = stringValue(clerkUser?.id) ?? stringValue(data.clerk_user_id);
90
+ if (clerkId)
91
+ return `server-side token for clerk ${clerkId}`;
92
+ if (custody && custody.stored === false)
93
+ return 'not enrolled';
94
+ return 'server-side token stored';
95
+ }
96
+ //# sourceMappingURL=whoami.js.map
@@ -16,6 +16,7 @@ export const ResearchOutputSchema = z.object({
16
16
  export const ResearchDataSchema = z.union([
17
17
  z.object({ processors: z.array(ResearchProcessorSchema), count: z.number().int().nonnegative() }),
18
18
  z.object({ run_id: z.string(), processor: z.string(), output: ResearchOutputSchema, path: z.string().optional() }),
19
+ z.object({ run_id: z.string(), processor: z.string(), status: z.string() }),
19
20
  z.object({ run_id: z.string(), status: z.string(), error: z.unknown().optional(), warnings: z.array(z.unknown()).optional() }),
20
21
  z.object({ run_id: z.string(), output: ResearchOutputSchema, path: z.string().optional() }),
21
22
  ]);
@@ -49,6 +50,8 @@ export default class Research extends BaseCommand {
49
50
  metadata: Flags.string({ description: 'Metadata as JSON or key=val,key=val.' }),
50
51
  'follow-up': Flags.string({ description: 'Follow up on a previous run.' }),
51
52
  'no-basis': Flags.boolean({ description: 'Exclude citations and reasoning from human/file output.', default: false }),
53
+ 'max-wait': Flags.integer({ description: 'Max seconds to poll for completion (default: 7200, i.e. 2h). Per-request HTTP timeout is still --timeout.' }),
54
+ async: Flags.boolean({ description: 'Submit task and exit immediately with run_id; do not poll. Recover with --status / --result.', default: false }),
52
55
  };
53
56
  static args = {
54
57
  query: Args.string({ description: 'Research question or topic.', required: false, multiple: true }),
@@ -66,6 +69,12 @@ export default class Research extends BaseCommand {
66
69
  const query = args.query?.join(' ').trim() ?? '';
67
70
  if (!query)
68
71
  throw new EscliError('usage: escli research "query" -o output.md', { code: ErrorCode.UsageRequired, exitCode: ExitCodes.Usage });
72
+ if (flags.async && flags.output) {
73
+ throw new EscliError('--output requires a completed result; use `escli research --result <run_id> -o <path>` after the task completes.', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
74
+ }
75
+ if (typeof flags['max-wait'] === 'number' && flags['max-wait'] < 1) {
76
+ throw new EscliError('--max-wait must be >= 1; use --async to submit without polling.', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
77
+ }
69
78
  if (!flags.quiet && !flags.json)
70
79
  await writeStderr(` ▸ submitting task (processor=${flags.processor})...\n`);
71
80
  return runResearch({
@@ -86,6 +95,8 @@ export default class Research extends BaseCommand {
86
95
  noBasis: flags['no-basis'],
87
96
  quiet: flags.quiet || flags.json,
88
97
  timeoutSeconds: flags.timeout,
98
+ maxWaitSeconds: flags['max-wait'],
99
+ async: flags.async,
89
100
  });
90
101
  }
91
102
  }
package/dist/entry.js CHANGED
@@ -7,11 +7,12 @@ import { buildErrorEnvelope, buildUsageError } from './lib/envelope.js';
7
7
  import { GLOBAL_SHORT_FLAGS, globalFlagTakesValue, isGlobalFlagToken } from './lib/globals.js';
8
8
  import { commandRegistry, commandsById } from './lib/registry.js';
9
9
  import { writeStdout } from './io/io.js';
10
+ import packageJson from '../package.json' with { type: 'json' };
10
11
  const moduleDir = dirname(fileURLToPath(import.meta.url));
11
12
  const pjson = {
12
- name: '@eightstate/escli',
13
- version: '0.5.0',
14
- type: 'module',
13
+ name: packageJson.name,
14
+ version: packageJson.version,
15
+ type: packageJson.type,
15
16
  oclif: {
16
17
  bin: 'escli',
17
18
  enableJsonFlag: true,
@@ -41,13 +42,19 @@ const rootManifestRequested = manifestRequested && argv.length === 0;
41
42
  const firstCommandToken = argv.shift() ?? (manifestRequested || versionRequested ? 'version' : undefined);
42
43
  let commandId = firstCommandToken;
43
44
  let registration = commandId ? commandsById.get(commandId) : undefined;
45
+ // Multi-level command resolution: try longest match first (handles
46
+ // `notion page read`, `notion db row update`, etc. where intermediate
47
+ // prefixes like `notion page` aren't themselves registered).
44
48
  if (firstCommandToken && argv.length > 0) {
45
- const nestedCommandId = `${firstCommandToken} ${argv[0]}`;
46
- const nestedRegistration = commandsById.get(nestedCommandId);
47
- if (nestedRegistration) {
48
- commandId = nestedCommandId;
49
- registration = nestedRegistration;
50
- argv.shift();
49
+ for (let take = argv.length; take >= 1; take--) {
50
+ const candidateId = [firstCommandToken, ...argv.slice(0, take)].join(' ');
51
+ const candidateRegistration = commandsById.get(candidateId);
52
+ if (candidateRegistration) {
53
+ commandId = candidateId;
54
+ registration = candidateRegistration;
55
+ argv.splice(0, take);
56
+ break;
57
+ }
51
58
  }
52
59
  }
53
60
  if (argv.includes('--help') || argv.includes('-h'))