@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,12 @@
1
+ export function renderKv(input) {
2
+ const entries = Array.isArray(input) ? input : Object.entries(input);
3
+ return entries.map(([key, value]) => `${key}: ${formatValue(value)}`).join('\n');
4
+ }
5
+ function formatValue(value) {
6
+ if (value === null || value === undefined)
7
+ return '';
8
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
9
+ return String(value);
10
+ return JSON.stringify(value);
11
+ }
12
+ //# sourceMappingURL=render-kv.js.map
@@ -0,0 +1,16 @@
1
+ import { renderKv } from './render-kv.js';
2
+ export function renderLabeled(records) {
3
+ return records.map(renderRecord).join('\n');
4
+ }
5
+ function renderRecord(record) {
6
+ const lines = [`--- ${record.label}`];
7
+ if (record.fields) {
8
+ const rendered = renderKv(record.fields);
9
+ if (rendered)
10
+ lines.push(rendered);
11
+ }
12
+ if (record.body)
13
+ lines.push(record.body);
14
+ return lines.join('\n');
15
+ }
16
+ //# sourceMappingURL=render-labeled.js.map
@@ -0,0 +1,19 @@
1
+ export function renderTable(input) {
2
+ const widths = input.header.map((heading, index) => Math.max(cellWidth(heading), ...input.rows.map((row) => cellWidth(formatCell(row[index])))));
3
+ const header = renderRow(input.header, widths);
4
+ const separator = renderRow(widths.map((width) => '-'.repeat(Math.max(3, width))), widths);
5
+ const body = input.rows.map((row) => renderRow(input.header.map((_, index) => formatCell(row[index])), widths));
6
+ return [header, separator, ...body].join('\n');
7
+ }
8
+ function renderRow(cells, widths) {
9
+ return `| ${cells.map((cell, index) => formatCell(cell).padEnd(widths[index] ?? 0)).join(' | ')} |`;
10
+ }
11
+ function formatCell(value) {
12
+ if (value === null || value === undefined)
13
+ return '';
14
+ return String(value).replace(/\r?\n/gu, ' ');
15
+ }
16
+ function cellWidth(value) {
17
+ return formatCell(value).length;
18
+ }
19
+ //# sourceMappingURL=render-table.js.map
@@ -0,0 +1,7 @@
1
+ export function renderTrailer(input) {
2
+ if (!input.next)
3
+ return '';
4
+ const prefix = typeof input.remaining === 'number' && Number.isFinite(input.remaining) && input.remaining > 0 ? `+${Math.trunc(input.remaining)} more` : 'more';
5
+ return `${prefix} — repeat with --next ${input.next}`;
6
+ }
7
+ //# sourceMappingURL=render-trailer.js.map
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { EnvelopeSchema } from '@eightstate/contracts/envelope';
3
3
  import { ManifestSchema } from '@eightstate/contracts/manifest';
4
4
  import { commandRegistry } from './registry.js';
5
+ import packageJson from '../../package.json' with { type: 'json' };
5
6
  function jsonSchema(schema) {
6
7
  return z.toJSONSchema(schema);
7
8
  }
@@ -25,7 +26,7 @@ export function buildManifest(compact = false, scope) {
25
26
  }),
26
27
  };
27
28
  }
28
- return ManifestSchema.parse({ version: '0.5.0', commands });
29
+ return ManifestSchema.parse({ version: packageJson.version, commands });
29
30
  }
30
31
  function isInScope(commandId, scope) {
31
32
  if (!scope)
@@ -63,5 +64,5 @@ function inferOptionType(name, value) {
63
64
  return 'integer';
64
65
  return 'string';
65
66
  }
66
- const integerFlags = new Set(['compression', 'speakers-expected', 'limit', 'tokens', 'page', 'max-results', 'days', 'timeout']);
67
+ const integerFlags = new Set(['compression', 'speakers-expected', 'limit', 'tokens', 'page', 'max-results', 'days', 'timeout', 'max-wait']);
67
68
  //# sourceMappingURL=manifest.js.map
@@ -0,0 +1,366 @@
1
+ import { Errors } from '@oclif/core';
2
+ import { z } from 'zod';
3
+ import { ErrorCode } from '@eightstate/contracts/errors';
4
+ import { ExitCodes } from '@eightstate/contracts/exit-codes';
5
+ import { BaseCommand } from '../../../base-command.js';
6
+ import { writeStderr, writeStdout } from '../../../io/io.js';
7
+ import { renderTrailer } from '../../../io/render-trailer.js';
8
+ import { EscliError } from '../../../lib/escli-error.js';
9
+ import { NOTION_VERSION } from '../../../services/notion.js';
10
+ import { hasManifestFlag } from '../manifest-pass.js';
11
+ export const notionCommentErrors = [
12
+ ErrorCode.UsageInvalid,
13
+ ErrorCode.UsageRequired,
14
+ ErrorCode.UsageUnknownFlag,
15
+ ErrorCode.NotionUnauthorized,
16
+ ErrorCode.NotionRestricted,
17
+ ErrorCode.NotionNotFound,
18
+ ErrorCode.NotionRateLimited,
19
+ ErrorCode.NotionValidation,
20
+ ErrorCode.NotionConflict,
21
+ ErrorCode.NotionTimeoutUncertain,
22
+ ErrorCode.NotionEnrollmentRequired,
23
+ ErrorCode.GateInvalidResponse,
24
+ ErrorCode.NetworkError,
25
+ ErrorCode.NetworkTimeout,
26
+ ErrorCode.ServiceUnavailable,
27
+ ErrorCode.ApiError,
28
+ ];
29
+ export class NotionCommentsCommand extends BaseCommand {
30
+ rawMode = false;
31
+ nextToken = null;
32
+ async run() {
33
+ if (hasManifestFlag(this.argv)) {
34
+ await super.run();
35
+ return;
36
+ }
37
+ try {
38
+ const flags = flagsFromArgv(this.argv);
39
+ const data = await this.execute();
40
+ if (this.rawMode && !flags.json) {
41
+ await writeStdout(`${JSON.stringify(data, null, 2)}\n`);
42
+ return;
43
+ }
44
+ if (flags.json) {
45
+ await writeStdout(`${JSON.stringify(successEnvelope(data, this.nextToken))}\n`);
46
+ return;
47
+ }
48
+ await writeStdout(`${this.render(data)}\n`);
49
+ }
50
+ catch (error) {
51
+ this.parsed = true;
52
+ const escliError = toEscliError(error);
53
+ process.exitCode = escliError.exitCode;
54
+ const flags = flagsFromArgv(this.argv);
55
+ if (flags.json) {
56
+ await writeStdout(`${JSON.stringify(errorEnvelope(escliError))}\n`);
57
+ return;
58
+ }
59
+ await writeStderr(`${escliError.message}\n`);
60
+ }
61
+ }
62
+ setRaw(raw) {
63
+ this.rawMode = true;
64
+ return raw;
65
+ }
66
+ setNext(next) {
67
+ this.nextToken = next ?? null;
68
+ }
69
+ }
70
+ export function successEnvelope(data, next) {
71
+ return { ok: true, data, error: null, meta: { next, notion_version: NOTION_VERSION } };
72
+ }
73
+ export function errorEnvelope(error) {
74
+ return {
75
+ ok: false,
76
+ data: null,
77
+ error: { code: error.code, message: error.message, details: error.details, remediation: error.remediation, cause_code: error.causeCode },
78
+ meta: { next: null, notion_version: NOTION_VERSION },
79
+ };
80
+ }
81
+ export function parseTarget(input) {
82
+ const urlTarget = parseUrlTarget(input);
83
+ if (urlTarget)
84
+ return urlTarget;
85
+ return { kind: inferTargetKind(input), id: normalizeId(input) };
86
+ }
87
+ export function formatTarget(target) {
88
+ if (!target)
89
+ return '';
90
+ return `${target.kind} ${target.id}${target.title ? ` · ${target.title}` : ''}`;
91
+ }
92
+ export function formatAuthor(comment) {
93
+ const author = comment.author ?? 'unknown';
94
+ return comment.author_kind && comment.author_kind !== 'unknown' ? `${author} (${comment.author_kind})` : author;
95
+ }
96
+ export function formatCreated(value, compact = false) {
97
+ if (!value)
98
+ return '';
99
+ const withoutMillis = value.replace(/\.000Z$/u, 'Z');
100
+ if (!compact)
101
+ return withoutMillis;
102
+ return withoutMillis.replace('T', ' ').replace(/:\d{2}Z$/u, 'Z');
103
+ }
104
+ export function renderContinuation(next, remaining) {
105
+ return renderTrailer({ next, remaining });
106
+ }
107
+ export function shapeList(result, target) {
108
+ const data = recordValue(result.data);
109
+ if (isBrokerListData(data))
110
+ return { ...data, next: result.meta.next ?? data.next ?? null };
111
+ const comments = arrayValue(data?.results).map(shapeComment);
112
+ const shapedTarget = targetFromData(data, target);
113
+ return {
114
+ target: shapedTarget,
115
+ comments,
116
+ summary: summarize(comments),
117
+ next: result.meta.next,
118
+ remaining: remainingCount(data, comments.length),
119
+ };
120
+ }
121
+ export function shapeGet(result) {
122
+ const data = recordValue(result.data);
123
+ if (isBrokerCommentData(data))
124
+ return data;
125
+ return shapeComment(data);
126
+ }
127
+ export function shapeThread(result, target, discussionId) {
128
+ const data = recordValue(result.data);
129
+ if (isBrokerThreadData(data))
130
+ return data;
131
+ const comments = arrayValue(data?.results).map(shapeComment).filter((comment) => comment.discussion_id === discussionId);
132
+ if (comments.length === 0) {
133
+ throw new EscliError(`Discussion not found or bot cannot access it: ${discussionId}`, {
134
+ code: ErrorCode.NotionNotFound,
135
+ exitCode: ExitCodes.NotFound,
136
+ details: { discussion_id: discussionId },
137
+ });
138
+ }
139
+ const last = comments.at(-1);
140
+ const botReplied = comments.some((comment) => comment.author_kind === 'bot');
141
+ return {
142
+ discussion_id: discussionId,
143
+ target: targetFromData(data, target),
144
+ comments,
145
+ summary: {
146
+ shown: comments.length,
147
+ bot_replied: botReplied,
148
+ last_author: last?.author_kind,
149
+ next_action: `escli notion comments reply ${discussionId} <markdown>`,
150
+ },
151
+ };
152
+ }
153
+ export function shapeReceipt(result, fallback) {
154
+ const data = recordValue(result.data);
155
+ if (isBrokerReceiptData(data))
156
+ return data;
157
+ const id = stringValue(data?.id) ?? '';
158
+ const body = plainText(data) || fallback.body;
159
+ const author = authorName(data);
160
+ const authorKind = authorKindValue(data);
161
+ const discussionId = stringValue(data?.discussion_id) ?? fallback.discussionId;
162
+ const partial = !discussionId && !data?.created_time && !data?.rich_text;
163
+ return {
164
+ created: id,
165
+ discussion_id: discussionId,
166
+ action: fallback.action,
167
+ where: fallback.where,
168
+ author,
169
+ author_kind: authorKind,
170
+ created_time: formatCreated(stringValue(data?.created_time)),
171
+ body,
172
+ warning: partial ? 'partial_comment_response' : undefined,
173
+ };
174
+ }
175
+ export function createBodyFromMarkdown(target, markdown) {
176
+ return { parent: target.kind === 'block' ? { block_id: target.id } : { page_id: target.id }, markdown };
177
+ }
178
+ export function replyBodyFromMarkdown(discussionId, markdown) {
179
+ return { discussion_id: discussionId, markdown };
180
+ }
181
+ export function joinMarkdown(value) {
182
+ const markdown = Array.isArray(value) ? value.join(' ') : value ?? '';
183
+ if (markdown.trim().length === 0) {
184
+ throw new EscliError('comments write requires a non-empty body.', { code: ErrorCode.UsageRequired, exitCode: ExitCodes.Usage });
185
+ }
186
+ return markdown;
187
+ }
188
+ export function kvEntries(input) {
189
+ return Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== '');
190
+ }
191
+ function shapeComment(value) {
192
+ const data = recordValue(value);
193
+ const parent = recordValue(data?.parent);
194
+ const where = whereFromParent(parent);
195
+ return {
196
+ id: stringValue(data?.id) ?? '',
197
+ discussion_id: stringValue(data?.discussion_id),
198
+ where,
199
+ block_id: stringValue(parent?.block_id),
200
+ author: authorName(data),
201
+ author_kind: authorKindValue(data),
202
+ created: formatCreated(stringValue(data?.created_time) ?? stringValue(data?.created)),
203
+ snippet: stringValue(data?.snippet) ?? snippet(plainText(data)),
204
+ body: stringValue(data?.body) ?? plainText(data),
205
+ };
206
+ }
207
+ function summarize(comments) {
208
+ return {
209
+ shown: comments.length,
210
+ threads: new Set(comments.map((comment) => comment.discussion_id).filter(Boolean)).size,
211
+ bot_comments: comments.filter((comment) => comment.author_kind === 'bot').length,
212
+ human_comments: comments.filter((comment) => comment.author_kind === 'person').length,
213
+ };
214
+ }
215
+ function targetFromData(data, fallback) {
216
+ const target = recordValue(data?.target);
217
+ if (target) {
218
+ return {
219
+ kind: target.kind === 'block' ? 'block' : 'page',
220
+ id: stringValue(target.id) ?? fallback.id,
221
+ title: stringValue(target.title),
222
+ };
223
+ }
224
+ const firstParent = recordValue(recordValue(arrayValue(data?.results)[0])?.parent);
225
+ const kind = firstParent?.type === 'block_id' || firstParent?.block_id ? 'block' : fallback.kind;
226
+ const id = stringValue(firstParent?.block_id) ?? stringValue(firstParent?.page_id) ?? fallback.id;
227
+ return { kind, id, title: fallback.title ?? inferTitle(data) };
228
+ }
229
+ function inferTitle(data) {
230
+ return stringValue(data?.title);
231
+ }
232
+ function whereFromParent(parent) {
233
+ if (!parent)
234
+ return undefined;
235
+ if (parent.type === 'page_id' && stringValue(parent.page_id))
236
+ return `page ${parent.page_id}`;
237
+ if (parent.type === 'block_id' && stringValue(parent.block_id))
238
+ return `block ${parent.block_id}`;
239
+ if (stringValue(parent.page_id))
240
+ return `page ${parent.page_id}`;
241
+ if (stringValue(parent.block_id))
242
+ return `block ${parent.block_id}`;
243
+ return undefined;
244
+ }
245
+ function plainText(data) {
246
+ const richText = arrayValue(data?.rich_text);
247
+ if (richText.length === 0)
248
+ return '';
249
+ return richText.map((item) => stringValue(recordValue(item)?.plain_text) ?? stringValue(recordValue(recordValue(item)?.text)?.content) ?? '').join('');
250
+ }
251
+ function snippet(value) {
252
+ const compact = value.replace(/\s+/gu, ' ').trim();
253
+ return compact.length > 140 ? `${compact.slice(0, 137)}…` : compact;
254
+ }
255
+ function authorName(data) {
256
+ return stringValue(data?.display_name) ?? stringValue(recordValue(data?.created_by)?.name) ?? stringValue(recordValue(data?.created_by)?.id);
257
+ }
258
+ function authorKindValue(data) {
259
+ const explicit = stringValue(data?.author_kind);
260
+ if (explicit === 'person' || explicit === 'bot')
261
+ return explicit;
262
+ const createdBy = recordValue(data?.created_by);
263
+ if (createdBy?.type === 'bot' || stringValue(createdBy?.id)?.startsWith('bot_'))
264
+ return 'bot';
265
+ if (createdBy?.type === 'person' || stringValue(createdBy?.id)?.startsWith('usr_'))
266
+ return 'person';
267
+ return undefined;
268
+ }
269
+ function remainingCount(data, shown) {
270
+ const remaining = numberValue(data?.remaining);
271
+ if (remaining !== undefined)
272
+ return remaining;
273
+ if (data?.has_more === true)
274
+ return 1;
275
+ const total = numberValue(data?.total);
276
+ return total !== undefined && total > shown ? total - shown : null;
277
+ }
278
+ function parseUrlTarget(input) {
279
+ try {
280
+ const url = new URL(input);
281
+ const hashId = firstNotionId(url.hash);
282
+ if (hashId)
283
+ return { kind: 'block', id: hashId };
284
+ const id = firstNotionId(`${url.pathname}${url.search}`);
285
+ return id ? { kind: 'page', id } : undefined;
286
+ }
287
+ catch {
288
+ return undefined;
289
+ }
290
+ }
291
+ function firstNotionId(value) {
292
+ const uuid = value.match(/[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}/iu)?.[0];
293
+ return uuid ? normalizeId(uuid) : undefined;
294
+ }
295
+ function normalizeId(value) {
296
+ const trimmed = value.trim();
297
+ const uuid = trimmed.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/iu)?.[0];
298
+ if (uuid)
299
+ return uuid.toLowerCase();
300
+ const compactUuid = trimmed.match(/[0-9a-f]{32}/iu)?.[0];
301
+ if (!compactUuid)
302
+ return trimmed;
303
+ return `${compactUuid.slice(0, 8)}-${compactUuid.slice(8, 12)}-${compactUuid.slice(12, 16)}-${compactUuid.slice(16, 20)}-${compactUuid.slice(20)}`.toLowerCase();
304
+ }
305
+ function inferTargetKind(input) {
306
+ return /(?:^|[-_])block(?:$|[-_])/iu.test(input) || /#[-\w]*[0-9a-f]{32}/iu.test(input) ? 'block' : 'page';
307
+ }
308
+ function flagsFromArgv(argv) {
309
+ return { json: argv.includes('--json') };
310
+ }
311
+ function toEscliError(error) {
312
+ if (error instanceof EscliError)
313
+ return error;
314
+ if (error instanceof z.ZodError) {
315
+ return new EscliError('input validation failed', { code: ErrorCode.ValidationFailed, exitCode: ExitCodes.Usage, details: error.issues });
316
+ }
317
+ if (error instanceof Errors.ExitError) {
318
+ return new EscliError(error.message, { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage, details: { oclif_exit: error.oclif?.exit } });
319
+ }
320
+ if (error instanceof Errors.CLIError) {
321
+ return new EscliError(cleanOclifMessage(error.message), { code: classifyOclifUsageError(error), exitCode: ExitCodes.Usage, details: { oclif_exit: error.oclif?.exit } });
322
+ }
323
+ if (error instanceof Error)
324
+ return new EscliError(error.message, { code: ErrorCode.Internal });
325
+ return new EscliError('unknown error', { code: ErrorCode.Unknown, details: error });
326
+ }
327
+ function classifyOclifUsageError(error) {
328
+ const name = error.constructor.name;
329
+ const message = error.message;
330
+ if (name === 'NonExistentFlagsError' || message.startsWith('Nonexistent flag') || message.startsWith('Unexpected flag'))
331
+ return ErrorCode.UsageUnknownFlag;
332
+ if (name === 'RequiredArgsError' || message.startsWith('Missing ') || message.includes('expects a value'))
333
+ return ErrorCode.UsageRequired;
334
+ if (message.includes('command ') && message.includes(' not found'))
335
+ return ErrorCode.UsageUnknownCommand;
336
+ return ErrorCode.UsageInvalid;
337
+ }
338
+ function cleanOclifMessage(message) {
339
+ return message.replace(/\nSee more help with --help$/u, '');
340
+ }
341
+ function isBrokerListData(data) {
342
+ return Boolean(data && recordValue(data.target) && Array.isArray(data.comments) && recordValue(data.summary));
343
+ }
344
+ function isBrokerCommentData(data) {
345
+ return Boolean(data && typeof data.id === 'string' && (typeof data.body === 'string' || typeof data.snippet === 'string') && !Array.isArray(data.results));
346
+ }
347
+ function isBrokerThreadData(data) {
348
+ return Boolean(data && typeof data.discussion_id === 'string' && Array.isArray(data.comments) && recordValue(data.summary));
349
+ }
350
+ function isBrokerReceiptData(data) {
351
+ return Boolean(data && typeof data.created === 'string' && typeof data.action === 'string');
352
+ }
353
+ function recordValue(value) {
354
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
355
+ }
356
+ function arrayValue(value) {
357
+ return Array.isArray(value) ? value : [];
358
+ }
359
+ function stringValue(value) {
360
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
361
+ }
362
+ function numberValue(value) {
363
+ const number = Number(value);
364
+ return Number.isFinite(number) ? number : undefined;
365
+ }
366
+ //# sourceMappingURL=shared.js.map