@goodtek/vibeops 0.2.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +444 -0
- package/dist/agent/loader.js +71 -0
- package/dist/agent/prompt.js +66 -0
- package/dist/bootstrap/installer.js +149 -0
- package/dist/bootstrap/manifest.js +15 -0
- package/dist/bootstrap/substitute.js +35 -0
- package/dist/cli.js +241 -0
- package/dist/commands/agent-list.js +32 -0
- package/dist/commands/agent-prompt.js +59 -0
- package/dist/commands/agent-show.js +26 -0
- package/dist/commands/github-init.js +554 -0
- package/dist/commands/github-status.js +164 -0
- package/dist/commands/init.js +179 -0
- package/dist/commands/notion-init.js +764 -0
- package/dist/commands/notion-sync.js +405 -0
- package/dist/commands/notion-test.js +595 -0
- package/dist/commands/plan.js +114 -0
- package/dist/commands/status.js +17 -0
- package/dist/commands/task-check.js +155 -0
- package/dist/commands/task-done.js +98 -0
- package/dist/commands/task-generate.js +206 -0
- package/dist/commands/task-pull.js +277 -0
- package/dist/commands/task-rollback.js +174 -0
- package/dist/commands/task-start.js +90 -0
- package/dist/lib/brief.js +349 -0
- package/dist/lib/config.js +158 -0
- package/dist/lib/filesystem.js +67 -0
- package/dist/lib/git.js +237 -0
- package/dist/lib/github-cli.js +247 -0
- package/dist/lib/inquirer-helpers.js +111 -0
- package/dist/lib/logger.js +42 -0
- package/dist/lib/notion-client.js +459 -0
- package/dist/lib/notion-discovery.js +671 -0
- package/dist/lib/notion-env.js +140 -0
- package/dist/lib/notion-mappers.js +148 -0
- package/dist/lib/notion-schema.js +272 -0
- package/dist/lib/notion-sync.js +337 -0
- package/dist/lib/notion-target.js +247 -0
- package/dist/lib/package-json.js +133 -0
- package/dist/lib/paths.js +26 -0
- package/dist/lib/project-docs.js +95 -0
- package/dist/lib/prompt-builder.js +125 -0
- package/dist/lib/task-generator.js +183 -0
- package/dist/lib/task-prompt.js +23 -0
- package/dist/lib/task-pull.js +354 -0
- package/dist/lib/task-scaffold.js +128 -0
- package/dist/lib/task-summary.js +276 -0
- package/dist/lib/task.js +364 -0
- package/dist/status/collector.js +103 -0
- package/dist/status/format.js +177 -0
- package/dist/types/brief.js +126 -0
- package/dist/types/config.js +17 -0
- package/dist/types/task.js +1 -0
- package/dist/version.js +8 -0
- package/package.json +61 -0
- package/templates/.cursor/rules/00-project-governance.mdc +28 -0
- package/templates/.cursor/rules/01-agent-orchestration.mdc +48 -0
- package/templates/.cursor/rules/02-task-workflow.mdc +38 -0
- package/templates/.cursor/rules/03-git-safety.mdc +30 -0
- package/templates/.cursor/rules/04-docs-update.mdc +22 -0
- package/templates/.vibeops/agents/architect.md +47 -0
- package/templates/.vibeops/agents/builder.md +38 -0
- package/templates/.vibeops/agents/docs.md +54 -0
- package/templates/.vibeops/agents/orchestrator.md +40 -0
- package/templates/.vibeops/agents/planner.md +60 -0
- package/templates/.vibeops/agents/recovery.md +49 -0
- package/templates/.vibeops/agents/reviewer.md +47 -0
- package/templates/.vibeops/agents/tester.md +43 -0
- package/templates/.vibeops/prompts/create-plan.md +33 -0
- package/templates/.vibeops/prompts/generate-tasks.md +41 -0
- package/templates/.vibeops/prompts/implement-task.md +39 -0
- package/templates/.vibeops/prompts/review-task.md +34 -0
- package/templates/.vibeops/prompts/rollback.md +32 -0
- package/templates/.vibeops/prompts/start-project.md +39 -0
- package/templates/.vibeops/workflows/notion-sync.md +53 -0
- package/templates/.vibeops/workflows/project-start.md +73 -0
- package/templates/.vibeops/workflows/rollback.md +45 -0
- package/templates/.vibeops/workflows/task-lifecycle.md +71 -0
- package/templates/AGENTS.md +98 -0
- package/templates/docs/logs/README.md +38 -0
- package/templates/docs/project/00-overview.md +27 -0
- package/templates/docs/project/01-requirements.md +30 -0
- package/templates/docs/project/02-mvp-scope.md +36 -0
- package/templates/docs/project/03-architecture.md +34 -0
- package/templates/docs/project/04-tech-stack.md +29 -0
- package/templates/docs/project/05-current-state.md +35 -0
- package/templates/docs/project/06-decisions.md +20 -0
- package/templates/docs/project/07-backlog.md +23 -0
- package/templates/docs/project/08-env.md +29 -0
- package/templates/docs/project/09-deployment.md +28 -0
- package/templates/docs/tasks/TASK-000-template.md +72 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin async wrapper around `@notionhq/client`.
|
|
3
|
+
*
|
|
4
|
+
* We lazy-import the SDK so that:
|
|
5
|
+
* - other commands (init / status / plan / task / …) don't pay the import
|
|
6
|
+
* cost when Notion is disabled,
|
|
7
|
+
* - a missing or broken install of `@notionhq/client` doesn't crash the
|
|
8
|
+
* entire CLI — only the notion subcommands.
|
|
9
|
+
*
|
|
10
|
+
* The wrapper exposes the surface VibeOps actually uses:
|
|
11
|
+
* - `users.me()` to validate the token, (TASK-010)
|
|
12
|
+
* - `databases.retrieve(id)` to verify access + schema, (TASK-010)
|
|
13
|
+
* - `dataSources.retrieve(id)` to read the actual schema (new API), (TASK-011 follow-up)
|
|
14
|
+
* - `databases.query(id, filter, page_size)` to look up a row, (TASK-011)
|
|
15
|
+
* - `pages.create({ parent, properties })` to insert, (TASK-011)
|
|
16
|
+
* - `pages.update({ page_id, properties })` to upsert in place, (TASK-011)
|
|
17
|
+
* - `search({ filter: { value: "data_source" }})` to discover DBs. (TASK-010 UX)
|
|
18
|
+
* - `blocks.children.list(block_id, page_size, start_cursor)` to walk
|
|
19
|
+
* the 1-depth children of a page when we need to find inline databases
|
|
20
|
+
* that Notion search doesn't surface. (TASK-010 UX)
|
|
21
|
+
*
|
|
22
|
+
* Network calls have a 5s timeout (per TASK-010 Risks).
|
|
23
|
+
*
|
|
24
|
+
* Notion API note (TASK-011 follow-up):
|
|
25
|
+
* In the current Notion API ("2025-09-03" and later), a `database` is a
|
|
26
|
+
* shell that can contain 0…N `data_source` children. Property schema lives
|
|
27
|
+
* on the `data_source`, not on the `database`. VibeOps therefore exposes
|
|
28
|
+
* `databasesRetrieve(id)` (returns `{ id, object, data_sources, ... }`)
|
|
29
|
+
* AND `dataSourcesRetrieve(id)` (returns `{ id, object, properties, ... }`)
|
|
30
|
+
* so the resolver (`notion-target.ts`) can fall back between them.
|
|
31
|
+
*/
|
|
32
|
+
const NOTION_API_TIMEOUT_MS = 5_000;
|
|
33
|
+
/**
|
|
34
|
+
* Notion API version VibeOps pins on every Client construction.
|
|
35
|
+
*
|
|
36
|
+
* `"2025-09-03"` is the first version in which `database.retrieve` returns
|
|
37
|
+
* `{ object: "database", data_sources: [...] }` and the property schema
|
|
38
|
+
* (`properties`) lives on `data_source` objects rather than on the
|
|
39
|
+
* `database` shell. VibeOps' resolver (`notion-target.ts`) targets that
|
|
40
|
+
* surface. Bumping this constant later requires re-validating the resolver
|
|
41
|
+
* + schema validator against the new response shape.
|
|
42
|
+
*/
|
|
43
|
+
export const NOTION_API_VERSION = "2025-09-03";
|
|
44
|
+
/**
|
|
45
|
+
* Defensive normaliser for the `data_sources` child array on a
|
|
46
|
+
* `databases.retrieve` response.
|
|
47
|
+
*
|
|
48
|
+
* Why this exists: Notion's official surface uses `data_sources`
|
|
49
|
+
* (snake_case), but VibeOps has seen / expects to support multiple naming
|
|
50
|
+
* shapes — `dataSources`, `child_data_sources`, `childDataSources` — as
|
|
51
|
+
* well as entries that wrap the id under `data_source.id` instead of `id`.
|
|
52
|
+
* This helper picks the first non-empty array it finds and normalises each
|
|
53
|
+
* entry to `{ id, name? }`. Returns `[]` when nothing usable is present.
|
|
54
|
+
*
|
|
55
|
+
* Tuple in the returned diagnostic: `[fieldName, items[]]` — `fieldName` is
|
|
56
|
+
* `null` when no array key was located, useful for `--debug-shape`.
|
|
57
|
+
*/
|
|
58
|
+
export function extractDataSourcesFromDatabaseResponse(response) {
|
|
59
|
+
if (response === null || typeof response !== "object") {
|
|
60
|
+
return { field: null, items: [] };
|
|
61
|
+
}
|
|
62
|
+
const obj = response;
|
|
63
|
+
// Order matters: prefer the canonical snake_case if both styles appear.
|
|
64
|
+
const candidates = [
|
|
65
|
+
"data_sources",
|
|
66
|
+
"dataSources",
|
|
67
|
+
"child_data_sources",
|
|
68
|
+
"childDataSources",
|
|
69
|
+
];
|
|
70
|
+
for (const key of candidates) {
|
|
71
|
+
const raw = obj[key];
|
|
72
|
+
if (!Array.isArray(raw))
|
|
73
|
+
continue;
|
|
74
|
+
if (raw.length === 0) {
|
|
75
|
+
// remember we saw the array even if empty (so the caller can show
|
|
76
|
+
// `field=data_sources, len=0` instead of `field=null, len=0`).
|
|
77
|
+
return { field: key, items: [] };
|
|
78
|
+
}
|
|
79
|
+
const items = raw
|
|
80
|
+
.map((entry) => {
|
|
81
|
+
if (entry === null || typeof entry !== "object")
|
|
82
|
+
return null;
|
|
83
|
+
const e = entry;
|
|
84
|
+
// Accept `id` OR `data_source.id` (some payloads nest the id).
|
|
85
|
+
const directId = typeof e.id === "string" ? e.id : "";
|
|
86
|
+
const nestedRaw = e.data_source !== undefined && typeof e.data_source === "object"
|
|
87
|
+
? e.data_source
|
|
88
|
+
: null;
|
|
89
|
+
const nestedId = nestedRaw !== null && typeof nestedRaw.id === "string"
|
|
90
|
+
? nestedRaw.id
|
|
91
|
+
: "";
|
|
92
|
+
const id = directId.length > 0 ? directId : nestedId;
|
|
93
|
+
if (id.length === 0)
|
|
94
|
+
return null;
|
|
95
|
+
// Pick the friendliest name: e.name, e.data_source.name, e.title text.
|
|
96
|
+
let name;
|
|
97
|
+
if (typeof e.name === "string" && e.name.length > 0) {
|
|
98
|
+
name = e.name;
|
|
99
|
+
}
|
|
100
|
+
else if (nestedRaw !== null &&
|
|
101
|
+
typeof nestedRaw.name === "string" &&
|
|
102
|
+
nestedRaw.name.length > 0) {
|
|
103
|
+
name = nestedRaw.name;
|
|
104
|
+
}
|
|
105
|
+
else if (Array.isArray(e.title)) {
|
|
106
|
+
const t = e.title
|
|
107
|
+
.map((seg) => seg.plain_text ?? "")
|
|
108
|
+
.join("")
|
|
109
|
+
.trim();
|
|
110
|
+
if (t.length > 0)
|
|
111
|
+
name = t;
|
|
112
|
+
}
|
|
113
|
+
return name === undefined ? { id } : { id, name };
|
|
114
|
+
})
|
|
115
|
+
.filter((x) => x !== null);
|
|
116
|
+
return { field: key, items };
|
|
117
|
+
}
|
|
118
|
+
return { field: null, items: [] };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Build a token-safe {@link DatabaseShapeProbe} from a raw retrieve
|
|
122
|
+
* response. Splits out from `probeDatabaseShape` so unit tests can hit it
|
|
123
|
+
* directly without an SDK client.
|
|
124
|
+
*/
|
|
125
|
+
export function summariseDatabaseShape(inputId, raw) {
|
|
126
|
+
if (raw === null || typeof raw !== "object") {
|
|
127
|
+
return {
|
|
128
|
+
inputId,
|
|
129
|
+
object: "(unknown)",
|
|
130
|
+
id: inputId,
|
|
131
|
+
hasProperties: false,
|
|
132
|
+
propertiesKeysLength: 0,
|
|
133
|
+
hasDataSources: false,
|
|
134
|
+
dataSourcesLength: 0,
|
|
135
|
+
dataSources: [],
|
|
136
|
+
topLevelKeys: [],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const obj = raw;
|
|
140
|
+
const object = typeof obj.object === "string" ? obj.object : "(unknown)";
|
|
141
|
+
const id = typeof obj.id === "string" && obj.id.length > 0 ? obj.id : inputId;
|
|
142
|
+
let title;
|
|
143
|
+
if (Array.isArray(obj.title)) {
|
|
144
|
+
const text = obj.title
|
|
145
|
+
.map((seg) => seg.plain_text ?? "")
|
|
146
|
+
.join("")
|
|
147
|
+
.trim();
|
|
148
|
+
if (text.length > 0)
|
|
149
|
+
title = text;
|
|
150
|
+
}
|
|
151
|
+
else if (typeof obj.title === "string" && obj.title.length > 0) {
|
|
152
|
+
title = obj.title;
|
|
153
|
+
}
|
|
154
|
+
else if (typeof obj.name === "string" && obj.name.length > 0) {
|
|
155
|
+
title = obj.name;
|
|
156
|
+
}
|
|
157
|
+
const props = obj.properties;
|
|
158
|
+
const hasProperties = props !== null && props !== undefined && typeof props === "object";
|
|
159
|
+
const propertiesKeysLength = hasProperties
|
|
160
|
+
? Object.keys(props).length
|
|
161
|
+
: 0;
|
|
162
|
+
const ds = extractDataSourcesFromDatabaseResponse(raw);
|
|
163
|
+
const topLevelKeys = Object.keys(obj).sort();
|
|
164
|
+
return {
|
|
165
|
+
inputId,
|
|
166
|
+
object,
|
|
167
|
+
id,
|
|
168
|
+
...(title !== undefined ? { title } : {}),
|
|
169
|
+
hasProperties,
|
|
170
|
+
propertiesKeysLength,
|
|
171
|
+
hasDataSources: ds.field !== null,
|
|
172
|
+
...(ds.field !== null ? { dataSourcesField: ds.field } : {}),
|
|
173
|
+
dataSourcesLength: ds.items.length,
|
|
174
|
+
dataSources: ds.items,
|
|
175
|
+
topLevelKeys,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export function notionApiError(err) {
|
|
179
|
+
const e = err;
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
code: typeof e.code === "string" ? e.code : "unknown_error",
|
|
183
|
+
...(typeof e.status === "number" ? { status: e.status } : {}),
|
|
184
|
+
message: typeof e.message === "string" ? e.message : String(err),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
export async function createNotionClient(token) {
|
|
188
|
+
// Use a dynamic specifier so TS/Node import this lazily even under
|
|
189
|
+
// `--noEmitOnError`. Missing dep → caller catches and surfaces a friendly
|
|
190
|
+
// "install @notionhq/client" message.
|
|
191
|
+
const modSpecifier = "@notionhq/client";
|
|
192
|
+
const mod = await import(/* @vite-ignore */ modSpecifier);
|
|
193
|
+
const ClientCtor = mod.Client ??
|
|
194
|
+
mod.default?.Client;
|
|
195
|
+
if (typeof ClientCtor !== "function") {
|
|
196
|
+
throw new Error("Could not find `Client` export in `@notionhq/client`. Re-install the dependency: `pnpm add @notionhq/client`.");
|
|
197
|
+
}
|
|
198
|
+
// `notionVersion: "2025-09-03"` pins the Notion API version VibeOps was
|
|
199
|
+
// designed against. In this revision the schema (`properties`) lives on
|
|
200
|
+
// `data_source` objects rather than on the `database` shell, so this pin
|
|
201
|
+
// makes the response shape deterministic regardless of what default the
|
|
202
|
+
// installed `@notionhq/client` build chose.
|
|
203
|
+
//
|
|
204
|
+
// `logLevel: "error"` suppresses the SDK's WARN-level "request fail"
|
|
205
|
+
// chatter that otherwise polutes stderr on expected 4xx (e.g. when the
|
|
206
|
+
// resolver intentionally probes `dataSources.retrieve(id)` and gets a 404
|
|
207
|
+
// before falling back to `databases.retrieve(id)`). Real errors still
|
|
208
|
+
// throw; this only silences the SDK's diagnostic console.warn.
|
|
209
|
+
const client = new ClientCtor({
|
|
210
|
+
auth: token,
|
|
211
|
+
timeoutMs: NOTION_API_TIMEOUT_MS,
|
|
212
|
+
logLevel: "error",
|
|
213
|
+
notionVersion: NOTION_API_VERSION,
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
async usersMe() {
|
|
217
|
+
const res = (await client.users.me({}));
|
|
218
|
+
return res;
|
|
219
|
+
},
|
|
220
|
+
async databasesRetrieve(databaseId) {
|
|
221
|
+
const res = (await client.databases.retrieve({
|
|
222
|
+
database_id: databaseId,
|
|
223
|
+
}));
|
|
224
|
+
return res;
|
|
225
|
+
},
|
|
226
|
+
async retrieveDatabase(databaseId) {
|
|
227
|
+
const res = (await client.databases.retrieve({
|
|
228
|
+
database_id: databaseId,
|
|
229
|
+
}));
|
|
230
|
+
return res;
|
|
231
|
+
},
|
|
232
|
+
async dataSourcesRetrieve(dataSourceId) {
|
|
233
|
+
// (A) preferred — typed SDK call when present.
|
|
234
|
+
if (client.dataSources !== undefined &&
|
|
235
|
+
typeof client.dataSources.retrieve === "function") {
|
|
236
|
+
const res = (await client.dataSources.retrieve({
|
|
237
|
+
data_source_id: dataSourceId,
|
|
238
|
+
}));
|
|
239
|
+
return res;
|
|
240
|
+
}
|
|
241
|
+
// (B) raw HTTP fallback via SDK's public arbitrary-request helper.
|
|
242
|
+
// The SDK already attaches Authorization / Notion-Version / JSON
|
|
243
|
+
// headers, so we never touch the token from here.
|
|
244
|
+
if (typeof client.request === "function") {
|
|
245
|
+
const res = (await client.request({
|
|
246
|
+
path: `data_sources/${dataSourceId}`,
|
|
247
|
+
method: "GET",
|
|
248
|
+
}));
|
|
249
|
+
return res;
|
|
250
|
+
}
|
|
251
|
+
// (C) neither path available — let the resolver decide what to do.
|
|
252
|
+
return null;
|
|
253
|
+
},
|
|
254
|
+
async retrieveDataSource(dataSourceId) {
|
|
255
|
+
if (client.dataSources !== undefined &&
|
|
256
|
+
typeof client.dataSources.retrieve === "function") {
|
|
257
|
+
const res = (await client.dataSources.retrieve({
|
|
258
|
+
data_source_id: dataSourceId,
|
|
259
|
+
}));
|
|
260
|
+
return res;
|
|
261
|
+
}
|
|
262
|
+
if (typeof client.request === "function") {
|
|
263
|
+
const res = (await client.request({
|
|
264
|
+
path: `data_sources/${dataSourceId}`,
|
|
265
|
+
method: "GET",
|
|
266
|
+
}));
|
|
267
|
+
return res;
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
},
|
|
271
|
+
async probeDatabaseShape(databaseId) {
|
|
272
|
+
const raw = (await client.databases.retrieve({
|
|
273
|
+
database_id: databaseId,
|
|
274
|
+
}));
|
|
275
|
+
return summariseDatabaseShape(databaseId, raw);
|
|
276
|
+
},
|
|
277
|
+
async databasesQuery(databaseId, options) {
|
|
278
|
+
const q = {};
|
|
279
|
+
if (options?.filter !== undefined)
|
|
280
|
+
q.filter = options.filter;
|
|
281
|
+
if (typeof options?.pageSize === "number")
|
|
282
|
+
q.page_size = options.pageSize;
|
|
283
|
+
if (typeof options?.startCursor === "string")
|
|
284
|
+
q.start_cursor = options.startCursor;
|
|
285
|
+
const res = (await (client.dataSources?.query !== undefined
|
|
286
|
+
? client.dataSources.query({ ...q, data_source_id: databaseId })
|
|
287
|
+
: client.databases.query({ ...q, database_id: databaseId })));
|
|
288
|
+
return {
|
|
289
|
+
results: res.results ?? [],
|
|
290
|
+
hasMore: res.has_more === true,
|
|
291
|
+
nextCursor: res.next_cursor ?? null,
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
async pagesCreate({ databaseId, properties }) {
|
|
295
|
+
let res;
|
|
296
|
+
try {
|
|
297
|
+
res = (await client.pages.create({
|
|
298
|
+
parent: { data_source_id: databaseId },
|
|
299
|
+
properties,
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
const apiErr = notionApiError(err);
|
|
304
|
+
if (apiErr.code !== "validation_error" && apiErr.code !== "object_not_found") {
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
// Legacy fallback for older configs that still store database ids.
|
|
308
|
+
res = (await client.pages.create({
|
|
309
|
+
parent: { database_id: databaseId },
|
|
310
|
+
properties,
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
return { id: res.id };
|
|
314
|
+
},
|
|
315
|
+
async queryDataSource(dataSourceId, options) {
|
|
316
|
+
const body = {};
|
|
317
|
+
if (options?.filter !== undefined)
|
|
318
|
+
body.filter = options.filter;
|
|
319
|
+
if (typeof options?.pageSize === "number")
|
|
320
|
+
body.page_size = options.pageSize;
|
|
321
|
+
if (typeof options?.startCursor === "string")
|
|
322
|
+
body.start_cursor = options.startCursor;
|
|
323
|
+
let raw;
|
|
324
|
+
// (A) typed SDK call when the installed build exposes `dataSources.query`.
|
|
325
|
+
if (client.dataSources !== undefined &&
|
|
326
|
+
typeof client.dataSources.query === "function") {
|
|
327
|
+
raw = (await client.dataSources.query({
|
|
328
|
+
data_source_id: dataSourceId,
|
|
329
|
+
...body,
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
else if (typeof client.request === "function") {
|
|
333
|
+
// (B) raw HTTP fallback — SDK attaches Authorization / Notion-Version /
|
|
334
|
+
// Content-Type headers itself; we never touch the token from here.
|
|
335
|
+
raw = (await client.request({
|
|
336
|
+
path: `data_sources/${dataSourceId}/query`,
|
|
337
|
+
method: "POST",
|
|
338
|
+
body,
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
throw new Error("`@notionhq/client` does not expose `dataSources.query` nor `client.request`; cannot query data source.");
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
results: Array.isArray(raw.results) ? raw.results : [],
|
|
346
|
+
hasMore: raw.has_more === true,
|
|
347
|
+
nextCursor: raw.next_cursor ?? null,
|
|
348
|
+
};
|
|
349
|
+
},
|
|
350
|
+
async createPageInDataSource({ dataSourceId, properties }) {
|
|
351
|
+
const body = {
|
|
352
|
+
parent: { type: "data_source_id", data_source_id: dataSourceId },
|
|
353
|
+
properties,
|
|
354
|
+
};
|
|
355
|
+
// (A) typed SDK call first — `parent` is typed as `unknown` in our cast
|
|
356
|
+
// so the new shape passes through transparently.
|
|
357
|
+
try {
|
|
358
|
+
const res = (await client.pages.create(body));
|
|
359
|
+
return { id: res.id };
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
const apiErr = notionApiError(err);
|
|
363
|
+
// Only fall back when the SDK itself rejected the shape (validation
|
|
364
|
+
// error on the SDK side). 401/403/404/429 must propagate so the
|
|
365
|
+
// caller can show the precise reason.
|
|
366
|
+
if (apiErr.code !== "validation_error") {
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
if (typeof client.request !== "function")
|
|
370
|
+
throw err;
|
|
371
|
+
const res = (await client.request({
|
|
372
|
+
path: "pages",
|
|
373
|
+
method: "POST",
|
|
374
|
+
body,
|
|
375
|
+
}));
|
|
376
|
+
return { id: res.id };
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
async pagesUpdate({ pageId, properties }) {
|
|
380
|
+
const res = (await client.pages.update({
|
|
381
|
+
page_id: pageId,
|
|
382
|
+
properties,
|
|
383
|
+
}));
|
|
384
|
+
return { id: res.id };
|
|
385
|
+
},
|
|
386
|
+
async updatePage({ pageId, properties }) {
|
|
387
|
+
const res = (await client.pages.update({
|
|
388
|
+
page_id: pageId,
|
|
389
|
+
properties,
|
|
390
|
+
}));
|
|
391
|
+
return { id: res.id };
|
|
392
|
+
},
|
|
393
|
+
async blocksChildrenList({ blockId, pageSize, startCursor }) {
|
|
394
|
+
const q = {
|
|
395
|
+
block_id: blockId,
|
|
396
|
+
};
|
|
397
|
+
// Notion API hard-caps page_size at 100; we keep ≤ 50 for snappy UX
|
|
398
|
+
// unless a caller asks for more, but never above 100.
|
|
399
|
+
const ps = typeof pageSize === "number" ? Math.min(100, Math.max(1, pageSize)) : 50;
|
|
400
|
+
q.page_size = ps;
|
|
401
|
+
if (typeof startCursor === "string")
|
|
402
|
+
q.start_cursor = startCursor;
|
|
403
|
+
const res = (await client.blocks.children.list(q));
|
|
404
|
+
return {
|
|
405
|
+
results: Array.isArray(res.results) ? res.results : [],
|
|
406
|
+
hasMore: res.has_more === true,
|
|
407
|
+
nextCursor: res.next_cursor ?? null,
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
async listBlockChildren(blockId, options) {
|
|
411
|
+
const q = {
|
|
412
|
+
block_id: blockId,
|
|
413
|
+
};
|
|
414
|
+
q.page_size =
|
|
415
|
+
typeof options?.limit === "number"
|
|
416
|
+
? Math.min(100, Math.max(1, options.limit))
|
|
417
|
+
: 50;
|
|
418
|
+
if (typeof options?.startCursor === "string") {
|
|
419
|
+
q.start_cursor = options.startCursor;
|
|
420
|
+
}
|
|
421
|
+
const res = (await client.blocks.children.list(q));
|
|
422
|
+
return {
|
|
423
|
+
results: Array.isArray(res.results) ? res.results : [],
|
|
424
|
+
hasMore: res.has_more === true,
|
|
425
|
+
nextCursor: res.next_cursor ?? null,
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
async search(options) {
|
|
429
|
+
const q = {};
|
|
430
|
+
if (typeof options?.query === "string")
|
|
431
|
+
q.query = options.query;
|
|
432
|
+
if (options?.objectFilter !== undefined)
|
|
433
|
+
q.filter = { property: "object", value: options.objectFilter };
|
|
434
|
+
q.page_size = typeof options?.pageSize === "number" ? options.pageSize : 50;
|
|
435
|
+
if (typeof options?.startCursor === "string")
|
|
436
|
+
q.start_cursor = options.startCursor;
|
|
437
|
+
const res = (await client.search(q));
|
|
438
|
+
return {
|
|
439
|
+
results: Array.isArray(res.results) ? res.results : [],
|
|
440
|
+
hasMore: res.has_more === true,
|
|
441
|
+
nextCursor: res.next_cursor ?? null,
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
async searchPages(query) {
|
|
445
|
+
const q = {
|
|
446
|
+
filter: { property: "object", value: "page" },
|
|
447
|
+
page_size: 50,
|
|
448
|
+
};
|
|
449
|
+
if (typeof query === "string" && query.length > 0)
|
|
450
|
+
q.query = query;
|
|
451
|
+
const res = (await client.search(q));
|
|
452
|
+
return {
|
|
453
|
+
results: Array.isArray(res.results) ? res.results : [],
|
|
454
|
+
hasMore: res.has_more === true,
|
|
455
|
+
nextCursor: res.next_cursor ?? null,
|
|
456
|
+
};
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
}
|