@notis_ai/cli 0.2.0-beta.35.1 → 0.2.0-beta.41.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 (83) hide show
  1. package/README.md +80 -75
  2. package/dist/scaffolds/notis-database/app/globals.css +44 -0
  3. package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
  4. package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
  5. package/dist/scaffolds/notis-database/index.html +12 -0
  6. package/dist/scaffolds/notis-database/lib/types.ts +134 -0
  7. package/dist/scaffolds/notis-database/metadata/cover.png +0 -0
  8. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  9. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  10. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  11. package/dist/scaffolds/notis-database/notis.config.ts +27 -0
  12. package/dist/scaffolds/notis-database/package.json +31 -0
  13. package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
  14. package/dist/scaffolds/notis-database/src/mock-runtime.ts +557 -0
  15. package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
  16. package/dist/scaffolds/{notes → notis-database}/tsconfig.json +2 -1
  17. package/dist/scaffolds/notis-database/vite.config.ts +22 -0
  18. package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
  19. package/dist/scaffolds/{notes → notis-notes}/app/page.tsx +783 -80
  20. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
  21. package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
  22. package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
  23. package/dist/scaffolds/notis-notes/components.json +20 -0
  24. package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
  25. package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
  26. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  27. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  28. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  29. package/dist/scaffolds/{notes → notis-notes}/notis.config.ts +12 -7
  30. package/dist/scaffolds/{notes → notis-notes}/package.json +3 -3
  31. package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
  32. package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
  33. package/dist/scaffolds/notis-random/README.md +33 -0
  34. package/dist/scaffolds/notis-random/app/globals.css +11 -0
  35. package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
  36. package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
  37. package/dist/scaffolds/notis-random/app/page.tsx +222 -0
  38. package/dist/scaffolds/notis-random/index.html +12 -0
  39. package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
  40. package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
  41. package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
  42. package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
  43. package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
  44. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  45. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  46. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  47. package/dist/scaffolds/notis-random/notis.config.ts +43 -0
  48. package/dist/scaffolds/notis-random/package.json +32 -0
  49. package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
  50. package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
  51. package/dist/scaffolds/notis-random/src/mock-runtime.ts +128 -0
  52. package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
  53. package/dist/scaffolds/notis-random/tsconfig.json +23 -0
  54. package/dist/scaffolds/notis-random/vite.config.ts +11 -0
  55. package/dist/scaffolds.json +27 -4
  56. package/package.json +1 -1
  57. package/skills/notis-apps/SKILL.md +27 -33
  58. package/skills/notis-apps/cli.md +51 -51
  59. package/skills/notis-cli/SKILL.md +65 -70
  60. package/skills/notis-query/cli.md +11 -11
  61. package/src/cli.js +10 -1
  62. package/src/command-specs/apps.js +2 -2
  63. package/src/command-specs/helpers.js +2 -0
  64. package/src/command-specs/tools.js +100 -2
  65. package/src/runtime/app-dev-server.js +2 -2
  66. package/src/runtime/app-platform.js +28 -0
  67. package/src/runtime/transport.js +129 -17
  68. package/template/packages/sdk/package.json +6 -0
  69. package/template/packages/sdk/src/hooks/useDatabase.ts +76 -0
  70. package/template/packages/sdk/src/hooks/useNotis.ts +10 -6
  71. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +50 -0
  72. package/template/packages/sdk/src/index.ts +9 -1
  73. package/template/packages/sdk/src/runtime.ts +83 -2
  74. /package/dist/scaffolds/{notes → notis-database}/app/layout.tsx +0 -0
  75. /package/dist/scaffolds/{notes → notis-database}/components/ui/badge.tsx +0 -0
  76. /package/dist/scaffolds/{notes → notis-database}/components/ui/button.tsx +0 -0
  77. /package/dist/scaffolds/{notes → notis-database}/components/ui/card.tsx +0 -0
  78. /package/dist/scaffolds/{notes → notis-database}/components.json +0 -0
  79. /package/dist/scaffolds/{notes → notis-database}/lib/utils.ts +0 -0
  80. /package/dist/scaffolds/{notes → notis-database}/postcss.config.mjs +0 -0
  81. /package/dist/scaffolds/{notes → notis-notes}/app/globals.css +0 -0
  82. /package/dist/scaffolds/{notes → notis-notes}/tailwind.config.ts +0 -0
  83. /package/dist/scaffolds/{notes → notis-notes}/vite.config.ts +0 -0
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { createReadStream } from 'node:fs';
2
3
  import { CliError, EXIT_CODES } from './errors.js';
3
4
  import {
4
5
  DEFAULT_PROFILE,
@@ -35,6 +36,73 @@ function canonicalizeToolName(toolName) {
35
36
  return toolName;
36
37
  }
37
38
 
39
+ function escapeMultipartHeaderValue(value) {
40
+ return String(value ?? '')
41
+ .replace(/"/g, '%22')
42
+ .replace(/\r/g, '%0D')
43
+ .replace(/\n/g, '%0A');
44
+ }
45
+
46
+ function multipartTextPart(boundary, name, value) {
47
+ return [
48
+ `--${boundary}`,
49
+ `Content-Disposition: form-data; name="${escapeMultipartHeaderValue(name)}"`,
50
+ '',
51
+ value,
52
+ '',
53
+ ].join('\r\n');
54
+ }
55
+
56
+ function multipartFileHeader(boundary, binding) {
57
+ const filename = escapeMultipartHeaderValue(binding.basename || binding.field_name);
58
+ return [
59
+ `--${boundary}`,
60
+ `Content-Disposition: form-data; name="${escapeMultipartHeaderValue(binding.field_name)}"; filename="${filename}"`,
61
+ `Content-Type: ${binding.contentType || 'application/octet-stream'}`,
62
+ '',
63
+ '',
64
+ ].join('\r\n');
65
+ }
66
+
67
+ async function* multipartFileBody(boundary, payload, bindingMetadata, fileBindings) {
68
+ yield Buffer.from(multipartTextPart(boundary, 'payload', JSON.stringify(payload)));
69
+ yield Buffer.from(multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)));
70
+ for (const binding of fileBindings) {
71
+ yield Buffer.from(multipartFileHeader(boundary, binding));
72
+ for await (const chunk of createReadStream(binding.localPath)) {
73
+ yield chunk;
74
+ }
75
+ yield Buffer.from('\r\n');
76
+ }
77
+ yield Buffer.from(`--${boundary}--\r\n`);
78
+ }
79
+
80
+ function createMultipartFileUpload(payload, bindingMetadata, fileBindings) {
81
+ const boundary = `----notis-cli-${randomUUID().replace(/-/g, '')}`;
82
+ const textParts = [
83
+ multipartTextPart(boundary, 'payload', JSON.stringify(payload)),
84
+ multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)),
85
+ ];
86
+ const fileHeaderParts = fileBindings.map((binding) => multipartFileHeader(boundary, binding));
87
+ const closingPart = `--${boundary}--\r\n`;
88
+ const contentLength = [
89
+ ...textParts,
90
+ ...fileHeaderParts,
91
+ ...fileBindings.map(() => '\r\n'),
92
+ closingPart,
93
+ ].reduce((total, part) => total + Buffer.byteLength(part), 0)
94
+ + fileBindings.reduce((total, binding) => total + binding.size, 0);
95
+
96
+ return {
97
+ body: multipartFileBody(boundary, payload, bindingMetadata, fileBindings),
98
+ headers: {
99
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
100
+ 'Content-Length': String(contentLength),
101
+ },
102
+ duplex: 'half',
103
+ };
104
+ }
105
+
38
106
  function normalizeBackendError(status, payload) {
39
107
  const backendError = payload?.error;
40
108
  const message =
@@ -197,6 +265,7 @@ export async function httpRequest({
197
265
  method = 'POST',
198
266
  path,
199
267
  body,
268
+ multipart = false,
200
269
  requireAuth = true,
201
270
  }) {
202
271
  await maybeRefreshDevPortalAuth(runtime);
@@ -211,21 +280,41 @@ export async function httpRequest({
211
280
  };
212
281
 
213
282
  const headers = {
214
- 'Content-Type': 'application/json',
215
283
  'X-Notis-CLI-Version': runtime.cliVersion,
216
284
  'X-Notis-Request-Id': requestId,
217
285
  };
286
+ if (!multipart) {
287
+ headers['Content-Type'] = 'application/json';
288
+ }
218
289
 
219
290
  if (requireAuth && runtime.jwt) {
220
291
  headers.Authorization = `Bearer ${runtime.jwt}`;
221
292
  }
222
293
 
294
+ const resolveBody = () => {
295
+ if (!multipart) {
296
+ return {
297
+ body: body ? JSON.stringify(body) : undefined,
298
+ headers: {},
299
+ };
300
+ }
301
+ if (typeof body === 'function') {
302
+ return body();
303
+ }
304
+ return {
305
+ body,
306
+ headers: {},
307
+ };
308
+ };
309
+
223
310
  try {
311
+ let requestBody = resolveBody();
224
312
  let response = await fetch(`${runtime.apiBase}${path}`, {
225
313
  method,
226
- headers,
227
- body: body ? JSON.stringify(body) : undefined,
314
+ headers: { ...headers, ...requestBody.headers },
315
+ body: requestBody.body,
228
316
  signal: controller.signal,
317
+ ...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
229
318
  });
230
319
 
231
320
  let payload = null;
@@ -243,11 +332,13 @@ export async function httpRequest({
243
332
  headers.Authorization = `Bearer ${runtime.jwt}`;
244
333
  }
245
334
  resetTimeout();
335
+ requestBody = resolveBody();
246
336
  response = await fetch(`${runtime.apiBase}${path}`, {
247
337
  method,
248
- headers,
249
- body: body ? JSON.stringify(body) : undefined,
338
+ headers: { ...headers, ...requestBody.headers },
339
+ body: requestBody.body,
250
340
  signal: controller.signal,
341
+ ...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
251
342
  });
252
343
  try {
253
344
  payload = await response.json();
@@ -302,22 +393,43 @@ export async function callTool({
302
393
  toolName,
303
394
  arguments_: argumentsPayload = {},
304
395
  idempotencyKey,
396
+ fileBindings = [],
305
397
  }) {
306
398
  const requestId = idempotencyKey || (runtime.mutating ? randomUUID() : null);
399
+ const payload = {
400
+ tool_name: canonicalizeToolName(toolName),
401
+ arguments: argumentsPayload,
402
+ idempotency_key: requestId,
403
+ cli_context: {
404
+ output_mode: runtime.outputMode,
405
+ profile: runtime.profileName,
406
+ cwd: process.cwd(),
407
+ agent_mode: runtime.agentMode,
408
+ cli_version: runtime.cliVersion,
409
+ },
410
+ };
411
+
412
+ if (Array.isArray(fileBindings) && fileBindings.length) {
413
+ const bindingMetadata = fileBindings.map((binding) => {
414
+ const {
415
+ contentType,
416
+ localPath,
417
+ ...metadata
418
+ } = binding;
419
+ return metadata;
420
+ });
421
+
422
+ return httpRequest({
423
+ runtime,
424
+ path: '/cli_tools',
425
+ body: () => createMultipartFileUpload(payload, bindingMetadata, fileBindings),
426
+ multipart: true,
427
+ });
428
+ }
429
+
307
430
  return httpRequest({
308
431
  runtime,
309
432
  path: '/cli_tools',
310
- body: {
311
- tool_name: canonicalizeToolName(toolName),
312
- arguments: argumentsPayload,
313
- idempotency_key: requestId,
314
- cli_context: {
315
- output_mode: runtime.outputMode,
316
- profile: runtime.profileName,
317
- cwd: process.cwd(),
318
- agent_mode: runtime.agentMode,
319
- cli_version: runtime.cliVersion,
320
- },
321
- },
433
+ body: payload,
322
434
  });
323
435
  }
@@ -14,6 +14,9 @@
14
14
  "src",
15
15
  "template"
16
16
  ],
17
+ "scripts": {
18
+ "type-check": "tsc --noEmit"
19
+ },
17
20
  "peerDependencies": {
18
21
  "react": ">=18.0.0",
19
22
  "react-dom": ">=18.0.0",
@@ -21,6 +24,9 @@
21
24
  "@vitejs/plugin-react": ">=4.0.0"
22
25
  },
23
26
  "devDependencies": {
27
+ "@types/node": "^20.0.0",
28
+ "@types/react": "^19.0.0",
29
+ "@types/react-dom": "^19.0.0",
24
30
  "typescript": "^5.0.0"
25
31
  }
26
32
  }
@@ -0,0 +1,76 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { useNotisRuntime } from '../provider';
5
+ import type { DocumentRecord, QueryFilter } from '../runtime';
6
+
7
+ interface UseDatabaseOptions {
8
+ filter?: QueryFilter;
9
+ pageSize?: number;
10
+ offset?: number;
11
+ enabled?: boolean;
12
+ }
13
+
14
+ interface UseDatabaseResult {
15
+ documents: DocumentRecord[];
16
+ loading: boolean;
17
+ error: Error | null;
18
+ refetch: () => void;
19
+ }
20
+
21
+ export function useDatabase(
22
+ databaseSlug: string,
23
+ options: UseDatabaseOptions = {},
24
+ ): UseDatabaseResult {
25
+ const runtime = useNotisRuntime();
26
+ const [documents, setDocuments] = useState<DocumentRecord[]>([]);
27
+ const [loading, setLoading] = useState(true);
28
+ const [error, setError] = useState<Error | null>(null);
29
+ const [fetchKey, setFetchKey] = useState(0);
30
+
31
+ const enabled = options.enabled !== false;
32
+ const filterKey = JSON.stringify(options.filter ?? null);
33
+
34
+ const refetch = useCallback(() => {
35
+ setFetchKey((key) => key + 1);
36
+ }, []);
37
+
38
+ useEffect(() => {
39
+ if (!runtime || !enabled) {
40
+ setLoading(false);
41
+ return;
42
+ }
43
+
44
+ let cancelled = false;
45
+ setLoading(true);
46
+ setError(null);
47
+
48
+ runtime
49
+ .queryDatabase({
50
+ databaseSlug,
51
+ query: {
52
+ ...options.filter,
53
+ page_size: options.pageSize,
54
+ },
55
+ offset: options.offset,
56
+ })
57
+ .then((result) => {
58
+ if (!cancelled) {
59
+ setDocuments(result.documents);
60
+ setLoading(false);
61
+ }
62
+ })
63
+ .catch((err) => {
64
+ if (!cancelled) {
65
+ setError(err instanceof Error ? err : new Error(String(err)));
66
+ setLoading(false);
67
+ }
68
+ });
69
+
70
+ return () => {
71
+ cancelled = true;
72
+ };
73
+ }, [runtime, databaseSlug, fetchKey, enabled, filterKey, options.offset, options.pageSize]);
74
+
75
+ return { documents, loading, error, refetch };
76
+ }
@@ -1,22 +1,25 @@
1
1
  'use client';
2
2
 
3
3
  import { useNotisRuntime } from '../provider';
4
- import type { AppDescriptor, NotisRuntimeContext, RouteDescriptor } from '../runtime';
4
+ import type { AppDescriptor, CollectionItemDetail, DatabaseDescriptor, RouteDescriptor } from '../runtime';
5
5
 
6
6
  interface NotisContext {
7
7
  /** App metadata (id, name, icon, description). Null before runtime loads. */
8
8
  app: AppDescriptor | null;
9
9
  /** Current route descriptor. Null before runtime loads. */
10
10
  route: RouteDescriptor | null;
11
- /** Generic route/app context supplied by the portal. */
12
- context: NotisRuntimeContext;
11
+ /** Databases declared by this app. Empty before runtime loads. */
12
+ databases: DatabaseDescriptor[];
13
+ /** Selected collection item for the current route, when applicable. */
14
+ collectionItem: CollectionItemDetail | null;
13
15
  /** Whether the runtime is loaded and available. */
14
16
  ready: boolean;
15
17
  }
16
18
 
17
19
  /**
18
- * Access app-level metadata, the current route, and generic portal context.
19
- * Returns safe defaults when the portal runtime is not available.
20
+ * Access app-level metadata, the current route, declared databases, and the
21
+ * selected collection item. Returns safe defaults when the portal runtime is
22
+ * not available.
20
23
  */
21
24
  export function useNotis(): NotisContext {
22
25
  const runtime = useNotisRuntime();
@@ -24,7 +27,8 @@ export function useNotis(): NotisContext {
24
27
  return {
25
28
  app: runtime?.app ?? null,
26
29
  route: runtime?.route ?? null,
27
- context: runtime?.context ?? {},
30
+ databases: runtime?.databases ?? [],
31
+ collectionItem: runtime?.context?.collectionItem ?? null,
28
32
  ready: runtime !== null,
29
33
  };
30
34
  }
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useState } from 'react';
4
+ import { useNotisRuntime } from '../provider';
5
+ import type { DocumentRecord } from '../runtime';
6
+
7
+ interface UpsertArgs {
8
+ databaseSlug: string;
9
+ documentId?: string;
10
+ title?: string;
11
+ properties?: Record<string, unknown>;
12
+ contentBlocknote?: Array<Record<string, unknown>> | null;
13
+ }
14
+
15
+ interface UseUpsertDocumentResult {
16
+ upsert: (args: UpsertArgs) => Promise<DocumentRecord>;
17
+ loading: boolean;
18
+ error: Error | null;
19
+ }
20
+
21
+ export function useUpsertDocument(): UseUpsertDocumentResult {
22
+ const runtime = useNotisRuntime();
23
+ const [loading, setLoading] = useState(false);
24
+ const [error, setError] = useState<Error | null>(null);
25
+
26
+ const upsert = useCallback(
27
+ async (args: UpsertArgs): Promise<DocumentRecord> => {
28
+ if (!runtime) {
29
+ throw new Error('Notis runtime not available. Ensure NotisProvider is mounted.');
30
+ }
31
+
32
+ setLoading(true);
33
+ setError(null);
34
+
35
+ try {
36
+ const result = await runtime.upsertDocument(args);
37
+ return result.document;
38
+ } catch (err) {
39
+ const e = err instanceof Error ? err : new Error(String(err));
40
+ setError(e);
41
+ throw e;
42
+ } finally {
43
+ setLoading(false);
44
+ }
45
+ },
46
+ [runtime],
47
+ );
48
+
49
+ return { upsert, loading, error };
50
+ }
@@ -7,10 +7,12 @@
7
7
  */
8
8
 
9
9
  // Provider
10
- export { NotisProvider } from './provider';
10
+ export { NotisProvider, useNotisRuntime } from './provider';
11
11
 
12
12
  // Hooks
13
13
  export { useNotis } from './hooks/useNotis';
14
+ export { useDatabase } from './hooks/useDatabase';
15
+ export { useUpsertDocument } from './hooks/useUpsertDocument';
14
16
  export { useTool } from './hooks/useTool';
15
17
  export type { ToolCallState, UseToolResult } from './hooks/useTool';
16
18
  export { useTools } from './hooks/useTools';
@@ -38,8 +40,14 @@ export type { MultiSelectDragOverlayProps } from './components/MultiSelectDragOv
38
40
  // Types (re-exported for convenience)
39
41
  export type {
40
42
  AppDescriptor,
43
+ CollectionItem,
44
+ CollectionItemDetail,
45
+ DatabaseDescriptor,
46
+ DatabaseProperty,
47
+ DocumentRecord,
41
48
  NotisRuntime,
42
49
  NotisRuntimeContext,
50
+ QueryFilter,
43
51
  RouteDescriptor,
44
52
  ToolDescriptor,
45
53
  ToolInputSchema,
@@ -7,6 +7,39 @@
7
7
  * (useTool, useNotis, etc.) which read from the NotisProvider context.
8
8
  */
9
9
 
10
+ // ---------------------------------------------------------------------------
11
+ // Database types
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export interface DatabaseProperty {
15
+ name: string;
16
+ type: 'title' | 'rich_text' | 'number' | 'checkbox' | 'date' | 'select' | 'multi_select' | 'status' | 'relation' | 'formula' | 'files';
17
+ description?: string;
18
+ options?: Array<{ name: string; id?: string }>;
19
+ }
20
+
21
+ export interface DatabaseDescriptor {
22
+ slug: string;
23
+ title: string;
24
+ description?: string | null;
25
+ icon?: string | null;
26
+ properties: DatabaseProperty[];
27
+ }
28
+
29
+ export interface DocumentRecord {
30
+ id: string;
31
+ title: string;
32
+ properties: Record<string, unknown>;
33
+ icon?: string | null;
34
+ cover?: string | null;
35
+ databaseSlug?: string;
36
+ contentBlocknote?: Array<Record<string, unknown>> | null;
37
+ contentMarkdown?: string | null;
38
+ plainText?: string | null;
39
+ createdAt?: string | null;
40
+ lastEditedTime?: string | null;
41
+ }
42
+
10
43
  // ---------------------------------------------------------------------------
11
44
  // Tool types
12
45
  // ---------------------------------------------------------------------------
@@ -30,7 +63,25 @@ export interface RouteDescriptor {
30
63
  icon?: string | null;
31
64
  parentSlug?: string | null;
32
65
  default?: boolean;
33
- collection?: Record<string, unknown> | null;
66
+ collection?: {
67
+ database: string;
68
+ titleProperty: string;
69
+ parentProperty?: string | null;
70
+ sidebar?: {
71
+ mode: 'flat-list' | 'tree';
72
+ allowCreate: boolean;
73
+ } | null;
74
+ } | null;
75
+ }
76
+
77
+ export interface CollectionItem {
78
+ id: string;
79
+ title: string;
80
+ icon?: string | null;
81
+ }
82
+
83
+ export interface CollectionItemDetail extends CollectionItem {
84
+ properties: Record<string, unknown>;
34
85
  }
35
86
 
36
87
  // ---------------------------------------------------------------------------
@@ -44,7 +95,22 @@ export interface AppDescriptor {
44
95
  description?: string | null;
45
96
  }
46
97
 
47
- export type NotisRuntimeContext = Record<string, unknown>;
98
+ export interface QueryFilter {
99
+ filters?: Array<{
100
+ property: string;
101
+ operator: string;
102
+ value: unknown;
103
+ }>;
104
+ sorts?: Array<{
105
+ property: string;
106
+ direction: 'asc' | 'desc';
107
+ }>;
108
+ page_size?: number;
109
+ }
110
+
111
+ export interface NotisRuntimeContext {
112
+ collectionItem?: CollectionItemDetail | null;
113
+ }
48
114
 
49
115
  // ---------------------------------------------------------------------------
50
116
  // NotisRuntime
@@ -53,6 +119,7 @@ export type NotisRuntimeContext = Record<string, unknown>;
53
119
  export interface NotisRuntime {
54
120
  app: AppDescriptor;
55
121
  route: RouteDescriptor;
122
+ databases: DatabaseDescriptor[];
56
123
  context: NotisRuntimeContext;
57
124
 
58
125
  navigate?: (payload: { kind: string; [key: string]: unknown }) => void;
@@ -72,6 +139,20 @@ export interface NotisRuntime {
72
139
  listTools(): Promise<ToolDescriptor[]>;
73
140
  callTool<TResult = unknown>(name: string, args?: Record<string, unknown>): Promise<TResult>;
74
141
 
142
+ queryDatabase(args: {
143
+ databaseSlug: string;
144
+ query?: QueryFilter;
145
+ offset?: number;
146
+ }): Promise<{ documents: DocumentRecord[] }>;
147
+
148
+ upsertDocument(args: {
149
+ databaseSlug: string;
150
+ documentId?: string;
151
+ title?: string;
152
+ properties?: Record<string, unknown>;
153
+ contentBlocknote?: Array<Record<string, unknown>> | null;
154
+ }): Promise<{ status: string; document: DocumentRecord }>;
155
+
75
156
  request(path: string, options?: {
76
157
  method?: string;
77
158
  headers?: Record<string, string>;