@notis_ai/cli 0.2.0-beta.19.1 → 0.2.0-beta.196.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 (216) hide show
  1. package/README.md +822 -135
  2. package/bin/check-runtime.js +15 -0
  3. package/bin/notis.js +2 -0
  4. package/config/notis_app_boundary_rules.json +50 -0
  5. package/config/notis_app_design_rules.json +135 -0
  6. package/config/portal-route-roots.json +48 -0
  7. package/dist/agent-hooks/notis-agent-hook.mjs +45587 -0
  8. package/dist/base-skills/notis-apps/SKILL.md +93 -0
  9. package/dist/base-skills/notis-apps/references/architecture.md +114 -0
  10. package/dist/base-skills/notis-apps/references/context.md +81 -0
  11. package/dist/base-skills/notis-apps/references/design.md +97 -0
  12. package/dist/base-skills/notis-apps/references/memory.md +58 -0
  13. package/dist/base-skills/notis-apps/references/reading.md +67 -0
  14. package/dist/base-skills/notis-apps/references/release.md +107 -0
  15. package/dist/base-skills/notis-apps/references/sdk.md +116 -0
  16. package/dist/base-skills/notis-apps/references/troubleshooting.md +24 -0
  17. package/dist/base-skills/notis-apps/references/views.md +142 -0
  18. package/dist/base-skills/notis-cli/SKILL.md +154 -0
  19. package/dist/base-skills/notis-cli/references/app-delivery.md +19 -0
  20. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  21. package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
  22. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  23. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  24. package/dist/base-skills/notis-query/SKILL.md +41 -0
  25. package/dist/base-skills/notis-query/references/database-discovery.md +60 -0
  26. package/dist/base-skills/notis-query/references/documents.md +63 -0
  27. package/dist/base-skills/notis-query/references/query.md +76 -0
  28. package/{template/packages/notis-sdk → dist/sdk}/package.json +14 -4
  29. package/dist/sdk/src/agentContext.ts +36 -0
  30. package/dist/sdk/src/bundleModule.ts +79 -0
  31. package/dist/sdk/src/components/Dialog.tsx +46 -0
  32. package/dist/sdk/src/components/DocumentEditor.tsx +106 -0
  33. package/dist/sdk/src/components/DocumentPage.tsx +141 -0
  34. package/dist/sdk/src/components/Markdown.tsx +60 -0
  35. package/dist/sdk/src/components/MarkdownEditor.tsx +121 -0
  36. package/dist/sdk/src/components/MultiSelectActionBar.tsx +285 -0
  37. package/dist/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
  38. package/dist/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  39. package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  40. package/dist/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
  41. package/dist/sdk/src/components/RecordComponents.tsx +33 -0
  42. package/dist/sdk/src/components/RenderChartData.tsx +19 -0
  43. package/dist/sdk/src/components/ShortcutHints.tsx +56 -0
  44. package/dist/sdk/src/components/Skeleton.tsx +24 -0
  45. package/dist/sdk/src/config.ts +239 -0
  46. package/dist/sdk/src/documents.ts +258 -0
  47. package/dist/sdk/src/hooks/useAction.ts +28 -0
  48. package/dist/sdk/src/hooks/useActiveResource.ts +19 -0
  49. package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
  50. package/dist/sdk/src/hooks/useCloudComputer.ts +95 -0
  51. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  52. package/dist/sdk/src/hooks/useCollectionInteractions.ts +838 -0
  53. package/dist/sdk/src/hooks/useDatabaseSchema.ts +49 -0
  54. package/dist/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  55. package/dist/sdk/src/hooks/useDocument.ts +43 -0
  56. package/dist/sdk/src/hooks/useDocuments.ts +84 -0
  57. package/dist/sdk/src/hooks/useHandover.ts +78 -0
  58. package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
  59. package/dist/sdk/src/hooks/useMultiSelect.ts +95 -0
  60. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  61. package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
  62. package/dist/sdk/src/hooks/useNotis.ts +42 -0
  63. package/dist/sdk/src/hooks/useNotisNavigation.ts +67 -0
  64. package/dist/sdk/src/hooks/useQuery.ts +74 -0
  65. package/dist/sdk/src/hooks/useTool.ts +65 -0
  66. package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
  67. package/dist/sdk/src/hooks/useTopBarSearch.ts +81 -0
  68. package/dist/sdk/src/hooks/useUpsertDocument.ts +95 -0
  69. package/dist/sdk/src/hooks/useViewParams.ts +121 -0
  70. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  71. package/dist/sdk/src/index.ts +200 -0
  72. package/dist/sdk/src/interactions/actions.ts +59 -0
  73. package/dist/sdk/src/interactions/shortcuts.tsx +740 -0
  74. package/dist/sdk/src/interactions/visibility.ts +13 -0
  75. package/dist/sdk/src/interactions.ts +45 -0
  76. package/dist/sdk/src/nativeData.ts +39 -0
  77. package/dist/sdk/src/presentation.tsx +82 -0
  78. package/dist/sdk/src/provider.tsx +49 -0
  79. package/dist/sdk/src/queryCache.ts +172 -0
  80. package/dist/sdk/src/queryErrors.ts +51 -0
  81. package/dist/sdk/src/runtime.ts +603 -0
  82. package/dist/sdk/src/space.ts +425 -0
  83. package/dist/sdk/src/styles.css +290 -0
  84. package/dist/sdk/src/tailwind.ts +66 -0
  85. package/dist/sdk/src/vite.ts +83 -0
  86. package/dist/skill-sync/index.js +2143 -0
  87. package/dist/skill-sync/index.js.map +7 -0
  88. package/dist/skill-sync-worker.mjs +3487 -0
  89. package/dist/space-harness/host.js +66 -0
  90. package/dist/view-renderer/dist/host.css +1 -0
  91. package/dist/view-renderer/dist/host.js +1042 -0
  92. package/dist/view-renderer/dist/theme.css +73 -0
  93. package/dist/view-renderer/src/dom.js +180 -0
  94. package/dist/view-renderer/src/errors.js +8 -0
  95. package/dist/view-renderer/src/host.tsx +173 -0
  96. package/dist/view-renderer/src/index.js +4 -0
  97. package/dist/view-renderer/src/pool.js +76 -0
  98. package/dist/view-renderer/src/remote-broker.js +59 -0
  99. package/dist/view-renderer/src/renderer.js +201 -0
  100. package/package.json +25 -8
  101. package/skills/notis-apps/cli.md +406 -0
  102. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  103. package/skills/notis-onboarding/BRIEF.md +133 -0
  104. package/skills/notis-query/cli.md +39 -0
  105. package/src/agent-hook-entry.js +5 -0
  106. package/src/cli.js +294 -25
  107. package/src/command-specs/agents.js +392 -0
  108. package/src/command-specs/apps.js +1476 -203
  109. package/src/command-specs/auth.js +114 -137
  110. package/src/command-specs/diagnostics.js +730 -0
  111. package/src/command-specs/files.js +29 -0
  112. package/src/command-specs/handover.js +374 -0
  113. package/src/command-specs/helpers.js +86 -82
  114. package/src/command-specs/index.js +31 -6
  115. package/src/command-specs/meta.js +150 -18
  116. package/src/command-specs/onboarding.js +290 -0
  117. package/src/command-specs/profile.js +358 -0
  118. package/src/command-specs/skills.js +304 -0
  119. package/src/command-specs/smoke.js +386 -0
  120. package/src/command-specs/spaces.js +572 -0
  121. package/src/command-specs/tools.js +455 -139
  122. package/src/command-specs/views.js +69 -0
  123. package/src/runtime/agent-browser.js +700 -0
  124. package/src/runtime/agent-memory-state.js +126 -0
  125. package/src/runtime/agent-setup.js +383 -0
  126. package/src/runtime/app-boundary-validator.js +407 -0
  127. package/src/runtime/app-changelog.js +79 -0
  128. package/src/runtime/app-data-literals.js +82 -0
  129. package/src/runtime/app-platform.js +2752 -237
  130. package/src/runtime/app-registry-scaffolds.js +367 -0
  131. package/src/runtime/app-test-server.js +293 -0
  132. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  133. package/src/runtime/auth-recovery.js +110 -0
  134. package/src/runtime/base-skills.d.ts +20 -0
  135. package/src/runtime/base-skills.js +167 -0
  136. package/src/runtime/channel.js +133 -0
  137. package/src/runtime/confined-space-build.js +74 -0
  138. package/src/runtime/confined-space-verify.js +49 -0
  139. package/src/runtime/delegated-context.js +68 -0
  140. package/src/runtime/errors.js +1 -0
  141. package/src/runtime/git.js +233 -0
  142. package/src/runtime/login-listener.js +15 -0
  143. package/src/runtime/native-record-files.js +47 -0
  144. package/src/runtime/oauth.js +2622 -0
  145. package/src/runtime/output.js +37 -5
  146. package/src/runtime/ports.js +31 -0
  147. package/src/runtime/profiles.js +906 -55
  148. package/src/runtime/retired-sdk-files.js +20 -0
  149. package/src/runtime/screenshot-framing.js +85 -0
  150. package/src/runtime/skill-sync/cloud-client.ts +121 -0
  151. package/src/runtime/skill-sync/index.ts +905 -0
  152. package/src/runtime/skill-sync/local-scanner.ts +1259 -0
  153. package/src/runtime/skill-sync/native-identity.ts +102 -0
  154. package/src/runtime/skill-sync/native-plan.ts +59 -0
  155. package/src/runtime/skill-sync/symlink-manager.ts +433 -0
  156. package/src/runtime/skill-sync/sync-plan.ts +54 -0
  157. package/src/runtime/skill-sync/types.ts +157 -0
  158. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  159. package/src/runtime/skill-sync-service.js +109 -0
  160. package/src/runtime/source-root.js +32 -0
  161. package/src/runtime/space-collection-scaffold.js +66 -0
  162. package/src/runtime/space-database-structures.js +229 -0
  163. package/src/runtime/space-fixture-broker.js +50 -0
  164. package/src/runtime/space-lock.js +337 -0
  165. package/src/runtime/space-platform.js +105 -0
  166. package/src/runtime/space-source.js +281 -0
  167. package/src/runtime/space-test-server.js +327 -0
  168. package/src/runtime/space-verification.js +89 -0
  169. package/src/runtime/space-verify-browser-worker.js +51 -0
  170. package/src/runtime/space-view-manifest.js +254 -0
  171. package/src/runtime/store-screenshot.js +146 -0
  172. package/src/runtime/sync-skills.d.ts +37 -0
  173. package/src/runtime/sync-skills.js +231 -0
  174. package/src/runtime/telemetry.js +92 -0
  175. package/src/runtime/transport.js +331 -45
  176. package/src/runtime/view-render.js +49 -0
  177. package/src/skill-sync-worker-entry.js +2 -0
  178. package/src/skill-sync-worker.js +50 -0
  179. package/src/space-harness-host.ts +103 -0
  180. package/template/.harness/index.html.tmpl +435 -0
  181. package/template/CHANGELOG.md +5 -0
  182. package/template/app/globals.css +29 -3
  183. package/template/app/layout.tsx +6 -3
  184. package/template/app/page.tsx +49 -42
  185. package/template/components/page-heading.tsx +23 -0
  186. package/template/components/ui/badge.tsx +7 -4
  187. package/template/components/ui/button.tsx +1 -1
  188. package/template/components/ui/card.tsx +24 -11
  189. package/template/components/ui/native-select.tsx +24 -0
  190. package/template/notis.config.ts +24 -6
  191. package/template/package-lock.json +3642 -0
  192. package/template/package.json +19 -16
  193. package/template/postcss.config.mjs +1 -1
  194. package/template/tailwind.config.ts +1 -6
  195. package/template/tsconfig.json +1 -0
  196. package/src/command-specs/db.js +0 -163
  197. package/src/runtime/app-preview-server.js +0 -312
  198. package/template/packages/notis-sdk/src/config.ts +0 -48
  199. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  200. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  201. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  202. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  203. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  204. package/template/packages/notis-sdk/src/hooks/useNotis.ts +0 -31
  205. package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +0 -49
  206. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  207. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  208. package/template/packages/notis-sdk/src/index.ts +0 -47
  209. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  210. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  211. package/template/packages/notis-sdk/src/styles.css +0 -123
  212. package/template/packages/notis-sdk/src/vite.ts +0 -54
  213. package/template/packages/notis-sdk/tsconfig.json +0 -15
  214. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useBackend.ts +0 -0
  215. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useTools.ts +0 -0
  216. /package/{template/packages/notis-sdk → dist/sdk}/src/ui.ts +0 -0
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import { useNotisRuntime } from '../provider';
4
+ import type { RuntimeResource, AppDescriptor, CollectionItemDetail, DatabaseDescriptor, RouteDescriptor } from '../runtime';
5
+
6
+ interface NotisContext {
7
+ /** UI selection only; it grants no access and is never a record scope. */
8
+ selection: string | null;
9
+ resource: RuntimeResource | null;
10
+ /** App metadata (id, name, icon, description). Null before runtime loads. */
11
+ app: AppDescriptor | null;
12
+ /** Current route descriptor. Null before runtime loads. */
13
+ route: RouteDescriptor | null;
14
+ /** Databases declared by this app. Empty before runtime loads. */
15
+ databases: DatabaseDescriptor[];
16
+ /** Selected collection item for the current route, when applicable. */
17
+ collectionItem: CollectionItemDetail | null;
18
+ /** Resource requested through this route's canonical deep link. */
19
+ resourceId: string | null;
20
+ /** Whether the runtime is loaded and available. */
21
+ ready: boolean;
22
+ }
23
+
24
+ /**
25
+ * Access app-level metadata, the current route, declared databases, and the
26
+ * selected collection item. Returns safe defaults when the portal runtime is
27
+ * not available.
28
+ */
29
+ export function useNotis(): NotisContext {
30
+ const runtime = useNotisRuntime();
31
+
32
+ return {
33
+ selection: runtime?.context?.selection ?? null,
34
+ resource: runtime?.resource ?? null,
35
+ app: runtime?.app ?? null,
36
+ route: runtime?.route ?? null,
37
+ databases: runtime?.databases ?? [],
38
+ collectionItem: runtime?.context?.collectionItem ?? null,
39
+ resourceId: runtime?.context?.resourceId ?? null,
40
+ ready: runtime !== null,
41
+ };
42
+ }
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+ import { useNotisRuntime } from '../provider';
5
+
6
+ export interface SpaceNavigationOptions {
7
+ recordKey?: string | null;
8
+ /** Declared view parameters. The destination authorizes records and drops unknown names. */
9
+ params?: Record<string, string | number | boolean | null | undefined>;
10
+ selection?: string | null;
11
+ }
12
+
13
+ interface NavigationActions {
14
+ /** Resolve a declared portable destination under the current viewer's access. */
15
+ toNamedSpace: (alias: string, options?: SpaceNavigationOptions) => Promise<void>;
16
+ /** Open an exact Space, optionally at a native record's stable record_key. */
17
+ toSpace: (spaceId: string, options?: SpaceNavigationOptions) => void;
18
+ /** Navigate to a route within the app by its path. */
19
+ toRoute: (path: string, options?: { resourceId?: string | null }) => void;
20
+ /** Navigate to the app's default route. */
21
+ toApp: () => void;
22
+ }
23
+
24
+ /**
25
+ * Navigation helpers for Spaces and app routes.
26
+ * When rendered inside the portal, uses the runtime.navigate() bridge.
27
+ * Without a portal runtime, falls back to window.location for route navigation.
28
+ *
29
+ * ```tsx
30
+ * const nav = useNotisNavigation();
31
+ * nav.toSpace(spaceId, { recordKey });
32
+ * ```
33
+ */
34
+ export function useNotisNavigation(): NavigationActions {
35
+ const runtime = useNotisRuntime();
36
+
37
+ const toNamedSpace = useCallback(async (alias: string, options?: SpaceNavigationOptions) => {
38
+ if (runtime?.resource?.kind !== 'space' || !runtime.navigate) throw new Error('Open this Space in a current Notis host to navigate.');
39
+ await runtime.navigate({ kind: 'space-reference', alias, documentId: options?.recordKey ?? null, selection: options?.selection ?? null,
40
+ ...(options?.params ? { params: options.params } : {}) });
41
+ }, [runtime]);
42
+
43
+ const toSpace = useCallback((spaceId: string, options?: SpaceNavigationOptions) => {
44
+ if (!runtime?.navigate) throw new Error('Open this Space in a current Notis host to navigate.');
45
+ runtime.navigate({ kind: 'space', spaceId, documentId: options?.recordKey ?? null,
46
+ ...(options?.params ? { params: options.params } : {}), ...(options?.selection ? { selection: options.selection } : {}) });
47
+ }, [runtime]);
48
+
49
+ const toRoute = useCallback((path: string, options?: { resourceId?: string | null }) => {
50
+ if (runtime?.navigate) {
51
+ runtime.navigate({ kind: 'route', path, resourceId: options?.resourceId ?? null });
52
+ } else if (typeof window !== 'undefined') {
53
+ const url = new URL(path, window.location.href);
54
+ if (options?.resourceId) url.searchParams.set('resource', options.resourceId);
55
+ else url.searchParams.delete('resource');
56
+ window.location.href = url.toString();
57
+ }
58
+ }, [runtime]);
59
+
60
+ const toApp = useCallback(() => {
61
+ if (runtime?.navigate) {
62
+ runtime.navigate({ kind: 'app' });
63
+ }
64
+ }, [runtime]);
65
+
66
+ return { toNamedSpace, toSpace, toRoute, toApp };
67
+ }
@@ -0,0 +1,74 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';
4
+ import { useNotisRuntime } from '../provider';
5
+ import { createQueryClient, queryKey } from '../queryCache';
6
+ import { spaceQueryKey } from '../space';
7
+ import { localizeQueryError } from '../queryErrors';
8
+
9
+ export interface UseQueryOptions {
10
+ /** Required acknowledgement: only idempotent reads may run on mount or prefetch. */
11
+ readOnly: true;
12
+ enabled?: boolean;
13
+ staleTimeMs?: number;
14
+ }
15
+
16
+ export interface UseQueryResult<T> {
17
+ data: T | undefined;
18
+ hasData: boolean;
19
+ loading: boolean;
20
+ isFetching: boolean;
21
+ error: Error | null;
22
+ refetch: () => Promise<void>;
23
+ }
24
+
25
+ /** Cached read callback; scope and lifetime are provided by the host, never by app globals. */
26
+ export function useQuery<T>(key: readonly unknown[], read: () => Promise<T>, options: UseQueryOptions): UseQueryResult<T> {
27
+ const runtime = useNotisRuntime();
28
+ // Older hosts deliberately get a hook-local cache: no cross-runtime/account reuse.
29
+ const fallback = useMemo(() => createQueryClient(), [runtime]);
30
+ const client = runtime?.queryClient ?? fallback;
31
+ const cacheKey = queryKey(spaceQueryKey(runtime, key));
32
+ const enabled = Boolean(runtime) && options.enabled !== false && options.readOnly === true;
33
+ const readRef = useRef(read);
34
+ useLayoutEffect(() => { readRef.current = read; });
35
+ const subscribe = useCallback((listener: () => void) => enabled ? client.subscribe(cacheKey, listener) : () => {}, [cacheKey, client, enabled]);
36
+ const snapshot = useSyncExternalStore(subscribe, () => client.getSnapshot<T>(cacheKey), () => client.getSnapshot<T>(cacheKey));
37
+ const fetch = useCallback(() => {
38
+ const capturedRead = readRef.current;
39
+ return client.fetch(cacheKey, capturedRead, { staleTimeMs: options.staleTimeMs });
40
+ }, [cacheKey, client, options.staleTimeMs]);
41
+ useEffect(() => {
42
+ if (!enabled) return;
43
+ void fetch().catch(() => {}); // Error belongs to the snapshot; no unhandled rejection.
44
+ }, [enabled, fetch, snapshot.invalidation]);
45
+ const refetch = useCallback(async () => {
46
+ if (!enabled) return;
47
+ client.invalidate(cacheKey);
48
+ await fetch().catch(() => {});
49
+ }, [cacheKey, client, enabled, fetch]);
50
+ const error = useMemo(() => localizeQueryError(snapshot.error, runtime?.context?.locale), [snapshot.error, runtime?.context?.locale]);
51
+ return {
52
+ data: enabled ? snapshot.data : undefined,
53
+ hasData: enabled && snapshot.hasData,
54
+ loading: enabled && !snapshot.hasData && !snapshot.error,
55
+ isFetching: enabled && snapshot.isFetching,
56
+ error: enabled ? error : null,
57
+ refetch,
58
+ };
59
+ }
60
+
61
+ /** Opt-in preparation for small, known read queries. Never use for writes or full-database crawls. */
62
+ export function useQueryClient(): {
63
+ invalidate(key?: readonly unknown[]): void;
64
+ prefetch<T>(key: readonly unknown[], read: () => Promise<T>, options: UseQueryOptions): Promise<T | undefined>;
65
+ } {
66
+ const runtime = useNotisRuntime();
67
+ return useMemo(() => ({
68
+ invalidate: (key?: readonly unknown[]) => runtime?.queryClient?.invalidate(key ? queryKey(spaceQueryKey(runtime, key)) : undefined),
69
+ prefetch: <T,>(key: readonly unknown[], read: () => Promise<T>, options: UseQueryOptions) => {
70
+ if (!runtime?.queryClient || options.enabled === false || options.readOnly !== true) return Promise.resolve(undefined);
71
+ return runtime.queryClient.prefetch(queryKey(spaceQueryKey(runtime, key)), read, { staleTimeMs: options.staleTimeMs });
72
+ },
73
+ }), [runtime]);
74
+ }
@@ -0,0 +1,65 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useState } from 'react';
4
+ import { useNotisRuntime } from '../provider';
5
+ import type { ToolCallOptions } from '../runtime';
6
+
7
+ type ToolArguments = Record<string, unknown>;
8
+
9
+ type ToolCall<TArgs extends ToolArguments | undefined, TResult> = undefined extends TArgs
10
+ ? (args?: Exclude<TArgs, undefined>, options?: ToolCallOptions) => Promise<TResult>
11
+ : (args: TArgs, options?: ToolCallOptions) => Promise<TResult>;
12
+
13
+ export interface ToolCallState {
14
+ loading: boolean;
15
+ error: Error | null;
16
+ }
17
+
18
+ export interface UseToolResult<
19
+ TArgs extends ToolArguments | undefined = ToolArguments | undefined,
20
+ TResult = unknown,
21
+ > extends ToolCallState {
22
+ call: ToolCall<TArgs, TResult>;
23
+ }
24
+
25
+ /**
26
+ * Call a specific Notis tool by name.
27
+ *
28
+ * ```tsx
29
+ * const { call, loading } = useTool('LOCAL_NOTIS_WEB_SEARCH');
30
+ * const result = await call({ query: 'latest news' });
31
+ * ```
32
+ */
33
+ export function useTool<
34
+ TArgs extends ToolArguments | undefined = ToolArguments | undefined,
35
+ TResult = unknown,
36
+ >(toolName: string): UseToolResult<TArgs, TResult> {
37
+ const runtime = useNotisRuntime();
38
+ const [loading, setLoading] = useState(false);
39
+ const [error, setError] = useState<Error | null>(null);
40
+
41
+ const call = useCallback(
42
+ async (args?: ToolArguments, options?: ToolCallOptions): Promise<TResult> => {
43
+ if (!runtime) {
44
+ throw new Error('Notis runtime not available. Ensure NotisProvider is mounted.');
45
+ }
46
+
47
+ setLoading(true);
48
+ setError(null);
49
+
50
+ try {
51
+ const result = await runtime.callTool<TResult>(toolName, args, options);
52
+ return result;
53
+ } catch (err) {
54
+ const e = err instanceof Error ? err : new Error(String(err));
55
+ setError(e);
56
+ throw e;
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ },
61
+ [runtime, toolName],
62
+ ) as ToolCall<TArgs, TResult>;
63
+
64
+ return { call, loading, error };
65
+ }
@@ -0,0 +1,12 @@
1
+ 'use client';
2
+
3
+ import { useNotisRuntime } from '../provider';
4
+ import { useQuery, type UseQueryOptions } from './useQuery';
5
+
6
+ /** An explicitly identified read-only tool, cached by tool name and exact arguments. */
7
+ export function useToolQuery<T = unknown>(name: string, args: Record<string, unknown> = {}, options: UseQueryOptions) {
8
+ const runtime = useNotisRuntime();
9
+ return useQuery<T>(['tool', name, args], () => runtime!.callTool<T>(name, args, {
10
+ readOnly: true, dedupe: true,
11
+ }), options);
12
+ }
@@ -0,0 +1,81 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
4
+ import { useNotisRuntime } from '../provider';
5
+
6
+ interface TopBarSearchOptions {
7
+ /** Current value shown in the top bar search input. */
8
+ value: string;
9
+ /** Called when the user types in the top bar. */
10
+ onChange: (value: string) => void;
11
+ /** Placeholder text for the input. Defaults to 'Search…'. */
12
+ placeholder?: string;
13
+ /** Called when the user presses Enter. May return a Promise. */
14
+ onSubmit?: () => void | Promise<void>;
15
+ }
16
+
17
+ interface TopBarSearchActions {
18
+ /** Toggle the loader that replaces the search icon in the top bar. */
19
+ setLoading: (loading: boolean) => void;
20
+ }
21
+
22
+ /**
23
+ * Registers this view's search binding with the portal's top bar search input.
24
+ * While the component is mounted, typing in the top bar calls `onChange`,
25
+ * pressing Enter calls `onSubmit`, and the `value` prop drives the input.
26
+ *
27
+ * Use `setLoading(true)` only for an explicitly submitted search action, then
28
+ * restore the icon with `setLoading(false)`. Automatic reads use content
29
+ * skeletons initially and refresh populated content silently.
30
+ *
31
+ * Without a portal runtime, this hook is a safe no-op.
32
+ *
33
+ * ```tsx
34
+ * const [q, setQ] = useState('');
35
+ * const { setLoading } = useTopBarSearch({
36
+ * value: q,
37
+ * onChange: setQ,
38
+ * placeholder: 'Search notes…',
39
+ * onSubmit: async () => {
40
+ * setLoading(true);
41
+ * try { await runSearch(q); } finally { setLoading(false); }
42
+ * },
43
+ * });
44
+ * ```
45
+ *
46
+ * Callback refs retain the latest committed handlers without re-registering
47
+ * search on every app render. Inline callbacks are safe.
48
+ */
49
+ export function useTopBarSearch(opts: TopBarSearchOptions): TopBarSearchActions {
50
+ const runtime = useNotisRuntime();
51
+ const { value, onChange, placeholder, onSubmit } = opts;
52
+ const callbacks = useRef({ onChange, onSubmit });
53
+ useLayoutEffect(() => { callbacks.current = { onChange, onSubmit }; });
54
+ const hasSubmit = Boolean(onSubmit);
55
+
56
+ useEffect(() => {
57
+ const register = runtime?.registerTopBarSearch;
58
+ if (!register) return;
59
+ register({
60
+ onChange: (next) => callbacks.current.onChange(next),
61
+ placeholder,
62
+ onSubmit: hasSubmit ? () => callbacks.current.onSubmit?.() : undefined,
63
+ });
64
+ return () => {
65
+ register(null);
66
+ };
67
+ }, [runtime, placeholder, hasSubmit]);
68
+
69
+ useEffect(() => {
70
+ runtime?.setTopBarSearchValue?.(value);
71
+ }, [runtime, value]);
72
+
73
+ const setLoading = useCallback(
74
+ (loading: boolean) => {
75
+ runtime?.setTopBarSearchLoading?.(loading);
76
+ },
77
+ [runtime],
78
+ );
79
+
80
+ return { setLoading };
81
+ }
@@ -0,0 +1,95 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useState } from 'react';
4
+ import { useNotisRuntime } from '../provider';
5
+ import { normalizeDocumentRecord } from '../documents';
6
+ import type { DocumentRecord } from '../runtime';
7
+
8
+ export interface UpsertDocumentArgs {
9
+ documentId?: string;
10
+ title?: string;
11
+ /** Property values keyed by property name (already-plain values). */
12
+ properties?: Record<string, unknown>;
13
+ /** Markdown content; converted server-side to BlockNote. Exclusive with contentBlocknote. */
14
+ contentMarkdown?: string | null;
15
+ /** BlockNote block JSON. Exclusive with contentMarkdown. */
16
+ contentBlocknote?: Array<Record<string, unknown>> | null;
17
+ /** 'archive' soft-deletes the document (sets archived_at); 'restore' undoes it. */
18
+ operation?: 'create' | 'update' | 'archive' | 'restore';
19
+ }
20
+
21
+ interface UpsertToolResult {
22
+ status?: string;
23
+ document?: unknown;
24
+ message?: string;
25
+ error?: string;
26
+ }
27
+
28
+ export interface UseUpsertDocumentResult {
29
+ upsert: (args: UpsertDocumentArgs) => Promise<DocumentRecord>;
30
+ loading: boolean;
31
+ error: Error | null;
32
+ }
33
+
34
+ function upsertToolName(databaseSlug: string): string {
35
+ return `LOCAL_NOTIS_DATABASE_UPSERT_${databaseSlug.replace(/-/g, '_').toUpperCase()}`;
36
+ }
37
+
38
+ function buildToolArgs(args: UpsertDocumentArgs): Record<string, unknown> {
39
+ const payload: Record<string, unknown> = {};
40
+ if (args.documentId) payload.document_id = args.documentId;
41
+ if (typeof args.title === 'string') payload.title = args.title;
42
+ if (args.properties) Object.assign(payload, args.properties);
43
+ if ('contentMarkdown' in args) payload.content_markdown = args.contentMarkdown ?? '';
44
+ if ('contentBlocknote' in args) payload.content_blocknote = args.contentBlocknote ?? null;
45
+ if (args.operation) payload.operation = args.operation;
46
+ return payload;
47
+ }
48
+
49
+ /**
50
+ * Create/update/archive documents in a Notis database.
51
+ *
52
+ * ```tsx
53
+ * const { upsert } = useUpsertDocument('journal_entries');
54
+ * await upsert({ documentId, properties: { Mood: 'Great' }, contentMarkdown: reflection });
55
+ * ```
56
+ */
57
+ export function useUpsertDocument(databaseSlug: string): UseUpsertDocumentResult {
58
+ const runtime = useNotisRuntime();
59
+ const [loading, setLoading] = useState(false);
60
+ const [error, setError] = useState<Error | null>(null);
61
+
62
+ const upsert = useCallback(
63
+ async (args: UpsertDocumentArgs): Promise<DocumentRecord> => {
64
+ if (!runtime) {
65
+ throw new Error('Notis runtime not available. Ensure NotisProvider is mounted.');
66
+ }
67
+ if ('contentMarkdown' in args && 'contentBlocknote' in args) {
68
+ throw new Error('Provide contentMarkdown or contentBlocknote, not both.');
69
+ }
70
+
71
+ setLoading(true);
72
+ setError(null);
73
+
74
+ try {
75
+ const result = await runtime.callTool<UpsertToolResult>(
76
+ upsertToolName(databaseSlug),
77
+ buildToolArgs(args),
78
+ );
79
+ if (!result.document) {
80
+ throw new Error(result.error ?? result.message ?? 'Failed to save document');
81
+ }
82
+ return normalizeDocumentRecord(result.document);
83
+ } catch (err) {
84
+ const e = err instanceof Error ? err : new Error(String(err));
85
+ setError(e);
86
+ throw e;
87
+ } finally {
88
+ setLoading(false);
89
+ }
90
+ },
91
+ [runtime, databaseSlug],
92
+ );
93
+
94
+ return { upsert, loading, error };
95
+ }
@@ -0,0 +1,121 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import { useNotisRuntime } from '../provider';
5
+ import { useQuery, type UseQueryResult } from './useQuery';
6
+ import { SdkQueryError } from '../queryErrors';
7
+ import type { SpaceDefinition, SpaceShownOptions, SpaceShownResult, SpaceViewParam } from '../space';
8
+
9
+ type ParamValue<P> = P extends { type: 'enum'; values: readonly (infer V)[] } ? V
10
+ : P extends { type: 'number' } ? number : P extends { type: 'boolean' } ? boolean : string;
11
+
12
+ /** Typed params of a definition: `useViewParams<typeof definition>()`. */
13
+ export type ViewParamsOf<D extends Pick<SpaceDefinition, 'params'>> = D['params'] extends Record<string, SpaceViewParam>
14
+ ? { [K in keyof D['params']]?: ParamValue<D['params'][K]> } : Record<string, string | number | boolean | undefined>;
15
+
16
+ export interface ViewParams<T> {
17
+ /** Declared params only, typed; invalid URL values are left out (their default applies). */
18
+ params: T;
19
+ /** Declared params whose URL value was invalid and ignored. */
20
+ invalid: string[];
21
+ /** Required params without a value. */
22
+ missing: string[];
23
+ declared: Record<string, SpaceViewParam>;
24
+ ready: boolean;
25
+ }
26
+
27
+ const RECORD_KEY = /^(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
28
+ const NUMBER = /^-?\d+(?:\.\d+)?$/;
29
+ // eslint-disable-next-line no-control-regex
30
+ const CONTROL = /[\u0000-\u001f\u007f]/;
31
+
32
+ function parseValue(declaration: SpaceViewParam, raw: string): string | number | boolean | undefined {
33
+ switch (declaration.type) {
34
+ case 'text': return raw.length <= 500 && !CONTROL.test(raw) ? raw : undefined;
35
+ case 'enum': return declaration.values?.includes(raw) ? raw : undefined;
36
+ case 'number': { const value = Number(raw); return NUMBER.test(raw) && Number.isFinite(value) ? value : undefined; }
37
+ case 'boolean': return raw === 'true' ? true : raw === 'false' ? false : undefined;
38
+ case 'date': {
39
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) return undefined;
40
+ const parsed = new Date(`${raw}T00:00:00Z`);
41
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === raw ? raw : undefined;
42
+ }
43
+ case 'record': {
44
+ if (!RECORD_KEY.test(raw)) return undefined;
45
+ const hex = raw.replaceAll('-', '').toLowerCase();
46
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
47
+ }
48
+ default: return undefined;
49
+ }
50
+ }
51
+
52
+ /** Keep declared, valid values only; unknown params are dropped and defaults applied. */
53
+ export function parseViewParams(declared: Record<string, SpaceViewParam> | undefined,
54
+ raw: Record<string, string> | null | undefined): Omit<ViewParams<Record<string, string | number | boolean>>, 'declared' | 'ready'> {
55
+ const params: Record<string, string | number | boolean> = {};
56
+ const invalid: string[] = [], missing: string[] = [];
57
+ for (const [name, declaration] of Object.entries(declared || {})) {
58
+ const value = raw && Object.hasOwn(raw, name) && typeof raw[name] === 'string' && raw[name] !== '' ? raw[name] : undefined;
59
+ const parsed = value === undefined ? undefined : parseValue(declaration, value);
60
+ if (value !== undefined && parsed === undefined) invalid.push(name);
61
+ if (parsed !== undefined) params[name] = parsed;
62
+ else if (declaration.default !== undefined) params[name] = declaration.default;
63
+ else if (declaration.required) missing.push(name);
64
+ }
65
+ return { params, invalid, missing };
66
+ }
67
+
68
+ /**
69
+ * The view's declared URL params, typed and validated. Unknown params (such as `?foo=`) never
70
+ * reach the view. Record params are record keys; the server authorizes them where it uses them.
71
+ */
72
+ export function useViewParams<D extends Pick<SpaceDefinition, 'params'> = SpaceDefinition>(): ViewParams<ViewParamsOf<D>> {
73
+ const runtime = useNotisRuntime();
74
+ const declared = runtime?.space?.params;
75
+ const raw = runtime?.context?.params;
76
+ return useMemo(() => ({ ...parseViewParams(declared, raw), declared: declared || {}, ready: runtime !== null }) as ViewParams<ViewParamsOf<D>>,
77
+ [declared, raw, runtime]);
78
+ }
79
+
80
+ export interface UseShownOptions extends SpaceShownOptions {
81
+ enabled?: boolean;
82
+ staleTimeMs?: number;
83
+ }
84
+
85
+ /** True when a declared list has an authorized host reader (Editor or exact record Site). */
86
+ export function useShownAvailable(name: string): boolean {
87
+ const runtime = useNotisRuntime();
88
+ const list = runtime?.space?.shows && Object.hasOwn(runtime.space.shows, name) ? runtime.space.shows[name] : null;
89
+ return runtime?.resource?.kind === 'space' && typeof runtime.shown === 'function' && Boolean(list && 'database' in list);
90
+ }
91
+
92
+ /**
93
+ * Runs exactly the list this view declares in `shows` with the current params, so the declaration
94
+ * is the list the view displays. Only sorting and paging are chosen here.
95
+ *
96
+ * A list this view context cannot read (undeclared, described only, or no host reader) answers
97
+ * with `error` instead of a query that never starts, so the view can show an error state. Missing
98
+ * required params are not an error: they are listed by `useViewParams().missing`.
99
+ */
100
+ export function useShown<TRow = Record<string, unknown>>(name: string, options: UseShownOptions = {}):
101
+ UseQueryResult<SpaceShownResult & { rows: TRow[] }> {
102
+ const runtime = useNotisRuntime();
103
+ const available = useShownAvailable(name);
104
+ const { params, missing } = useViewParams();
105
+ const { sorts, pageSize, offset, includeContent } = options;
106
+ const list = runtime?.space?.shows && Object.hasOwn(runtime.space.shows, name) ? runtime.space.shows[name] : null;
107
+ const reads = list && 'database' in list && Array.isArray(list.params) ? list.params : null;
108
+ // Rows are keyed by the params the list reads: picking another record in the view keeps the rows
109
+ // already shown instead of reading the same list again. The read itself always sends every param.
110
+ const keyed = useMemo(() => reads ? Object.fromEntries(Object.entries(params).filter(([param]) => reads.includes(param))) : params,
111
+ [params, reads]);
112
+ const result = useQuery(['shown', name, keyed, sorts ?? null, pageSize ?? null, offset ?? null, includeContent ?? false], async () => {
113
+ if (!runtime?.shown) throw new SdkQueryError('listUnavailable');
114
+ return runtime.shown<SpaceShownResult & { rows: TRow[] }>(name, params as Record<string, unknown>,
115
+ { sorts, pageSize, offset, includeContent });
116
+ }, { readOnly: true, enabled: available && !missing.length && options.enabled !== false, staleTimeMs: options.staleTimeMs });
117
+ const unavailable = runtime !== null && !available && options.enabled !== false;
118
+ const error = useMemo(() => unavailable ? new SdkQueryError('listUnavailable', `The list "${name}" is unavailable in this view: declare it in \`shows\` `
119
+ + `with a database, or gate it on useShownAvailable("${name}").`, runtime?.context?.locale) : null, [unavailable, name, runtime?.context?.locale]);
120
+ return unavailable ? { ...result, loading: false, isFetching: false, error } : result;
121
+ }