@notis_ai/cli 0.2.5 → 0.2.6

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 (106) hide show
  1. package/README.md +31 -9
  2. package/dist/scaffolds/notis-database/CHANGELOG.md +5 -0
  3. package/dist/scaffolds/notis-database/notis.config.ts +0 -1
  4. package/dist/scaffolds/notis-database/src/mock-runtime.ts +1 -0
  5. package/dist/scaffolds/notis-journal/CHANGELOG.md +25 -0
  6. package/dist/scaffolds/notis-journal/app/globals.css +37 -0
  7. package/dist/scaffolds/notis-journal/app/insights/page.tsx +513 -0
  8. package/dist/scaffolds/notis-journal/app/journal-core.tsx +362 -0
  9. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +337 -0
  10. package/dist/scaffolds/notis-journal/app/layout.tsx +6 -0
  11. package/dist/scaffolds/notis-journal/app/page.tsx +485 -0
  12. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +28 -0
  13. package/dist/scaffolds/notis-journal/components/ui/button.tsx +53 -0
  14. package/dist/scaffolds/notis-journal/components/ui/card.tsx +56 -0
  15. package/dist/scaffolds/notis-journal/components.json +20 -0
  16. package/dist/scaffolds/notis-journal/index.html +12 -0
  17. package/dist/scaffolds/notis-journal/lib/utils.ts +6 -0
  18. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  19. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  20. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  21. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  22. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  23. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  24. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +132 -0
  25. package/dist/scaffolds/notis-journal/notis.config.ts +93 -0
  26. package/dist/scaffolds/notis-journal/package.json +34 -0
  27. package/dist/scaffolds/notis-journal/packages/sdk/package.json +36 -0
  28. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  29. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +60 -0
  30. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  31. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  32. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  33. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +145 -0
  34. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +229 -0
  35. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +41 -0
  36. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  37. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +78 -0
  38. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +121 -0
  39. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  40. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +34 -0
  41. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  42. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +64 -0
  43. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +56 -0
  44. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  45. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  46. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +83 -0
  47. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +43 -0
  48. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +220 -0
  49. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +186 -0
  50. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +15 -0
  51. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +56 -0
  52. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +15 -0
  53. package/dist/scaffolds/notis-journal/postcss.config.mjs +8 -0
  54. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +120 -0
  55. package/dist/scaffolds/notis-journal/src/dev-main.tsx +58 -0
  56. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +197 -0
  57. package/dist/scaffolds/notis-journal/tailwind.config.ts +58 -0
  58. package/dist/scaffolds/notis-journal/tsconfig.json +23 -0
  59. package/dist/scaffolds/notis-journal/vite.config.ts +10 -0
  60. package/dist/scaffolds/notis-notes/CHANGELOG.md +5 -0
  61. package/dist/scaffolds/notis-notes/app/page.tsx +17 -373
  62. package/dist/scaffolds/notis-notes/notis.config.ts +0 -1
  63. package/dist/scaffolds/notis-random/CHANGELOG.md +5 -0
  64. package/dist/scaffolds/notis-random/notis.config.ts +0 -1
  65. package/dist/scaffolds/notis-random/src/mock-runtime.ts +1 -0
  66. package/dist/scaffolds.json +11 -0
  67. package/package.json +3 -3
  68. package/skills/notis-apps/SKILL.md +43 -5
  69. package/skills/notis-apps/cli.md +22 -6
  70. package/skills/notis-cli/SKILL.md +20 -2
  71. package/skills/notis-query/cli.md +1 -1
  72. package/src/command-specs/apps.js +307 -36
  73. package/src/command-specs/index.js +1 -1
  74. package/src/command-specs/meta.js +8 -2
  75. package/src/command-specs/tools.js +50 -37
  76. package/src/runtime/agent-browser.js +204 -21
  77. package/src/runtime/app-changelog.js +79 -0
  78. package/src/runtime/app-dev-server.js +21 -4
  79. package/src/runtime/app-dev-sessions.js +99 -1
  80. package/src/runtime/app-platform.js +197 -18
  81. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  82. package/src/runtime/desktop-auth.js +93 -0
  83. package/src/runtime/profiles.js +37 -15
  84. package/src/runtime/store-screenshot.js +138 -0
  85. package/src/runtime/transport.js +21 -82
  86. package/template/.harness/index.html.tmpl +128 -6
  87. package/template/CHANGELOG.md +5 -0
  88. package/template/app/page.tsx +11 -41
  89. package/template/notis.config.ts +0 -1
  90. package/template/package-lock.json +4137 -0
  91. package/template/package.json +1 -0
  92. package/template/packages/sdk/package.json +4 -0
  93. package/template/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  94. package/template/packages/sdk/src/components/Markdown.tsx +60 -0
  95. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
  96. package/template/packages/sdk/src/config.ts +74 -0
  97. package/template/packages/sdk/src/documents.ts +229 -0
  98. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  99. package/template/packages/sdk/src/hooks/useDocument.ts +78 -0
  100. package/template/packages/sdk/src/hooks/useDocuments.ts +121 -0
  101. package/template/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
  102. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +54 -9
  103. package/template/packages/sdk/src/index.ts +30 -1
  104. package/template/packages/sdk/src/runtime.ts +76 -17
  105. package/template/packages/sdk/src/styles.css +148 -0
  106. package/template/packages/sdk/src/hooks/useDatabase.ts +0 -76
@@ -3,11 +3,11 @@ import { createReadStream } from 'node:fs';
3
3
  import { CliError, EXIT_CODES } from './errors.js';
4
4
  import {
5
5
  DEFAULT_PROFILE,
6
- ensureProfile,
7
6
  getProfile,
7
+ isJwtExpired,
8
8
  loadConfig,
9
- saveConfig,
10
9
  } from './profiles.js';
10
+ import { createExpiredAuthError, createInvalidAuthHints } from './desktop-auth.js';
11
11
 
12
12
  function escapeMultipartHeaderValue(value) {
13
13
  return String(value ?? '')
@@ -76,7 +76,7 @@ function createMultipartFileUpload(payload, bindingMetadata, fileBindings) {
76
76
  };
77
77
  }
78
78
 
79
- function normalizeBackendError(status, payload) {
79
+ function normalizeBackendError(status, payload, runtime) {
80
80
  const backendError = payload?.error;
81
81
  const message =
82
82
  backendError?.message ||
@@ -91,10 +91,7 @@ function normalizeBackendError(status, payload) {
91
91
  message,
92
92
  exitCode: EXIT_CODES.auth,
93
93
  hints: [
94
- {
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',
97
- },
94
+ ...createInvalidAuthHints(runtime),
98
95
  ],
99
96
  details: payload || {},
100
97
  });
@@ -145,46 +142,10 @@ function normalizeBackendError(status, payload) {
145
142
  });
146
143
  }
147
144
 
148
- function shouldAttemptDevPortalRefresh(runtime, { force = false } = {}) {
149
- if (runtime.authMode !== 'dev_portal' || !runtime.refreshToken || !runtime.apiBase) {
150
- return false;
151
- }
152
-
153
- if (force) {
154
- return true;
155
- }
156
-
157
- if (typeof runtime.accessExpiresAt !== 'number' || runtime.accessExpiresAt <= 0) {
145
+ function reloadJwtFromConfig(runtime) {
146
+ if (runtime.credentialSource === 'env') {
158
147
  return false;
159
148
  }
160
-
161
- const thresholdSeconds = 60;
162
- return runtime.accessExpiresAt - Math.floor(Date.now() / 1000) <= thresholdSeconds;
163
- }
164
-
165
- function persistDevPortalRuntimeAuth(runtime, session) {
166
- const config = ensureProfile(loadConfig(), runtime.profileName || DEFAULT_PROFILE);
167
- const profileName = runtime.profileName || DEFAULT_PROFILE;
168
- config.current_profile = profileName;
169
- config.profiles[profileName] = {
170
- ...config.profiles[profileName],
171
- jwt: session.access_token,
172
- api_base: runtime.apiBase,
173
- auth_mode: 'dev_portal',
174
- refresh_token: session.refresh_token,
175
- access_expires_at: session.access_expires_at,
176
- refresh_expires_at: session.refresh_expires_at,
177
- };
178
- saveConfig(config);
179
-
180
- runtime.jwt = session.access_token;
181
- runtime.authMode = 'dev_portal';
182
- runtime.refreshToken = session.refresh_token;
183
- runtime.accessExpiresAt = session.access_expires_at;
184
- runtime.refreshExpiresAt = session.refresh_expires_at;
185
- }
186
-
187
- function reloadJwtFromConfig(runtime) {
188
149
  const profileName = runtime.profileName || DEFAULT_PROFILE;
189
150
  let profile;
190
151
  try {
@@ -197,39 +158,8 @@ function reloadJwtFromConfig(runtime) {
197
158
  return false;
198
159
  }
199
160
  runtime.jwt = nextJwt;
200
- runtime.authMode = profile.auth_mode;
201
- runtime.refreshToken = profile.refresh_token;
202
- runtime.accessExpiresAt = profile.access_expires_at;
203
- runtime.refreshExpiresAt = profile.refresh_expires_at;
204
- return true;
205
- }
206
-
207
- async function maybeRefreshDevPortalAuth(runtime, { force = false } = {}) {
208
- if (!shouldAttemptDevPortalRefresh(runtime, { force })) {
209
- return false;
210
- }
211
-
212
- const response = await fetch(`${runtime.apiBase}/portal_auth/dev-refresh`, {
213
- method: 'POST',
214
- headers: {
215
- 'Content-Type': 'application/json',
216
- 'X-Notis-CLI-Version': runtime.cliVersion,
217
- },
218
- body: JSON.stringify({ refresh_token: runtime.refreshToken }),
219
- });
220
-
221
- let payload = null;
222
- try {
223
- payload = await response.json();
224
- } catch {
225
- payload = null;
226
- }
227
-
228
- if (!response.ok || !payload?.session?.access_token || !payload?.session?.refresh_token) {
229
- throw normalizeBackendError(response.status, payload);
230
- }
231
-
232
- persistDevPortalRuntimeAuth(runtime, payload.session);
161
+ runtime.desktopAppName = profile.desktop_app_name;
162
+ runtime.desktopPid = profile.desktop_pid;
233
163
  return true;
234
164
  }
235
165
 
@@ -241,7 +171,14 @@ export async function httpRequest({
241
171
  multipart = false,
242
172
  requireAuth = true,
243
173
  }) {
244
- await maybeRefreshDevPortalAuth(runtime);
174
+ // The desktop renderer is the single owner of the rotating Supabase refresh
175
+ // token. Each CLI request only consumes the newest access token it synced to
176
+ // disk, avoiding refresh-token races between independent CLI processes and
177
+ // the running desktop session.
178
+ reloadJwtFromConfig(runtime);
179
+ if (requireAuth && isJwtExpired(runtime.jwt)) {
180
+ throw createExpiredAuthError(runtime);
181
+ }
245
182
 
246
183
  let controller = new AbortController();
247
184
  let timeout = setTimeout(() => controller.abort(), runtime.timeoutMs);
@@ -298,8 +235,7 @@ export async function httpRequest({
298
235
  }
299
236
 
300
237
  if (response.status === 401) {
301
- const refreshed = (await maybeRefreshDevPortalAuth(runtime, { force: true }))
302
- || reloadJwtFromConfig(runtime);
238
+ const refreshed = reloadJwtFromConfig(runtime) && !isJwtExpired(runtime.jwt);
303
239
  if (refreshed) {
304
240
  if (requireAuth && runtime.jwt) {
305
241
  headers.Authorization = `Bearer ${runtime.jwt}`;
@@ -324,7 +260,10 @@ export async function httpRequest({
324
260
  clearTimeout(timeout);
325
261
 
326
262
  if (!response.ok) {
327
- throw normalizeBackendError(response.status, payload);
263
+ if (response.status === 401 && isJwtExpired(runtime.jwt)) {
264
+ throw createExpiredAuthError(runtime);
265
+ }
266
+ throw normalizeBackendError(response.status, payload, runtime);
328
267
  }
329
268
 
330
269
  return {
@@ -3,6 +3,11 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <title>Notis App Harness</title>
6
+ <script>
7
+ const harnessTheme = new URLSearchParams(window.location.search).get('theme') === 'dark' ? 'dark' : 'light';
8
+ document.documentElement.classList.toggle('dark', harnessTheme === 'dark');
9
+ document.documentElement.dataset.theme = harnessTheme;
10
+ </script>
6
11
  <link rel="stylesheet" href="./bundle/app.css">
7
12
  <script type="importmap">
8
13
  {
@@ -14,8 +19,80 @@
14
19
  }
15
20
  }
16
21
  </script>
22
+ <link rel="preconnect" href="https://fonts.googleapis.com">
23
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
24
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
17
25
  <style>
18
- body { margin: 0; font-family: system-ui, sans-serif; }
26
+ /*
27
+ * Portal-parity theme tokens. In production the portal copies its computed
28
+ * :root custom properties onto the app root, so app CSS written against
29
+ * hsl(var(--token)) resolves to the real portal palette. The harness mirrors
30
+ * the portal's light theme here so dev renders and listing screenshots look
31
+ * exactly like the app inside the portal. Keep in sync with
32
+ * portal/src/app/globals.css (:root).
33
+ */
34
+ :root {
35
+ color-scheme: light;
36
+ --background: 0 0% 100%;
37
+ --foreground: 222.2 84% 4.9%;
38
+ --card: 0 0% 100%;
39
+ --card-foreground: 222.2 84% 4.9%;
40
+ --popover: 0 0% 100%;
41
+ --popover-foreground: 222.2 84% 4.9%;
42
+ --primary: 222.2 47.4% 11.2%;
43
+ --primary-foreground: 210 40% 98%;
44
+ --secondary: 210 40% 96.1%;
45
+ --secondary-foreground: 222.2 47.4% 11.2%;
46
+ --muted: 210 40% 96.1%;
47
+ --muted-foreground: 0 0% 46.9%;
48
+ --accent: 210 40% 96.1%;
49
+ --accent-foreground: 222.2 47.4% 11.2%;
50
+ --destructive: 0 84.2% 60.2%;
51
+ --destructive-foreground: 210 40% 98%;
52
+ --border: 214.3 31.8% 91.4%;
53
+ --input: 214.3 31.8% 91.4%;
54
+ --ring: 222.2 84% 4.9%;
55
+ --radius: 0.5rem;
56
+ --chart-1: 210 100% 49%;
57
+ --chart-2: 173 58% 39%;
58
+ --chart-3: 197 37% 24%;
59
+ --chart-4: 43 74% 66%;
60
+ --chart-5: 27 87% 67%;
61
+ }
62
+ :root.dark {
63
+ color-scheme: dark;
64
+ --background: 0 0% 13%;
65
+ --foreground: 210 40% 98%;
66
+ --card: 0 0% 13%;
67
+ --card-foreground: 210 40% 98%;
68
+ --popover: 0 0% 13%;
69
+ --popover-foreground: 210 40% 98%;
70
+ --primary: 210 40% 98%;
71
+ --primary-foreground: 222.2 47.4% 11.2%;
72
+ --secondary: 0 0% 18%;
73
+ --secondary-foreground: 210 40% 98%;
74
+ --muted: 0 0% 18%;
75
+ --muted-foreground: 0 0% 65.1%;
76
+ --accent: 0 0% 18%;
77
+ --accent-foreground: 210 40% 98%;
78
+ --destructive: 0 84.2% 60.2%;
79
+ --destructive-foreground: 210 40% 98%;
80
+ --border: 0 0% 23.5%;
81
+ --input: 0 0% 23.5%;
82
+ --ring: 212.7 26.8% 83.9%;
83
+ --chart-1: 220 70% 50%;
84
+ --chart-2: 160 60% 45%;
85
+ --chart-3: 30 80% 55%;
86
+ --chart-4: 280 65% 60%;
87
+ --chart-5: 340 75% 55%;
88
+ }
89
+ body {
90
+ margin: 0;
91
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
92
+ background: hsl(var(--background));
93
+ color: hsl(var(--foreground));
94
+ -webkit-font-smoothing: antialiased;
95
+ }
19
96
  #harness-status { position: fixed; top: 0; left: 0; right: 0; padding: 4px 8px; font-size: 11px; background: #111; color: #0f0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; z-index: 9999; }
20
97
  #root { padding-top: 24px; min-height: 100vh; }
21
98
  </style>
@@ -29,6 +106,7 @@
29
106
  const mode = {{MODE}};
30
107
  const apiBase = {{API_BASE}};
31
108
  const jwt = {{JWT}};
109
+ const fixtures = {{FIXTURES}};
32
110
  const status = document.getElementById('harness-status');
33
111
  const setStatus = (msg, color = '#0f0') => {
34
112
  status.textContent = `harness: ${msg}`;
@@ -42,6 +120,7 @@
42
120
  mounted: false,
43
121
  mode,
44
122
  route: descriptor.route && descriptor.route.slug,
123
+ scenario: descriptor.context && descriptor.context.screenshotScenario,
45
124
  };
46
125
 
47
126
  window.addEventListener('error', (event) => {
@@ -118,15 +197,48 @@
118
197
  },
119
198
  callTool: async (name, args) => {
120
199
  record('callTool', { name, arguments: args || {} });
200
+ if (fixtures && fixtures.tools && Object.prototype.hasOwnProperty.call(fixtures.tools, name)) {
201
+ return structuredClone(fixtures.tools[name]);
202
+ }
121
203
  return { ok: true, result: null };
122
204
  },
123
205
  request: async (path, options) => {
124
206
  record('request', { path, options });
207
+ if (fixtures && fixtures.requests && Object.prototype.hasOwnProperty.call(fixtures.requests, path)) {
208
+ return structuredClone(fixtures.requests[path]);
209
+ }
125
210
  return null;
126
211
  },
127
212
  };
128
213
  }
129
214
 
215
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
216
+
217
+ async function applyScenarioActions() {
218
+ const actions = fixtures && fixtures.scenario && Array.isArray(fixtures.scenario.actions)
219
+ ? fixtures.scenario.actions
220
+ : [];
221
+ for (const action of actions) {
222
+ if (action && action.type === 'wait') {
223
+ await delay(Math.max(0, Number(action.ms) || 0));
224
+ continue;
225
+ }
226
+ if (action && action.type === 'click' && typeof action.selector === 'string') {
227
+ const deadline = Date.now() + 5000;
228
+ let element = null;
229
+ while (!element && Date.now() < deadline) {
230
+ element = document.querySelector(action.selector);
231
+ if (!element) await delay(50);
232
+ }
233
+ if (!element || typeof element.click !== 'function') {
234
+ throw new Error(`Screenshot scenario could not click ${action.selector}`);
235
+ }
236
+ element.click();
237
+ await delay(Number(action.settle_ms) || 350);
238
+ }
239
+ }
240
+ }
241
+
130
242
  function liveRuntime() {
131
243
  return {
132
244
  ...stubRuntime(),
@@ -192,11 +304,21 @@
192
304
  window.__harness.renderStarted = true;
193
305
  createRoot(document.getElementById('root')).render(React.createElement(Root));
194
306
 
195
- requestAnimationFrame(() => requestAnimationFrame(() => {
196
- window.__harness.mounted = true;
197
- const errors = window.__harness.errors.length;
198
- const calls = window.__harness.runtimeCalls.length;
199
- setStatus(`mounted (errors=${errors}, runtimeCalls=${calls})`, errors ? '#f55' : '#0f0');
307
+ requestAnimationFrame(() => requestAnimationFrame(async () => {
308
+ try {
309
+ await delay(500);
310
+ await applyScenarioActions();
311
+ window.__harness.mounted = true;
312
+ const errors = window.__harness.errors.length;
313
+ const calls = window.__harness.runtimeCalls.length;
314
+ setStatus(`mounted (errors=${errors}, runtimeCalls=${calls})`, errors ? '#f55' : '#0f0');
315
+ } catch (error) {
316
+ window.__harness.errors.push({
317
+ type: 'scenario',
318
+ message: error && error.message ? error.message : String(error),
319
+ });
320
+ setStatus(`scenario failed: ${error && error.message ? error.message : String(error)}`, '#f55');
321
+ }
200
322
  }));
201
323
  } catch (error) {
202
324
  window.__harness.errors.push({
@@ -0,0 +1,5 @@
1
+ # App Changelog
2
+
3
+ ## [Initial Release] - {PR_MERGE_DATE}
4
+
5
+ - First Store release.
@@ -1,47 +1,14 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useState } from 'react';
4
- import { useNotis, useTool } from '@notis/sdk';
3
+ import { getDocumentPreview, useDocuments, useNotis } from '@notis/sdk';
5
4
  import { Badge } from '@/components/ui/badge';
6
5
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
7
6
 
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
-
26
7
  export default function HomePage() {
27
8
  const { app, ready } = useNotis();
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]);
9
+ // Documents come back normalized: plain property values, camelCase fields,
10
+ // and typed content (contentMarkdown / contentBlocknote / plainText).
11
+ const { documents, loading } = useDocuments('items', { pageSize: 25 });
45
12
 
46
13
  return (
47
14
  <main className="notis-app-shell space-y-6">
@@ -63,7 +30,7 @@ export default function HomePage() {
63
30
  <CardDescription>Use shadcn surfaces and portal tokens so the app feels native inside Notis.</CardDescription>
64
31
  </CardHeader>
65
32
  <CardContent>
66
- {queryItems.loading ? (
33
+ {loading ? (
67
34
  <p className="text-sm text-muted-foreground">Loading...</p>
68
35
  ) : documents.length === 0 ? (
69
36
  <div className="rounded-xl border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground">
@@ -72,13 +39,16 @@ export default function HomePage() {
72
39
  ) : (
73
40
  <div className="space-y-3">
74
41
  {documents.map((doc) => (
75
- <div key={doc.id || doc.document_id || doc.title} className="rounded-xl border border-border bg-background px-4 py-3">
42
+ <div key={doc.id} className="rounded-xl border border-border bg-background px-4 py-3">
76
43
  <div className="flex items-center justify-between gap-3">
77
44
  <p className="font-medium">{doc.title || 'Untitled'}</p>
78
- {doc.properties?.status ? (
79
- <Badge variant="outline">{String(doc.properties.status)}</Badge>
45
+ {typeof doc.properties.status === 'string' ? (
46
+ <Badge variant="outline">{doc.properties.status}</Badge>
80
47
  ) : null}
81
48
  </div>
49
+ <p className="mt-1 line-clamp-1 text-xs text-muted-foreground">
50
+ {getDocumentPreview(doc)}
51
+ </p>
82
52
  </div>
83
53
  ))}
84
54
  </div>
@@ -8,7 +8,6 @@ export default defineNotisApp({
8
8
  author: { name: 'Notis User' },
9
9
  categories: ['Productivity'],
10
10
  tagline: 'A clean starting point for a focused workflow.',
11
- versionNotes: 'Initial release.',
12
11
 
13
12
  databases: ['items'],
14
13