@notis_ai/cli 0.2.1 → 0.2.3

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 (117) hide show
  1. package/README.md +106 -170
  2. package/config/notis_app_boundary_rules.json +50 -0
  3. package/dist/scaffolds/notis-database/app/globals.css +44 -0
  4. package/dist/scaffolds/notis-database/app/layout.tsx +6 -0
  5. package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
  6. package/dist/scaffolds/notis-database/components/ui/badge.tsx +28 -0
  7. package/dist/scaffolds/notis-database/components/ui/button.tsx +53 -0
  8. package/dist/scaffolds/notis-database/components/ui/card.tsx +56 -0
  9. package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
  10. package/dist/scaffolds/notis-database/components.json +20 -0
  11. package/dist/scaffolds/notis-database/index.html +12 -0
  12. package/dist/scaffolds/notis-database/lib/types.ts +134 -0
  13. package/dist/scaffolds/notis-database/lib/utils.ts +6 -0
  14. package/dist/scaffolds/notis-database/metadata/cover.png +0 -0
  15. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  16. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  17. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  18. package/dist/scaffolds/notis-database/notis.config.ts +27 -0
  19. package/dist/scaffolds/notis-database/package.json +31 -0
  20. package/dist/scaffolds/notis-database/postcss.config.mjs +8 -0
  21. package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
  22. package/dist/scaffolds/notis-database/src/mock-runtime.ts +557 -0
  23. package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
  24. package/dist/scaffolds/notis-database/tsconfig.json +23 -0
  25. package/dist/scaffolds/notis-database/vite.config.ts +22 -0
  26. package/dist/scaffolds/notis-notes/app/globals.css +3 -0
  27. package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
  28. package/dist/scaffolds/notis-notes/app/page.tsx +2168 -0
  29. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
  30. package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
  31. package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
  32. package/dist/scaffolds/notis-notes/components.json +20 -0
  33. package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
  34. package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
  35. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  36. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  37. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  38. package/dist/scaffolds/notis-notes/notis.config.ts +36 -0
  39. package/dist/scaffolds/notis-notes/package.json +31 -0
  40. package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
  41. package/dist/scaffolds/notis-notes/tailwind.config.ts +58 -0
  42. package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
  43. package/dist/scaffolds/notis-notes/vite.config.ts +10 -0
  44. package/dist/scaffolds/notis-random/README.md +33 -0
  45. package/dist/scaffolds/notis-random/app/globals.css +11 -0
  46. package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
  47. package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
  48. package/dist/scaffolds/notis-random/app/page.tsx +222 -0
  49. package/dist/scaffolds/notis-random/index.html +12 -0
  50. package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
  51. package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
  52. package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
  53. package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
  54. package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
  55. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  56. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  57. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  58. package/dist/scaffolds/notis-random/notis.config.ts +43 -0
  59. package/dist/scaffolds/notis-random/package.json +32 -0
  60. package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
  61. package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
  62. package/dist/scaffolds/notis-random/src/mock-runtime.ts +128 -0
  63. package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
  64. package/dist/scaffolds/notis-random/tsconfig.json +23 -0
  65. package/dist/scaffolds/notis-random/vite.config.ts +11 -0
  66. package/dist/scaffolds.json +36 -0
  67. package/package.json +8 -3
  68. package/skills/notis-apps/SKILL.md +162 -0
  69. package/skills/notis-apps/cli.md +208 -0
  70. package/skills/notis-cli/SKILL.md +260 -0
  71. package/skills/notis-query/cli.md +39 -0
  72. package/src/cli.js +31 -3
  73. package/src/command-specs/apps.js +248 -56
  74. package/src/command-specs/helpers.js +72 -104
  75. package/src/command-specs/index.js +0 -6
  76. package/src/command-specs/meta.js +14 -13
  77. package/src/command-specs/tools.js +196 -115
  78. package/src/runtime/app-boundary-validator.js +65 -14
  79. package/src/runtime/app-dev-server.js +32 -4
  80. package/src/runtime/app-platform.js +404 -24
  81. package/src/runtime/profiles.js +12 -6
  82. package/src/runtime/transport.js +124 -39
  83. package/template/.harness/index.html.tmpl +1 -50
  84. package/template/app/page.tsx +41 -6
  85. package/template/metadata/cover.png +0 -0
  86. package/template/metadata/screenshot-1.png +0 -0
  87. package/template/metadata/screenshot-2.png +0 -0
  88. package/template/metadata/screenshot-3.png +0 -0
  89. package/template/notis.config.ts +10 -6
  90. package/template/package.json +2 -2
  91. package/template/packages/{notis-sdk → sdk}/package.json +6 -0
  92. package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectActionBar.tsx +2 -1
  93. package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectCheckbox.tsx +2 -2
  94. package/template/packages/{notis-sdk → sdk}/src/components/MultiSelectDragOverlay.tsx +2 -2
  95. package/template/packages/{notis-sdk → sdk}/src/config.ts +1 -1
  96. package/template/packages/{notis-sdk → sdk}/src/hooks/useDatabase.ts +1 -13
  97. package/template/packages/{notis-sdk → sdk}/src/hooks/useMultiSelect.ts +4 -3
  98. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +4 -3
  99. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +3 -3
  100. package/template/packages/{notis-sdk → sdk}/src/hooks/useTool.ts +22 -7
  101. package/template/packages/{notis-sdk → sdk}/src/hooks/useUpsertDocument.ts +0 -8
  102. package/template/packages/{notis-sdk → sdk}/src/index.ts +2 -15
  103. package/template/packages/{notis-sdk → sdk}/src/provider.tsx +1 -1
  104. package/template/packages/{notis-sdk → sdk}/src/runtime.ts +5 -26
  105. package/src/command-specs/auth.js +0 -178
  106. package/src/command-specs/db.js +0 -163
  107. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  108. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  109. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  110. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  111. /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
  112. /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
  113. /package/template/packages/{notis-sdk → sdk}/src/hooks/useTopBarSearch.ts +0 -0
  114. /package/template/packages/{notis-sdk → sdk}/src/styles.css +0 -0
  115. /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
  116. /package/template/packages/{notis-sdk → sdk}/src/vite.ts +0 -0
  117. /package/template/packages/{notis-sdk → sdk}/tsconfig.json +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,
@@ -8,31 +9,71 @@ import {
8
9
  saveConfig,
9
10
  } from './profiles.js';
10
11
 
11
- const META_TOOL_NAMES = new Set([
12
- 'notis_find_toolkits',
13
- 'notis_find_tools',
14
- 'notis_execute_tool',
15
- 'notis_find_additional_tools',
16
- ]);
17
-
18
- function canonicalizeToolName(toolName) {
19
- if (typeof toolName !== 'string' || !toolName.length) {
20
- return toolName;
21
- }
12
+ function escapeMultipartHeaderValue(value) {
13
+ return String(value ?? '')
14
+ .replace(/"/g, '%22')
15
+ .replace(/\r/g, '%0D')
16
+ .replace(/\n/g, '%0A');
17
+ }
22
18
 
23
- if (META_TOOL_NAMES.has(toolName) || toolName.startsWith('notis-default-')) {
24
- return toolName;
25
- }
19
+ function multipartTextPart(boundary, name, value) {
20
+ return [
21
+ `--${boundary}`,
22
+ `Content-Disposition: form-data; name="${escapeMultipartHeaderValue(name)}"`,
23
+ '',
24
+ value,
25
+ '',
26
+ ].join('\r\n');
27
+ }
26
28
 
27
- if (toolName.startsWith('notis_')) {
28
- return `notis-default-${toolName.slice('notis_'.length)}`;
29
- }
29
+ function multipartFileHeader(boundary, binding) {
30
+ const filename = escapeMultipartHeaderValue(binding.basename || binding.field_name);
31
+ return [
32
+ `--${boundary}`,
33
+ `Content-Disposition: form-data; name="${escapeMultipartHeaderValue(binding.field_name)}"; filename="${filename}"`,
34
+ `Content-Type: ${binding.contentType || 'application/octet-stream'}`,
35
+ '',
36
+ '',
37
+ ].join('\r\n');
38
+ }
30
39
 
31
- if (toolName.includes('_notis_')) {
32
- return `notis-default-${toolName.replace(/_notis_/g, '_')}`;
40
+ async function* multipartFileBody(boundary, payload, bindingMetadata, fileBindings) {
41
+ yield Buffer.from(multipartTextPart(boundary, 'payload', JSON.stringify(payload)));
42
+ yield Buffer.from(multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)));
43
+ for (const binding of fileBindings) {
44
+ yield Buffer.from(multipartFileHeader(boundary, binding));
45
+ for await (const chunk of createReadStream(binding.localPath)) {
46
+ yield chunk;
47
+ }
48
+ yield Buffer.from('\r\n');
33
49
  }
50
+ yield Buffer.from(`--${boundary}--\r\n`);
51
+ }
34
52
 
35
- return toolName;
53
+ function createMultipartFileUpload(payload, bindingMetadata, fileBindings) {
54
+ const boundary = `----notis-cli-${randomUUID().replace(/-/g, '')}`;
55
+ const textParts = [
56
+ multipartTextPart(boundary, 'payload', JSON.stringify(payload)),
57
+ multipartTextPart(boundary, 'file_bindings', JSON.stringify(bindingMetadata)),
58
+ ];
59
+ const fileHeaderParts = fileBindings.map((binding) => multipartFileHeader(boundary, binding));
60
+ const closingPart = `--${boundary}--\r\n`;
61
+ const contentLength = [
62
+ ...textParts,
63
+ ...fileHeaderParts,
64
+ ...fileBindings.map(() => '\r\n'),
65
+ closingPart,
66
+ ].reduce((total, part) => total + Buffer.byteLength(part), 0)
67
+ + fileBindings.reduce((total, binding) => total + binding.size, 0);
68
+
69
+ return {
70
+ body: multipartFileBody(boundary, payload, bindingMetadata, fileBindings),
71
+ headers: {
72
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
73
+ 'Content-Length': String(contentLength),
74
+ },
75
+ duplex: 'half',
76
+ };
36
77
  }
37
78
 
38
79
  function normalizeBackendError(status, payload) {
@@ -51,8 +92,8 @@ function normalizeBackendError(status, payload) {
51
92
  exitCode: EXIT_CODES.auth,
52
93
  hints: [
53
94
  {
54
- command: 'notis auth login --jwt <token>',
55
- reason: 'Refresh the stored JWT for this profile',
95
+ command: 'Open the Notis desktop app and sign in',
96
+ reason: 'Open the Notis desktop app to refresh CLI auth, or set NOTIS_JWT',
56
97
  },
57
98
  ],
58
99
  details: payload || {},
@@ -197,6 +238,7 @@ export async function httpRequest({
197
238
  method = 'POST',
198
239
  path,
199
240
  body,
241
+ multipart = false,
200
242
  requireAuth = true,
201
243
  }) {
202
244
  await maybeRefreshDevPortalAuth(runtime);
@@ -211,21 +253,41 @@ export async function httpRequest({
211
253
  };
212
254
 
213
255
  const headers = {
214
- 'Content-Type': 'application/json',
215
256
  'X-Notis-CLI-Version': runtime.cliVersion,
216
257
  'X-Notis-Request-Id': requestId,
217
258
  };
259
+ if (!multipart) {
260
+ headers['Content-Type'] = 'application/json';
261
+ }
218
262
 
219
263
  if (requireAuth && runtime.jwt) {
220
264
  headers.Authorization = `Bearer ${runtime.jwt}`;
221
265
  }
222
266
 
267
+ const resolveBody = () => {
268
+ if (!multipart) {
269
+ return {
270
+ body: body ? JSON.stringify(body) : undefined,
271
+ headers: {},
272
+ };
273
+ }
274
+ if (typeof body === 'function') {
275
+ return body();
276
+ }
277
+ return {
278
+ body,
279
+ headers: {},
280
+ };
281
+ };
282
+
223
283
  try {
284
+ let requestBody = resolveBody();
224
285
  let response = await fetch(`${runtime.apiBase}${path}`, {
225
286
  method,
226
- headers,
227
- body: body ? JSON.stringify(body) : undefined,
287
+ headers: { ...headers, ...requestBody.headers },
288
+ body: requestBody.body,
228
289
  signal: controller.signal,
290
+ ...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
229
291
  });
230
292
 
231
293
  let payload = null;
@@ -243,11 +305,13 @@ export async function httpRequest({
243
305
  headers.Authorization = `Bearer ${runtime.jwt}`;
244
306
  }
245
307
  resetTimeout();
308
+ requestBody = resolveBody();
246
309
  response = await fetch(`${runtime.apiBase}${path}`, {
247
310
  method,
248
- headers,
249
- body: body ? JSON.stringify(body) : undefined,
311
+ headers: { ...headers, ...requestBody.headers },
312
+ body: requestBody.body,
250
313
  signal: controller.signal,
314
+ ...(requestBody.duplex ? { duplex: requestBody.duplex } : {}),
251
315
  });
252
316
  try {
253
317
  payload = await response.json();
@@ -302,22 +366,43 @@ export async function callTool({
302
366
  toolName,
303
367
  arguments_: argumentsPayload = {},
304
368
  idempotencyKey,
369
+ fileBindings = [],
305
370
  }) {
306
371
  const requestId = idempotencyKey || (runtime.mutating ? randomUUID() : null);
372
+ const payload = {
373
+ tool_name: toolName,
374
+ arguments: argumentsPayload,
375
+ idempotency_key: requestId,
376
+ cli_context: {
377
+ output_mode: runtime.outputMode,
378
+ profile: runtime.profileName,
379
+ cwd: process.cwd(),
380
+ agent_mode: runtime.agentMode,
381
+ cli_version: runtime.cliVersion,
382
+ },
383
+ };
384
+
385
+ if (Array.isArray(fileBindings) && fileBindings.length) {
386
+ const bindingMetadata = fileBindings.map((binding) => {
387
+ const {
388
+ contentType,
389
+ localPath,
390
+ ...metadata
391
+ } = binding;
392
+ return metadata;
393
+ });
394
+
395
+ return httpRequest({
396
+ runtime,
397
+ path: '/cli_tools',
398
+ body: () => createMultipartFileUpload(payload, bindingMetadata, fileBindings),
399
+ multipart: true,
400
+ });
401
+ }
402
+
307
403
  return httpRequest({
308
404
  runtime,
309
405
  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
- },
406
+ body: payload,
322
407
  });
323
408
  }
@@ -107,8 +107,7 @@
107
107
  return {
108
108
  app: descriptor.app,
109
109
  route: descriptor.route,
110
- databases: descriptor.databases || [],
111
- context: descriptor.context || { collectionItem: null },
110
+ context: descriptor.context || {},
112
111
  navigate: (args) => record('navigate', args),
113
112
  registerTopBarSearch: () => {},
114
113
  setTopBarSearchValue: () => {},
@@ -121,34 +120,6 @@
121
120
  record('callTool', { name, arguments: args || {} });
122
121
  return { ok: true, result: null };
123
122
  },
124
- queryDatabase: async (args) => {
125
- record('queryDatabase', args || {});
126
- return { documents: [], next_offset: null };
127
- },
128
- getDocument: async (args) => {
129
- record('getDocument', args || {});
130
- return null;
131
- },
132
- upsertDocument: async (args) => {
133
- record('upsertDocument', args || {});
134
- return {
135
- status: 'ok',
136
- document: {
137
- id: (args && args.documentId) || 'mock-doc',
138
- title: (args && args.title) || '',
139
- properties: (args && args.properties) || {},
140
- databaseSlug: args && args.databaseSlug,
141
- },
142
- };
143
- },
144
- listCollectionItems: async (args) => {
145
- record('listCollectionItems', args || {});
146
- return { items: [] };
147
- },
148
- listCollectionTree: async (args) => {
149
- record('listCollectionTree', args || {});
150
- return { items: [] };
151
- },
152
123
  request: async (path, options) => {
153
124
  record('request', { path, options });
154
125
  return null;
@@ -168,26 +139,6 @@
168
139
  record('callTool', { name, arguments: args || {} });
169
140
  return runtimeQuery({ method: 'tools/call', name, arguments: args || {} });
170
141
  },
171
- queryDatabase: async (args) => {
172
- record('queryDatabase', args || {});
173
- const result = await runtimeQuery({ method: 'tools/call', name: 'notis_query_database', arguments: args || {} });
174
- return result.result || result;
175
- },
176
- getDocument: async (args) => {
177
- record('getDocument', args || {});
178
- const result = await runtimeQuery({ method: 'tools/call', name: 'notis_get_document', arguments: args || {} });
179
- return result.result || result;
180
- },
181
- upsertDocument: async (args) => {
182
- record('upsertDocument', args || {});
183
- const result = await runtimeQuery({ method: 'tools/call', name: 'notis_upsert_document', arguments: args || {} });
184
- return result.result || result;
185
- },
186
- listCollectionItems: async (args) => {
187
- record('listCollectionItems', args || {});
188
- const result = await runtimeQuery({ method: 'tools/call', name: 'notis_list_collection_items', arguments: args || {} });
189
- return result.result || result;
190
- },
191
142
  request: async (path, options) => {
192
143
  record('request', { path, options });
193
144
  const response = await fetch(`${String(apiBase).replace(/\/$/, '')}${path}`, {
@@ -1,12 +1,47 @@
1
1
  'use client';
2
2
 
3
- import { useNotis, useDatabase } from '@notis/sdk';
3
+ import { useEffect, useState } from 'react';
4
+ import { useNotis, useTool } from '@notis/sdk';
4
5
  import { Badge } from '@/components/ui/badge';
5
6
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
6
7
 
8
+ type ItemDocument = {
9
+ id?: string;
10
+ document_id?: string;
11
+ title?: string;
12
+ properties?: Record<string, unknown>;
13
+ };
14
+
15
+ type QueryItemsArgs = {
16
+ database_slug: string;
17
+ query: {
18
+ page_size?: number;
19
+ };
20
+ };
21
+
22
+ type QueryItemsResult = {
23
+ documents?: ItemDocument[];
24
+ };
25
+
7
26
  export default function HomePage() {
8
27
  const { app, ready } = useNotis();
9
- const { documents, loading } = useDatabase('items');
28
+ const queryItems = useTool<QueryItemsArgs, QueryItemsResult>('LOCAL_NOTIS_DATABASE_QUERY');
29
+ const [documents, setDocuments] = useState<ItemDocument[]>([]);
30
+
31
+ useEffect(() => {
32
+ let cancelled = false;
33
+ queryItems
34
+ .call({ database_slug: 'items', query: { page_size: 25 } })
35
+ .then((result) => {
36
+ if (!cancelled) setDocuments(result.documents || []);
37
+ })
38
+ .catch(() => {
39
+ if (!cancelled) setDocuments([]);
40
+ });
41
+ return () => {
42
+ cancelled = true;
43
+ };
44
+ }, [queryItems.call]);
10
45
 
11
46
  return (
12
47
  <main className="notis-app-shell space-y-6">
@@ -28,7 +63,7 @@ export default function HomePage() {
28
63
  <CardDescription>Use shadcn surfaces and portal tokens so the app feels native inside Notis.</CardDescription>
29
64
  </CardHeader>
30
65
  <CardContent>
31
- {loading ? (
66
+ {queryItems.loading ? (
32
67
  <p className="text-sm text-muted-foreground">Loading...</p>
33
68
  ) : documents.length === 0 ? (
34
69
  <div className="rounded-xl border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground">
@@ -37,10 +72,10 @@ export default function HomePage() {
37
72
  ) : (
38
73
  <div className="space-y-3">
39
74
  {documents.map((doc) => (
40
- <div key={doc.id} className="rounded-xl border border-border bg-background px-4 py-3">
75
+ <div key={doc.id || doc.document_id || doc.title} className="rounded-xl border border-border bg-background px-4 py-3">
41
76
  <div className="flex items-center justify-between gap-3">
42
- <p className="font-medium">{doc.title}</p>
43
- {doc.properties.status ? (
77
+ <p className="font-medium">{doc.title || 'Untitled'}</p>
78
+ {doc.properties?.status ? (
44
79
  <Badge variant="outline">{String(doc.properties.status)}</Badge>
45
80
  ) : null}
46
81
  </div>
Binary file
@@ -1,14 +1,19 @@
1
1
  import { defineNotisApp } from '@notis/sdk/config';
2
2
 
3
3
  export default defineNotisApp({
4
- name: 'My Notis App',
5
- description: 'A new Notis app',
6
- icon: 'lucide:layout-dashboard',
4
+ name: 'my-notis-app',
5
+ title: 'My Notis App',
6
+ description: 'A focused Notis app with one route and one database-backed workflow.',
7
+ icon: 'phosphor:squares-four',
8
+ author: { name: 'Notis User' },
9
+ categories: ['Productivity'],
10
+ tagline: 'A clean starting point for a focused workflow.',
11
+ versionNotes: 'Initial release.',
7
12
 
8
13
  databases: ['items'],
9
14
 
10
15
  routes: [
11
- { path: '/', slug: 'home', name: 'Home', icon: 'lucide:home', default: true },
16
+ { path: '/', slug: 'home', name: 'Home', icon: 'phosphor:house', default: true },
12
17
  // Example collection tree route:
13
18
  // {
14
19
  // path: '/notes',
@@ -27,7 +32,6 @@ export default defineNotisApp({
27
32
  ],
28
33
 
29
34
  tools: [
30
- 'notis_query_database',
31
- 'notis_upsert_document',
35
+ 'LOCAL_NOTIS_DATABASE_QUERY',
32
36
  ],
33
37
  });
@@ -8,11 +8,11 @@
8
8
  "build": "vite build"
9
9
  },
10
10
  "dependencies": {
11
- "@notis/sdk": "file:./packages/notis-sdk",
11
+ "@notis/sdk": "file:./packages/sdk",
12
+ "@phosphor-icons/react": "^2.1.10",
12
13
  "@radix-ui/react-slot": "^1.1.0",
13
14
  "class-variance-authority": "^0.7.0",
14
15
  "clsx": "^2.1.1",
15
- "lucide-react": "^0.474.0",
16
16
  "react": "^19.0.0",
17
17
  "react-dom": "^19.0.0",
18
18
  "tailwind-merge": "^2.6.0",
@@ -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
  }
@@ -6,6 +6,7 @@ import {
6
6
  useRef,
7
7
  useState,
8
8
  type CSSProperties,
9
+ type ReactElement,
9
10
  type ReactNode,
10
11
  } from 'react';
11
12
 
@@ -174,7 +175,7 @@ export function MultiSelectActionBar({
174
175
  itemLabel,
175
176
  className,
176
177
  style,
177
- }: MultiSelectActionBarProps): JSX.Element | null {
178
+ }: MultiSelectActionBarProps): ReactElement | null {
178
179
  const actionsRef = useRef(actions);
179
180
  actionsRef.current = actions;
180
181
 
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { type CSSProperties, type MouseEvent as ReactMouseEvent } from 'react';
3
+ import { type CSSProperties, type MouseEvent as ReactMouseEvent, type ReactElement } from 'react';
4
4
 
5
5
  export interface MultiSelectCheckboxProps {
6
6
  isSelected: boolean;
@@ -44,7 +44,7 @@ export function MultiSelectCheckbox({
44
44
  ariaLabel,
45
45
  className,
46
46
  style,
47
- }: MultiSelectCheckboxProps): JSX.Element {
47
+ }: MultiSelectCheckboxProps): ReactElement {
48
48
  const merged: CSSProperties = {
49
49
  ...baseStyle,
50
50
  ...(isSelected ? selectedStyle : null),
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { type CSSProperties } from 'react';
3
+ import { type CSSProperties, type ReactElement } from 'react';
4
4
  import type { DragRect } from '../hooks/useMultiSelect';
5
5
 
6
6
  export interface MultiSelectDragOverlayProps {
@@ -22,7 +22,7 @@ export function MultiSelectDragOverlay({
22
22
  rect,
23
23
  className,
24
24
  style,
25
- }: MultiSelectDragOverlayProps): JSX.Element | null {
25
+ }: MultiSelectDragOverlayProps): ReactElement | null {
26
26
  if (!rect) return null;
27
27
  if (rect.width === 0 && rect.height === 0) return null;
28
28
 
@@ -9,7 +9,7 @@
9
9
  * export default defineNotisApp({
10
10
  * name: 'My App',
11
11
  * description: 'Does things',
12
- * icon: 'lucide:layout-dashboard',
12
+ * icon: 'phosphor:squares-four',
13
13
  * databases: ['tasks'],
14
14
  * routes: [
15
15
  * {
@@ -8,7 +8,6 @@ interface UseDatabaseOptions {
8
8
  filter?: QueryFilter;
9
9
  pageSize?: number;
10
10
  offset?: number;
11
- /** Set to false to skip the initial fetch. */
12
11
  enabled?: boolean;
13
12
  }
14
13
 
@@ -19,16 +18,6 @@ interface UseDatabaseResult {
19
18
  refetch: () => void;
20
19
  }
21
20
 
22
- /**
23
- * Query documents from a Notis database owned by this app.
24
- *
25
- * ```tsx
26
- * const { documents, loading } = useDatabase('transactions', {
27
- * filter: { sorts: [{ property: 'date', direction: 'desc' }] },
28
- * pageSize: 20,
29
- * });
30
- * ```
31
- */
32
21
  export function useDatabase(
33
22
  databaseSlug: string,
34
23
  options: UseDatabaseOptions = {},
@@ -43,7 +32,7 @@ export function useDatabase(
43
32
  const filterKey = JSON.stringify(options.filter ?? null);
44
33
 
45
34
  const refetch = useCallback(() => {
46
- setFetchKey((k) => k + 1);
35
+ setFetchKey((key) => key + 1);
47
36
  }, []);
48
37
 
49
38
  useEffect(() => {
@@ -81,7 +70,6 @@ export function useDatabase(
81
70
  return () => {
82
71
  cancelled = true;
83
72
  };
84
- // eslint-disable-next-line react-hooks/exhaustive-deps
85
73
  }, [runtime, databaseSlug, fetchKey, enabled, filterKey, options.offset, options.pageSize]);
86
74
 
87
75
  return { documents, loading, error, refetch };
@@ -149,7 +149,7 @@ export function useMultiSelect<T>(opts: UseMultiSelectOptions<T>): MultiSelectCo
149
149
 
150
150
  const select = useCallback((ids: string[]) => {
151
151
  setSelectedIds(new Set(ids));
152
- setLastClickedId(ids.length > 0 ? ids[ids.length - 1] : null);
152
+ setLastClickedId(ids.length > 0 ? ids[ids.length - 1] ?? null : null);
153
153
  }, []);
154
154
 
155
155
  const clear = useCallback(() => {
@@ -159,7 +159,7 @@ export function useMultiSelect<T>(opts: UseMultiSelectOptions<T>): MultiSelectCo
159
159
  const selectAll = useCallback(() => {
160
160
  const all = orderedIdsRef.current;
161
161
  setSelectedIds(new Set(all));
162
- setLastClickedId(all.length > 0 ? all[all.length - 1] : null);
162
+ setLastClickedId(all.length > 0 ? all[all.length - 1] ?? null : null);
163
163
  }, []);
164
164
 
165
165
  const selectRange = useCallback((anchorId: string, targetId: string) => {
@@ -346,7 +346,7 @@ export function useMultiSelect<T>(opts: UseMultiSelectOptions<T>): MultiSelectCo
346
346
  bottom: rect.top + rect.height,
347
347
  });
348
348
  setSelectedIds(new Set(ids));
349
- setLastClickedId(ids.length > 0 ? ids[ids.length - 1] : null);
349
+ setLastClickedId(ids.length > 0 ? ids[ids.length - 1] ?? null : null);
350
350
  };
351
351
 
352
352
  const handleMouseUp = () => {
@@ -447,6 +447,7 @@ export function useMultiSelect<T>(opts: UseMultiSelectOptions<T>): MultiSelectCo
447
447
  : Math.min(all.length - 1, Math.max(0, currentIdx + direction));
448
448
  if (newIdx === currentIdx) return;
449
449
  const newHead = all[newIdx];
450
+ if (!newHead) return;
450
451
  const anchorId = lastClickedIdRef.current ?? currentHead ?? newHead;
451
452
  const anchorIdx = all.indexOf(anchorId);
452
453
  if (anchorIdx === -1) {
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useNotisRuntime } from '../provider';
4
- import type { AppDescriptor, RouteDescriptor, DatabaseDescriptor, CollectionItemDetail } 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. */
@@ -17,8 +17,9 @@ interface NotisContext {
17
17
  }
18
18
 
19
19
  /**
20
- * Access app-level metadata: the app descriptor, current route, and declared
21
- * databases. 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.
22
23
  */
23
24
  export function useNotis(): NotisContext {
24
25
  const runtime = useNotisRuntime();
@@ -7,7 +7,7 @@ interface NavigationActions {
7
7
  /** Navigate to a route within the app by its path. */
8
8
  toRoute: (path: string) => void;
9
9
  /** Navigate to a document detail view. */
10
- toDocument: (documentId: string) => void;
10
+ toDocument: (documentId: string, title?: string | null) => void;
11
11
  /** Navigate to the app's default route. */
12
12
  toApp: () => void;
13
13
  }
@@ -33,9 +33,9 @@ export function useNotisNavigation(): NavigationActions {
33
33
  }
34
34
  }, [runtime]);
35
35
 
36
- const toDocument = useCallback((documentId: string) => {
36
+ const toDocument = useCallback((documentId: string, title?: string | null) => {
37
37
  if (runtime?.navigate) {
38
- runtime.navigate({ kind: 'document', documentId });
38
+ runtime.navigate({ kind: 'document', documentId, title: title ?? undefined });
39
39
  }
40
40
  }, [runtime]);
41
41