@eightstate/escli 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +12 -6
- 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 +1 -1
- 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,165 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { hasManifestFlag } from '../../../../lib/notion/manifest-pass.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { ErrorCode } from '@eightstate/contracts/errors';
|
|
5
|
+
import { ExitCodes } from '@eightstate/contracts/exit-codes';
|
|
6
|
+
import { BaseCommand } from '../../../../base-command.js';
|
|
7
|
+
import { EscliError } from '../../../../lib/escli-error.js';
|
|
8
|
+
import { writeStdout } from '../../../../io/io.js';
|
|
9
|
+
import { NOTION_VERSION, notionPagesPatchProps, proxy } from '../../../../services/notion.js';
|
|
10
|
+
import { dbWriteErrors, emitErrorEnvelope, emitSuccessEnvelope, normalizeUpdate, rawOutput, readIdsFileRows, readJsonObjectInput, renderUpdate, stripCommandFields, toEscliError, } from '../../../../lib/notion/db-row/common.js';
|
|
11
|
+
export const NotionDbRowUpdateDataSchema = z.union([
|
|
12
|
+
z.object({
|
|
13
|
+
action: z.literal('updated'),
|
|
14
|
+
page: z.object({ id: z.string(), title: z.string(), url: z.string() }),
|
|
15
|
+
changes: z.array(z.object({
|
|
16
|
+
property: z.string(),
|
|
17
|
+
type: z.string().optional(),
|
|
18
|
+
from: z.unknown(),
|
|
19
|
+
to: z.unknown(),
|
|
20
|
+
cleared: z.boolean().optional(),
|
|
21
|
+
})),
|
|
22
|
+
unchanged: z.array(z.string()),
|
|
23
|
+
last_edited_time: z.string(),
|
|
24
|
+
bot: z.string(),
|
|
25
|
+
}).passthrough(),
|
|
26
|
+
z.object({
|
|
27
|
+
action: z.literal('updated (batch)'),
|
|
28
|
+
rows_count: z.number().int(),
|
|
29
|
+
ok_count: z.number().int(),
|
|
30
|
+
err_count: z.number().int(),
|
|
31
|
+
rows: z.array(z.object({
|
|
32
|
+
index: z.number().int(),
|
|
33
|
+
id: z.string(),
|
|
34
|
+
status: z.enum(['ok', 'err']),
|
|
35
|
+
reason: z.string(),
|
|
36
|
+
}).passthrough()),
|
|
37
|
+
bot: z.string(),
|
|
38
|
+
}).passthrough(),
|
|
39
|
+
]);
|
|
40
|
+
export default class NotionDbRowUpdate extends BaseCommand {
|
|
41
|
+
static errors = dbWriteErrors;
|
|
42
|
+
static summary = 'Update Notion data-source row properties';
|
|
43
|
+
static description = 'Update typed properties on one Notion data-source row, or sequentially update rows from an NDJSON --ids-file batch.';
|
|
44
|
+
static examples = [
|
|
45
|
+
'<%= config.bin %> notion db row update 23f104cd-477e-8120-9a61-f2a2b73a9c11 --properties \'{"Status":{"status":{"name":"In Progress"}}}\'',
|
|
46
|
+
'<%= config.bin %> notion db row update --ids-file updates.ndjson',
|
|
47
|
+
'<%= config.bin %> notion db row update --ids-file - --raw',
|
|
48
|
+
];
|
|
49
|
+
static aliases = ['notion db update row', 'notion ds row update'];
|
|
50
|
+
static flags = {
|
|
51
|
+
properties: Flags.string({ description: 'Typed Notion properties as JSON, @file, or - for stdin.' }),
|
|
52
|
+
'ids-file': Flags.string({ description: 'NDJSON batch file path, or - for stdin.' }),
|
|
53
|
+
raw: Flags.boolean({ description: 'Emit verbatim Notion response body or ordered batch body chain.', default: false }),
|
|
54
|
+
};
|
|
55
|
+
static args = {
|
|
56
|
+
page_id: Args.string({ description: 'Target Notion page/row ID.', required: false }),
|
|
57
|
+
};
|
|
58
|
+
static enableJsonFlag = true;
|
|
59
|
+
static strict = true;
|
|
60
|
+
async run() {
|
|
61
|
+
if (hasManifestFlag(this.argv)) {
|
|
62
|
+
await super.run();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const start = Date.now();
|
|
66
|
+
let json = this.argv.includes('--json');
|
|
67
|
+
try {
|
|
68
|
+
const { args, flags } = await this.parse(NotionDbRowUpdate);
|
|
69
|
+
json = Boolean(flags.json);
|
|
70
|
+
if (flags['ids-file']) {
|
|
71
|
+
const batch = await this.executeBatch(flags['ids-file']);
|
|
72
|
+
if (batch.err_count > 0)
|
|
73
|
+
process.exitCode = ExitCodes.Error;
|
|
74
|
+
if (flags.raw) {
|
|
75
|
+
await writeStdout(`${JSON.stringify(batch.raw)}\n`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const cleanBatch = stripCommandFields(batch);
|
|
79
|
+
if (json)
|
|
80
|
+
await emitSuccessEnvelope(this.id ?? 'notion db row update', this.config.version, cleanBatch, Date.now() - start);
|
|
81
|
+
else
|
|
82
|
+
await writeStdout(`${renderUpdate(batch)}\n`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!args.page_id) {
|
|
86
|
+
throw new EscliError('page_id is required unless --ids-file is provided', { code: ErrorCode.UsageRequired, exitCode: ExitCodes.Usage });
|
|
87
|
+
}
|
|
88
|
+
const properties = await readJsonObjectInput(flags.properties, 'properties');
|
|
89
|
+
const data = await updateOne(args.page_id, properties);
|
|
90
|
+
if (flags.raw) {
|
|
91
|
+
await writeStdout(`${JSON.stringify(data.raw)}\n`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const cleanData = stripCommandFields(data);
|
|
95
|
+
if (json)
|
|
96
|
+
await emitSuccessEnvelope(this.id ?? 'notion db row update', this.config.version, cleanData, Date.now() - start);
|
|
97
|
+
else
|
|
98
|
+
await writeStdout(`${renderUpdate(data)}\n`);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
await emitErrorEnvelope(this.id ?? 'notion db row update', this.config.version, toEscliError(error), Date.now() - start, json);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async execute() {
|
|
105
|
+
const { args, flags } = await this.parse(NotionDbRowUpdate);
|
|
106
|
+
if (flags['ids-file']) {
|
|
107
|
+
const batch = await this.executeBatch(flags['ids-file']);
|
|
108
|
+
if (batch.err_count > 0)
|
|
109
|
+
process.exitCode = ExitCodes.Error;
|
|
110
|
+
return flags.raw ? batch.raw : stripCommandFields(batch);
|
|
111
|
+
}
|
|
112
|
+
if (!args.page_id)
|
|
113
|
+
throw new EscliError('page_id is required unless --ids-file is provided', { code: ErrorCode.UsageRequired, exitCode: ExitCodes.Usage });
|
|
114
|
+
const properties = await readJsonObjectInput(flags.properties, 'properties');
|
|
115
|
+
const data = await updateOne(args.page_id, properties);
|
|
116
|
+
return flags.raw ? data.raw : stripCommandFields(data);
|
|
117
|
+
}
|
|
118
|
+
async executeBatch(idsFile) {
|
|
119
|
+
const rows = await readIdsFileRows(idsFile);
|
|
120
|
+
const outcomes = [];
|
|
121
|
+
// Fail-fast on writes: stop at the first error so later rows are not
|
|
122
|
+
// mutated. Partial progress (rows processed before the failure) is
|
|
123
|
+
// returned in the outcomes array; exit code is set to 1 by the caller.
|
|
124
|
+
for (const [index, row] of rows.entries()) {
|
|
125
|
+
try {
|
|
126
|
+
const data = await updateOne(row.id, row.properties);
|
|
127
|
+
outcomes.push({ index: index + 1, id: row.id, status: 'ok', reason: '', data, raw: data.raw });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
outcomes.push({ index: index + 1, id: row.id, status: 'err', reason: errorReason(error), raw: errorRaw(error) });
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const errCount = outcomes.filter((row) => row.status === 'err').length;
|
|
135
|
+
const firstOk = outcomes.find((row) => row.status === 'ok');
|
|
136
|
+
const batch = {
|
|
137
|
+
action: 'updated (batch)',
|
|
138
|
+
rows_count: outcomes.length,
|
|
139
|
+
ok_count: outcomes.length - errCount,
|
|
140
|
+
err_count: errCount,
|
|
141
|
+
rows: outcomes,
|
|
142
|
+
bot: firstOk?.data?.bot ?? '',
|
|
143
|
+
raw: outcomes.map((row) => row.raw),
|
|
144
|
+
meta: { next: null, notion_version: NOTION_VERSION },
|
|
145
|
+
};
|
|
146
|
+
return batch;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function updateOne(pageId, properties) {
|
|
150
|
+
const before = await proxy('GET', `/v1/pages/${encodeURIComponent(pageId)}`);
|
|
151
|
+
const after = await notionPagesPatchProps(pageId, properties);
|
|
152
|
+
return normalizeUpdate(after, before.raw, properties);
|
|
153
|
+
}
|
|
154
|
+
function errorReason(error) {
|
|
155
|
+
if (error instanceof EscliError)
|
|
156
|
+
return `${error.causeCode ?? error.code} — ${error.message}`;
|
|
157
|
+
return error instanceof Error ? error.message : String(error);
|
|
158
|
+
}
|
|
159
|
+
function errorRaw(error) {
|
|
160
|
+
if (error instanceof EscliError)
|
|
161
|
+
return { error: { code: error.code, cause_code: error.causeCode, message: error.message, details: error.details } };
|
|
162
|
+
return rawOutput(error);
|
|
163
|
+
}
|
|
164
|
+
export { NotionDbRowUpdate };
|
|
165
|
+
//# sourceMappingURL=update.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
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, normalizeDsCreateResponse, normalizeSchemaFlagArgv, notionCreateErrors, parseSchemaInput, parseViewFileInput, parseViewFlags, renderDsCreate, } from '../db/create.js';
|
|
7
|
+
export default class NotionDsCreate extends BaseCommand {
|
|
8
|
+
static errors = notionCreateErrors;
|
|
9
|
+
static summary = 'Create a Notion data source under a database';
|
|
10
|
+
static description = 'Add another table/data source with its own schema under an existing Notion database, optionally requesting views over it.';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> notion ds create 362e7f31-7d53-4f6a-a0f6-7a7a7b68b0dc Accounts --schema accounts.schema.json --view table:"All accounts"',
|
|
13
|
+
'<%= config.bin %> notion ds create 362e7f31-7d53-4f6a-a0f6-7a7a7b68b0dc Accounts --schema accounts.schema.json --dry-run',
|
|
14
|
+
'<%= config.bin %> --json notion ds create 362e7f31-7d53-4f6a-a0f6-7a7a7b68b0dc Accounts --schema accounts.schema.json',
|
|
15
|
+
];
|
|
16
|
+
static aliases = ['notion data-source create', 'n ds create', 'n data-source create'];
|
|
17
|
+
static flags = {
|
|
18
|
+
'schema-input': Flags.string({ description: 'Schema JSON path, inline JSON, or - for stdin.', required: true, helpValue: 'schema' }),
|
|
19
|
+
view: Flags.string({ description: 'View as <type:name>.', multiple: true }),
|
|
20
|
+
'view-file': Flags.string({ description: 'JSON file containing richer view specs.' }),
|
|
21
|
+
raw: Flags.boolean({ description: 'Emit verbatim Notion response body.', default: false }),
|
|
22
|
+
'dry-run': Flags.boolean({ description: 'Validate inputs and print the create payload without writing to Notion.', default: false }),
|
|
23
|
+
};
|
|
24
|
+
static args = {
|
|
25
|
+
database_id: Args.string({ description: 'Database ID or URL.', required: true }),
|
|
26
|
+
name: Args.string({ description: 'Data source name.', required: true }),
|
|
27
|
+
};
|
|
28
|
+
static enableJsonFlag = true;
|
|
29
|
+
static strict = true;
|
|
30
|
+
async run() {
|
|
31
|
+
if (hasManifestFlag(this.argv)) {
|
|
32
|
+
await super.run();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
normalizeSchemaFlagArgv(this.argv);
|
|
36
|
+
if (this.argv.includes('--raw')) {
|
|
37
|
+
await this.execute();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
await super.run();
|
|
41
|
+
}
|
|
42
|
+
async execute() {
|
|
43
|
+
const { args, flags } = await this.parse(NotionDsCreate);
|
|
44
|
+
const typedFlags = flags;
|
|
45
|
+
const schema = await parseSchemaInput(typedFlags['schema-input']);
|
|
46
|
+
const views = [...parseViewFlags(typedFlags.view), ...await parseViewFileInput(typedFlags['view-file'])];
|
|
47
|
+
const request = {
|
|
48
|
+
parent: { database_id: args.database_id },
|
|
49
|
+
name: args.name,
|
|
50
|
+
schema,
|
|
51
|
+
views,
|
|
52
|
+
};
|
|
53
|
+
if (flags['dry-run']) {
|
|
54
|
+
await maybeDryRunWarning(flags, 'data source create');
|
|
55
|
+
const data = { ...dryRunData('data_source', request), database_id: args.database_id };
|
|
56
|
+
if (!typedFlags.json) {
|
|
57
|
+
await writeStdout(`${renderDsCreate(data)}\n`);
|
|
58
|
+
this.humanOutputHandled = true;
|
|
59
|
+
}
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
const result = await proxy('POST', '/v1/data_sources', { body: request });
|
|
63
|
+
const payload = await emitRawOrReturn(result, typedFlags.raw);
|
|
64
|
+
const data = normalizeDsCreateResponse(payload, args.database_id, args.name);
|
|
65
|
+
if (!typedFlags.json && !typedFlags.raw) {
|
|
66
|
+
await writeStdout(`${renderDsCreate(data)}\n`);
|
|
67
|
+
this.humanOutputHandled = true;
|
|
68
|
+
}
|
|
69
|
+
return { ...data, raw: result.raw };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export { renderDsCreate };
|
|
73
|
+
//# sourceMappingURL=create.js.map
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { Flags, Errors } from '@oclif/core';
|
|
2
|
+
import { ErrorCode } from '@eightstate/contracts/errors';
|
|
3
|
+
import { ExitCodes } from '@eightstate/contracts/exit-codes';
|
|
4
|
+
import { BaseCommand } from '../../base-command.js';
|
|
5
|
+
import { buildUsageError } from '../../lib/envelope.js';
|
|
6
|
+
import { EscliError } from '../../lib/escli-error.js';
|
|
7
|
+
import { renderKv } from '../../io/render-kv.js';
|
|
8
|
+
import { writeStderr, writeStdout } from '../../io/io.js';
|
|
9
|
+
import { enroll, NOTION_VERSION } from '../../services/notion.js';
|
|
10
|
+
import { hasManifestFlag } from '../../lib/notion/manifest-pass.js';
|
|
11
|
+
const VERIFY_OPTIONS = ['identity', 'read', 'comments', 'users', 'all'];
|
|
12
|
+
export default class NotionEnroll extends BaseCommand {
|
|
13
|
+
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];
|
|
14
|
+
static summary = "Store/replace this Clerk user's server-side Notion bot token";
|
|
15
|
+
static description = 'Validate a Notion internal-connection bot token and send it once to Eightstate custody. The token is never accepted as a CLI argument and is never printed.';
|
|
16
|
+
static examples = ['<%= config.bin %> notion enroll', 'printf %s "$NOTION_TOKEN" | <%= config.bin %> notion enroll --token-stdin', '<%= config.bin %> notion enroll --token-env NOTION_TOKEN --expect-workspace "Eight State"'];
|
|
17
|
+
static aliases = [];
|
|
18
|
+
static flags = {
|
|
19
|
+
'token-stdin': Flags.boolean({ description: 'Read the Notion token from stdin exactly once.' }),
|
|
20
|
+
'token-env': Flags.string({ description: 'Read the Notion token from the named environment variable.' }),
|
|
21
|
+
replace: Flags.boolean({ description: 'Replace an existing enrollment after confirming workspace/bot.', default: false }),
|
|
22
|
+
'expect-workspace': Flags.string({ description: 'Fail without storing if the verified workspace does not contain this text.' }),
|
|
23
|
+
'expect-bot': Flags.string({ description: 'Fail without storing if the verified bot name/id does not contain this text.' }),
|
|
24
|
+
label: Flags.string({ description: 'Human label stored beside the custody record.' }),
|
|
25
|
+
'probe-page': Flags.string({ description: 'Page/block ID or URL used for non-mutating read/comment probes.' }),
|
|
26
|
+
verify: Flags.string({ description: 'Verification depth.', options: [...VERIFY_OPTIONS], default: 'identity' }),
|
|
27
|
+
raw: Flags.boolean({ description: 'Emit verbatim ordered Notion response body chain.', default: false }),
|
|
28
|
+
};
|
|
29
|
+
static args = {};
|
|
30
|
+
static enableJsonFlag = true;
|
|
31
|
+
static strict = true;
|
|
32
|
+
outputHandled = false;
|
|
33
|
+
async run() {
|
|
34
|
+
if (hasManifestFlag(this.argv)) {
|
|
35
|
+
await super.run();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
await runNotionCommand(this);
|
|
39
|
+
}
|
|
40
|
+
async execute() {
|
|
41
|
+
const { flags } = await this.parse(NotionEnroll);
|
|
42
|
+
validateVerify(flags.verify, flags['probe-page']);
|
|
43
|
+
const token = await resolveEnrollmentToken({ tokenStdin: flags['token-stdin'], tokenEnv: flags['token-env'] });
|
|
44
|
+
const result = await enroll(token, { force: flags.replace === true });
|
|
45
|
+
const data = normalizeRecord(result.data);
|
|
46
|
+
if (flags.raw) {
|
|
47
|
+
await writeStdout(`${JSON.stringify(result.raw)}\n`);
|
|
48
|
+
this.outputHandled = true;
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
if (!flags.json) {
|
|
52
|
+
await writeStdout(`${renderEnroll(data)}\n`);
|
|
53
|
+
this.outputHandled = true;
|
|
54
|
+
}
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function runNotionCommand(command) {
|
|
59
|
+
const wantsJson = command.argv.includes('--json');
|
|
60
|
+
try {
|
|
61
|
+
const data = await command.execute();
|
|
62
|
+
if (command.outputHandled)
|
|
63
|
+
return;
|
|
64
|
+
if (wantsJson)
|
|
65
|
+
await writeStdout(`${JSON.stringify(successEnvelope(data))}\n`);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const escliError = toEscliError(error);
|
|
69
|
+
process.exitCode = escliError.exitCode;
|
|
70
|
+
if (wantsJson) {
|
|
71
|
+
await writeStdout(`${JSON.stringify(errorEnvelope(escliError))}\n`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
await writeStderr(`${escliError.message}\n`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function successEnvelope(data) {
|
|
78
|
+
return { ok: true, data, error: null, meta: notionEnvelopeMeta() };
|
|
79
|
+
}
|
|
80
|
+
export function errorEnvelope(error) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
data: null,
|
|
84
|
+
error: {
|
|
85
|
+
code: error.code,
|
|
86
|
+
message: error.message,
|
|
87
|
+
...(error.remediation === undefined ? {} : { remediation: error.remediation }),
|
|
88
|
+
...(error.details === undefined ? {} : { details: error.details }),
|
|
89
|
+
...(error.causeCode === undefined ? {} : { cause_code: error.causeCode }),
|
|
90
|
+
},
|
|
91
|
+
meta: notionEnvelopeMeta(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function notionEnvelopeMeta() {
|
|
95
|
+
return { next: null, notion_version: NOTION_VERSION };
|
|
96
|
+
}
|
|
97
|
+
export function renderEnroll(data) {
|
|
98
|
+
const enrollment = recordValue(data.enrollment) ?? data;
|
|
99
|
+
const identity = recordValue(data.notion_identity) ?? recordValue(data.identity) ?? recordValue(data.bot) ?? data;
|
|
100
|
+
const clerkUser = recordValue(data.clerk_user);
|
|
101
|
+
const capabilities = recordValue(data.capabilities);
|
|
102
|
+
const rows = [
|
|
103
|
+
['notion enrollment', stringValue(data.status) ?? 'ready'],
|
|
104
|
+
['clerk_user', stringValue(clerkUser?.id) ?? stringValue(data.clerk_user_id) ?? ''],
|
|
105
|
+
['bot', formatBot(identity)],
|
|
106
|
+
['workspace', stringValue(identity.workspace_name) ?? stringValue(data.workspace_name) ?? ''],
|
|
107
|
+
['verified', formatEnrollCapabilities(capabilities)],
|
|
108
|
+
['custody', formatEnrollCustody(enrollment)],
|
|
109
|
+
['next', stringValue(data.next) ?? 'whoami --probe-page <page_id> --verify read'],
|
|
110
|
+
];
|
|
111
|
+
return renderKv(rows);
|
|
112
|
+
}
|
|
113
|
+
export function validateVerify(verify, probePage) {
|
|
114
|
+
if ((verify === 'read' || verify === 'comments') && !probePage) {
|
|
115
|
+
throw buildUsageError(`--verify ${verify} requires --probe-page`, ErrorCode.UsageRequired);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function resolveEnrollmentToken(input) {
|
|
119
|
+
if (input.tokenStdin && input.tokenEnv)
|
|
120
|
+
throw buildUsageError('usage: choose only one token source: --token-stdin or --token-env NAME');
|
|
121
|
+
let token;
|
|
122
|
+
if (input.tokenEnv)
|
|
123
|
+
token = process.env[input.tokenEnv];
|
|
124
|
+
else if (input.tokenStdin)
|
|
125
|
+
token = await readStdinOnce();
|
|
126
|
+
else if (process.stdin.isTTY)
|
|
127
|
+
token = await promptForTokenAndOptionalGuards();
|
|
128
|
+
else
|
|
129
|
+
throw buildUsageError('usage: non-interactive enroll requires --token-stdin or --token-env NAME');
|
|
130
|
+
if (token === undefined)
|
|
131
|
+
throw buildUsageError('usage: non-interactive enroll requires --token-stdin or --token-env NAME');
|
|
132
|
+
const trimmed = token.trim();
|
|
133
|
+
if (!trimmed)
|
|
134
|
+
throw buildUsageError('usage: Notion token was empty');
|
|
135
|
+
return trimmed;
|
|
136
|
+
}
|
|
137
|
+
async function promptForTokenAndOptionalGuards() {
|
|
138
|
+
await writeStderr('Create/open a Notion internal connection, enable needed capabilities,\nshare target pages/databases with the connection, then paste its token.\nThe token is sent once to Eightstate custody and is never printed or stored locally.\n');
|
|
139
|
+
return promptLine('Notion internal connection token: ', true);
|
|
140
|
+
}
|
|
141
|
+
async function promptLine(label, hidden) {
|
|
142
|
+
await writeStderr(label);
|
|
143
|
+
const input = process.stdin;
|
|
144
|
+
const wasRaw = input.isRaw;
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
let value = '';
|
|
147
|
+
const cleanup = () => {
|
|
148
|
+
input.off('data', onData);
|
|
149
|
+
if (input.setRawMode)
|
|
150
|
+
input.setRawMode(Boolean(wasRaw));
|
|
151
|
+
input.pause();
|
|
152
|
+
};
|
|
153
|
+
const finish = () => {
|
|
154
|
+
cleanup();
|
|
155
|
+
void writeStderr('\n');
|
|
156
|
+
resolve(value);
|
|
157
|
+
};
|
|
158
|
+
const fail = (error) => {
|
|
159
|
+
cleanup();
|
|
160
|
+
void writeStderr('\n');
|
|
161
|
+
reject(error);
|
|
162
|
+
};
|
|
163
|
+
const onData = (chunk) => {
|
|
164
|
+
for (const character of chunk.toString('utf8')) {
|
|
165
|
+
if (character === '\u0003') {
|
|
166
|
+
fail(new EscliError('prompt cancelled', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage }));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (character === '\r' || character === '\n') {
|
|
170
|
+
finish();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (character === '\u007f' || character === '\b') {
|
|
174
|
+
value = value.slice(0, -1);
|
|
175
|
+
if (!hidden)
|
|
176
|
+
void writeStderr('\b \b');
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
value += character;
|
|
180
|
+
if (!hidden)
|
|
181
|
+
void writeStderr(character);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
if (input.setRawMode)
|
|
185
|
+
input.setRawMode(true);
|
|
186
|
+
input.on('data', onData);
|
|
187
|
+
input.resume();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function readStdinOnce() {
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
const chunks = [];
|
|
193
|
+
const onData = (chunk) => {
|
|
194
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
195
|
+
};
|
|
196
|
+
const onEnd = () => {
|
|
197
|
+
cleanup();
|
|
198
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
199
|
+
};
|
|
200
|
+
const onError = (error) => {
|
|
201
|
+
cleanup();
|
|
202
|
+
reject(new EscliError(`failed to read token from stdin: ${error.message}`, { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage }));
|
|
203
|
+
};
|
|
204
|
+
const cleanup = () => {
|
|
205
|
+
process.stdin.off('data', onData);
|
|
206
|
+
process.stdin.off('end', onEnd);
|
|
207
|
+
process.stdin.off('error', onError);
|
|
208
|
+
process.stdin.pause();
|
|
209
|
+
};
|
|
210
|
+
process.stdin.on('data', onData);
|
|
211
|
+
process.stdin.once('end', onEnd);
|
|
212
|
+
process.stdin.once('error', onError);
|
|
213
|
+
process.stdin.resume();
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function formatEnrollCapabilities(capabilities) {
|
|
217
|
+
return [
|
|
218
|
+
`identity ${capabilityWord(capabilities?.identity, 'verified')}`,
|
|
219
|
+
`read ${capabilityWord(capabilities?.read_content, 'unprobed')}`,
|
|
220
|
+
`comments ${capabilityWord(capabilities?.read_comments, 'unprobed')}`,
|
|
221
|
+
`users ${capabilityWord(capabilities?.user_info, 'limited')}`,
|
|
222
|
+
].join(' | ');
|
|
223
|
+
}
|
|
224
|
+
export function formatWhoamiCapabilities(capabilities) {
|
|
225
|
+
const parts = [
|
|
226
|
+
`identity ${capabilityWord(capabilities?.identity, 'verified')}`,
|
|
227
|
+
`read ${capabilityWord(capabilities?.read_content, 'unverified')}`,
|
|
228
|
+
];
|
|
229
|
+
const write = capabilities?.write_content ?? capabilities?.update_content ?? capabilities?.insert_content;
|
|
230
|
+
if (write !== undefined)
|
|
231
|
+
parts.push(`write ${capabilityWord(write, 'unverified')}`);
|
|
232
|
+
else
|
|
233
|
+
parts.push('write unverified');
|
|
234
|
+
parts.push(`comments ${capabilityWord(capabilities?.read_comments, 'unverified')}`);
|
|
235
|
+
parts.push(`users ${capabilityWord(capabilities?.user_info, 'limited')}`);
|
|
236
|
+
return parts.join(' | ');
|
|
237
|
+
}
|
|
238
|
+
function capabilityWord(value, fallback) {
|
|
239
|
+
const normalized = typeof value === 'object' && value !== null && !Array.isArray(value) ? stringValue(value.status) : stringValue(value);
|
|
240
|
+
if (!normalized)
|
|
241
|
+
return fallback;
|
|
242
|
+
if (normalized === 'verified')
|
|
243
|
+
return '✓';
|
|
244
|
+
return normalized.replaceAll('_', '-');
|
|
245
|
+
}
|
|
246
|
+
function formatEnrollCustody(enrollment) {
|
|
247
|
+
const storedAt = stringValue(enrollment.stored_at);
|
|
248
|
+
if (storedAt)
|
|
249
|
+
return `server-side token stored ${storedAt}; token never printed`;
|
|
250
|
+
return 'server-side token stored; token never printed';
|
|
251
|
+
}
|
|
252
|
+
export function formatBot(identity) {
|
|
253
|
+
const name = stringValue(identity.name) ?? stringValue(identity.bot_name) ?? 'unknown';
|
|
254
|
+
const type = stringValue(identity.type) ?? 'bot';
|
|
255
|
+
const id = stringValue(identity.user_id) ?? stringValue(identity.id) ?? stringValue(identity.bot_user_id) ?? '';
|
|
256
|
+
return id ? `${name} (${type} ${compactId(id)})` : `${name} (${type})`;
|
|
257
|
+
}
|
|
258
|
+
export function compactId(id) {
|
|
259
|
+
if (id.includes('…'))
|
|
260
|
+
return id;
|
|
261
|
+
const clean = id.replaceAll('-', '');
|
|
262
|
+
if (clean.length <= 12)
|
|
263
|
+
return id;
|
|
264
|
+
return `${clean.slice(0, 4)}…${clean.slice(-3)}`;
|
|
265
|
+
}
|
|
266
|
+
export function recordValue(value) {
|
|
267
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
268
|
+
}
|
|
269
|
+
export function normalizeRecord(value) {
|
|
270
|
+
return recordValue(value) ?? { value };
|
|
271
|
+
}
|
|
272
|
+
export function stringValue(value) {
|
|
273
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
274
|
+
}
|
|
275
|
+
function toEscliError(error) {
|
|
276
|
+
if (error instanceof EscliError) {
|
|
277
|
+
// Exit codes for notion.* are owned by contracts/failure-taxonomy; no
|
|
278
|
+
// surface-specific override here. NotionEnrollmentRequired → Auth and
|
|
279
|
+
// NotionConflict → Error come through automatically.
|
|
280
|
+
return error;
|
|
281
|
+
}
|
|
282
|
+
if (error instanceof Errors.ExitError)
|
|
283
|
+
return new EscliError(error.message, { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage, details: { oclif_exit: error.oclif?.exit } });
|
|
284
|
+
if (error instanceof Errors.CLIError)
|
|
285
|
+
return new EscliError(cleanOclifMessage(error.message), { code: classifyOclifUsageError(error), exitCode: ExitCodes.Usage, details: { oclif_exit: error.oclif?.exit } });
|
|
286
|
+
if (error instanceof Error)
|
|
287
|
+
return new EscliError(error.message, { code: ErrorCode.Internal });
|
|
288
|
+
return new EscliError('unknown error', { code: ErrorCode.Internal, details: error });
|
|
289
|
+
}
|
|
290
|
+
function classifyOclifUsageError(error) {
|
|
291
|
+
const name = error.constructor.name;
|
|
292
|
+
const message = error.message;
|
|
293
|
+
if (name === 'NonExistentFlagsError' || message.startsWith('Nonexistent flag') || message.startsWith('Unexpected flag'))
|
|
294
|
+
return ErrorCode.UsageUnknownFlag;
|
|
295
|
+
if (name === 'RequiredArgsError' || message.startsWith('Missing ') || message.includes('expects a value'))
|
|
296
|
+
return ErrorCode.UsageRequired;
|
|
297
|
+
return ErrorCode.UsageInvalid;
|
|
298
|
+
}
|
|
299
|
+
function cleanOclifMessage(message) {
|
|
300
|
+
return message.replace(/\nSee more help with --help$/u, '');
|
|
301
|
+
}
|
|
302
|
+
//# sourceMappingURL=enroll.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command.js';
|
|
2
|
+
import { buildUsageError } from '../../lib/envelope.js';
|
|
3
|
+
import { ErrorCode } from '@eightstate/contracts/errors';
|
|
4
|
+
export default class NotionTopic extends BaseCommand {
|
|
5
|
+
static errors = [ErrorCode.UsageInvalid];
|
|
6
|
+
static summary = 'Read and write Notion under a per-user AI bot identity';
|
|
7
|
+
static description = 'Token-lean by default; JSON opt-in via --json. Use notion enroll | whoami | page | db | search | comments | upload.';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> notion enroll',
|
|
10
|
+
'<%= config.bin %> notion whoami',
|
|
11
|
+
'<%= config.bin %> notion page read <id>',
|
|
12
|
+
'<%= config.bin %> notion db query <data-source-id>',
|
|
13
|
+
'<%= config.bin %> notion search "roadmap"',
|
|
14
|
+
];
|
|
15
|
+
static aliases = ['n'];
|
|
16
|
+
static flags = {};
|
|
17
|
+
static args = {};
|
|
18
|
+
static enableJsonFlag = true;
|
|
19
|
+
static strict = true;
|
|
20
|
+
async execute() {
|
|
21
|
+
throw buildUsageError('usage: escli notion <enroll|whoami|page|db|ds|view|search|comments|upload|block>');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
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, PageContentEditDataSchema, PageRefArg, buildEditBody, emitRaw, excerptMarkdown, normalizeMarkdownBody, operationStatuses, pageIdFromRef, parseEditOperations, renderEdit, withoutRaw, RawBypassCommand, } from '../../../lib/notion/page/content-common.js';
|
|
6
|
+
export { PageContentEditDataSchema };
|
|
7
|
+
export default class NotionPageEdit extends RawBypassCommand {
|
|
8
|
+
static errors = [...PAGE_CONTENT_ERROR_CODES, ErrorCode.UsageUnknownFlag];
|
|
9
|
+
static summary = 'Edit page body markdown';
|
|
10
|
+
static description = 'Apply targeted exact search-and-replace edits through Notion update_content.';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> notion page edit 362e1f4a-7d8c-81b0-9f6e-f2c9a7b630dc --old "Review command surfaces" --new "Review page-content output contract"',
|
|
13
|
+
'<%= config.bin %> notion page edit 362e1f4a-7d8c-81b0-9f6e-f2c9a7b630dc --old foo --new bar --replace-all',
|
|
14
|
+
'<%= config.bin %> notion page edit 362e1f4a-7d8c-81b0-9f6e-f2c9a7b630dc --ops ops.json --raw',
|
|
15
|
+
];
|
|
16
|
+
static aliases = [];
|
|
17
|
+
static flags = {
|
|
18
|
+
old: Flags.string({ description: 'Exact old string to replace.', multiple: true }),
|
|
19
|
+
new: Flags.string({ description: 'Replacement string.', multiple: true }),
|
|
20
|
+
ops: Flags.string({ description: 'JSON operations array or @file.' }),
|
|
21
|
+
'replace-all': Flags.boolean({ description: 'Replace all matches for --old/--new operations.', default: false }),
|
|
22
|
+
'allow-deleting-content': Flags.boolean({ description: 'Allow deletion of nested child pages/databases if Notion detects it.', default: false }),
|
|
23
|
+
raw: Flags.boolean({ description: 'Emit the verbatim Notion response body.', default: false }),
|
|
24
|
+
};
|
|
25
|
+
static args = {
|
|
26
|
+
page: Args.string(PageRefArg),
|
|
27
|
+
};
|
|
28
|
+
static enableJsonFlag = true;
|
|
29
|
+
static strict = true;
|
|
30
|
+
async execute() {
|
|
31
|
+
const { args, flags } = await this.parse(NotionPageEdit);
|
|
32
|
+
const pageId = pageIdFromRef(args.page);
|
|
33
|
+
const ops = parseEditOperations(flags);
|
|
34
|
+
const result = await notionPagePatchMarkdown(pageId, buildEditBody(ops, flags['allow-deleting-content']));
|
|
35
|
+
if (flags.raw) {
|
|
36
|
+
await emitRaw(result.raw);
|
|
37
|
+
this.humanOutputHandled = true;
|
|
38
|
+
return {
|
|
39
|
+
surface: 'page-content',
|
|
40
|
+
command: 'edit',
|
|
41
|
+
page: { id: pageId },
|
|
42
|
+
status: 'changed',
|
|
43
|
+
ops_requested: ops.length,
|
|
44
|
+
ops_applied: 0,
|
|
45
|
+
replace_all: flags['replace-all'],
|
|
46
|
+
allow_deleting_content: flags['allow-deleting-content'],
|
|
47
|
+
ops: ops.map((operation) => ({ old: operation.old_str, new: operation.new_str, status: 'submitted' })),
|
|
48
|
+
after_excerpt: '',
|
|
49
|
+
raw: result.raw,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const body = normalizeMarkdownBody(result, pageId);
|
|
53
|
+
const statuses = operationStatuses(ops, body.markdown);
|
|
54
|
+
const data = {
|
|
55
|
+
surface: 'page-content',
|
|
56
|
+
command: 'edit',
|
|
57
|
+
page: { id: body.id, title: body.title, url: body.url },
|
|
58
|
+
status: 'changed',
|
|
59
|
+
ops_requested: ops.length,
|
|
60
|
+
ops_applied: statuses.filter((operation) => operation.status === 'applied').length,
|
|
61
|
+
replace_all: flags['replace-all'],
|
|
62
|
+
allow_deleting_content: flags['allow-deleting-content'],
|
|
63
|
+
ops: statuses,
|
|
64
|
+
after_excerpt: excerptMarkdown(body.markdown),
|
|
65
|
+
};
|
|
66
|
+
if (flags.json)
|
|
67
|
+
return withoutRaw(data);
|
|
68
|
+
await writeStdout(`${renderEdit(data)}\n`);
|
|
69
|
+
this.humanOutputHandled = true;
|
|
70
|
+
return data;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=edit.js.map
|