@notis_ai/cli 0.2.0-beta.16.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 (52) hide show
  1. package/README.md +335 -0
  2. package/bin/notis.js +2 -0
  3. package/package.json +38 -0
  4. package/src/cli.js +147 -0
  5. package/src/command-specs/apps.js +496 -0
  6. package/src/command-specs/auth.js +178 -0
  7. package/src/command-specs/db.js +163 -0
  8. package/src/command-specs/helpers.js +193 -0
  9. package/src/command-specs/index.js +20 -0
  10. package/src/command-specs/meta.js +154 -0
  11. package/src/command-specs/tools.js +391 -0
  12. package/src/runtime/app-platform.js +624 -0
  13. package/src/runtime/app-preview-server.js +312 -0
  14. package/src/runtime/errors.js +55 -0
  15. package/src/runtime/help.js +60 -0
  16. package/src/runtime/output.js +180 -0
  17. package/src/runtime/profiles.js +202 -0
  18. package/src/runtime/transport.js +198 -0
  19. package/template/app/globals.css +3 -0
  20. package/template/app/layout.tsx +7 -0
  21. package/template/app/page.tsx +55 -0
  22. package/template/components/ui/badge.tsx +28 -0
  23. package/template/components/ui/button.tsx +53 -0
  24. package/template/components/ui/card.tsx +56 -0
  25. package/template/components.json +20 -0
  26. package/template/lib/utils.ts +6 -0
  27. package/template/notis.config.ts +18 -0
  28. package/template/package.json +32 -0
  29. package/template/packages/notis-sdk/package.json +26 -0
  30. package/template/packages/notis-sdk/src/config.ts +48 -0
  31. package/template/packages/notis-sdk/src/helpers.ts +131 -0
  32. package/template/packages/notis-sdk/src/hooks/useAppState.ts +50 -0
  33. package/template/packages/notis-sdk/src/hooks/useBackend.ts +41 -0
  34. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +58 -0
  35. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +87 -0
  36. package/template/packages/notis-sdk/src/hooks/useDocument.ts +61 -0
  37. package/template/packages/notis-sdk/src/hooks/useNotis.ts +31 -0
  38. package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +49 -0
  39. package/template/packages/notis-sdk/src/hooks/useTool.ts +49 -0
  40. package/template/packages/notis-sdk/src/hooks/useTools.ts +56 -0
  41. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +57 -0
  42. package/template/packages/notis-sdk/src/index.ts +47 -0
  43. package/template/packages/notis-sdk/src/provider.tsx +44 -0
  44. package/template/packages/notis-sdk/src/runtime.ts +159 -0
  45. package/template/packages/notis-sdk/src/styles.css +123 -0
  46. package/template/packages/notis-sdk/src/ui.ts +15 -0
  47. package/template/packages/notis-sdk/src/vite.ts +54 -0
  48. package/template/packages/notis-sdk/tsconfig.json +15 -0
  49. package/template/postcss.config.mjs +8 -0
  50. package/template/tailwind.config.ts +58 -0
  51. package/template/tsconfig.json +22 -0
  52. package/template/vite.config.ts +10 -0
@@ -0,0 +1,202 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { CliError, EXIT_CODES } from './errors.js';
5
+
6
+ export const CONFIG_DIR = join(homedir(), '.notis');
7
+ export const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
8
+ export const WORKSPACE_DIR = join(CONFIG_DIR, 'workspace');
9
+ export const DEFAULT_API_BASE = 'https://api.notis.ai';
10
+ export const DEFAULT_PROFILE = 'default';
11
+ const LOCAL_DEFAULT_API_BASES = new Set([
12
+ 'http://localhost:3001',
13
+ 'http://127.0.0.1:3001',
14
+ ]);
15
+
16
+ function clone(value) {
17
+ return JSON.parse(JSON.stringify(value));
18
+ }
19
+
20
+ export function normalizeConfig(rawConfig = {}) {
21
+ const raw = rawConfig && typeof rawConfig === 'object' ? clone(rawConfig) : {};
22
+
23
+ if (raw.profiles && typeof raw.profiles === 'object') {
24
+ const profiles = {};
25
+ for (const [name, profile] of Object.entries(raw.profiles)) {
26
+ if (!profile || typeof profile !== 'object') {
27
+ continue;
28
+ }
29
+ profiles[name] = {
30
+ jwt: typeof profile.jwt === 'string' ? profile.jwt : undefined,
31
+ api_base: typeof profile.api_base === 'string' ? profile.api_base : undefined,
32
+ };
33
+ }
34
+
35
+ if (!profiles[DEFAULT_PROFILE]) {
36
+ profiles[DEFAULT_PROFILE] = {};
37
+ }
38
+
39
+ return {
40
+ current_profile:
41
+ typeof raw.current_profile === 'string' && raw.current_profile in profiles
42
+ ? raw.current_profile
43
+ : DEFAULT_PROFILE,
44
+ profiles,
45
+ };
46
+ }
47
+
48
+ return {
49
+ current_profile: DEFAULT_PROFILE,
50
+ profiles: {
51
+ [DEFAULT_PROFILE]: {
52
+ jwt: typeof raw.jwt === 'string' ? raw.jwt : undefined,
53
+ api_base: typeof raw.api_base === 'string' ? raw.api_base : undefined,
54
+ },
55
+ },
56
+ };
57
+ }
58
+
59
+ export function loadConfig() {
60
+ if (!existsSync(CONFIG_FILE)) {
61
+ return normalizeConfig({});
62
+ }
63
+
64
+ return normalizeConfig(JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')));
65
+ }
66
+
67
+ export function saveConfig(config) {
68
+ mkdirSync(CONFIG_DIR, { recursive: true });
69
+ writeFileSync(CONFIG_FILE, JSON.stringify(normalizeConfig(config), null, 2));
70
+ }
71
+
72
+ export function getProfile(config, profileName) {
73
+ const normalized = normalizeConfig(config);
74
+ return normalized.profiles[profileName] || {};
75
+ }
76
+
77
+ export function getCurrentProfileName(config, preferredName) {
78
+ const normalized = normalizeConfig(config);
79
+ if (preferredName && normalized.profiles[preferredName]) {
80
+ return preferredName;
81
+ }
82
+ return normalized.current_profile || DEFAULT_PROFILE;
83
+ }
84
+
85
+ export function ensureProfile(config, profileName) {
86
+ const normalized = normalizeConfig(config);
87
+ if (!normalized.profiles[profileName]) {
88
+ normalized.profiles[profileName] = {};
89
+ }
90
+ return normalized;
91
+ }
92
+
93
+ export function getApiBase(config, profileName, override) {
94
+ if (override) {
95
+ return override;
96
+ }
97
+ const env = process.env.NOTIS_API_BASE;
98
+ if (env) {
99
+ return env;
100
+ }
101
+ const profile = getProfile(config, profileName);
102
+ const profileApiBase = profile.api_base;
103
+ const conductorPort = Number.parseInt(process.env.CONDUCTOR_PORT || '', 10);
104
+
105
+ // Conductor assigns dynamic local ports. When a workspace is running under
106
+ // Conductor, treat the legacy localhost:3001 profile value as stale and
107
+ // transparently retarget the CLI at the active backend port.
108
+ if (
109
+ Number.isInteger(conductorPort) &&
110
+ conductorPort > 0 &&
111
+ (!profileApiBase || LOCAL_DEFAULT_API_BASES.has(profileApiBase))
112
+ ) {
113
+ return `http://localhost:${conductorPort + 1}`;
114
+ }
115
+
116
+ return profileApiBase || DEFAULT_API_BASE;
117
+ }
118
+
119
+ export function getJwt(config, profileName) {
120
+ const env = process.env.NOTIS_JWT;
121
+ if (env) {
122
+ return env;
123
+ }
124
+ const profile = getProfile(config, profileName);
125
+ return profile.jwt;
126
+ }
127
+
128
+ export function isAgentMode(globalOptions = {}) {
129
+ return process.env.NOTIS_AGENT === '1' || Boolean(globalOptions.agentMode);
130
+ }
131
+
132
+ export function isNonInteractive(globalOptions = {}) {
133
+ if (process.env.NOTIS_NON_INTERACTIVE === '1') {
134
+ return true;
135
+ }
136
+ if (globalOptions.nonInteractive) {
137
+ return true;
138
+ }
139
+ return isAgentMode(globalOptions);
140
+ }
141
+
142
+ export function resolveOutputMode(globalOptions = {}) {
143
+ if (globalOptions.json) {
144
+ return 'json';
145
+ }
146
+ if (globalOptions.output) {
147
+ return globalOptions.output;
148
+ }
149
+ if (process.env.NOTIS_OUTPUT) {
150
+ return process.env.NOTIS_OUTPUT;
151
+ }
152
+ return !process.stdout.isTTY || isAgentMode(globalOptions) ? 'json' : 'table';
153
+ }
154
+
155
+ export function resolveTimeoutMs(globalOptions = {}) {
156
+ const raw = globalOptions.timeoutMs || process.env.NOTIS_TIMEOUT_MS;
157
+ if (!raw) {
158
+ return 30000;
159
+ }
160
+ const parsed = Number.parseInt(raw, 10);
161
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 30000;
162
+ }
163
+
164
+ export function resolveRuntimeProfile(globalOptions = {}, { requireAuth = true } = {}) {
165
+ const config = loadConfig();
166
+ const profileName = getCurrentProfileName(config, globalOptions.profile);
167
+ const apiBase = getApiBase(config, profileName, globalOptions.apiBase);
168
+ const jwt = getJwt(config, profileName);
169
+ const agentMode = isAgentMode(globalOptions);
170
+ const nonInteractive = isNonInteractive(globalOptions);
171
+ const outputMode = resolveOutputMode(globalOptions);
172
+ const timeoutMs = resolveTimeoutMs(globalOptions);
173
+
174
+ if (requireAuth && !jwt) {
175
+ throw new CliError({
176
+ code: 'auth_missing',
177
+ message: `No JWT configured for profile ${profileName}`,
178
+ exitCode: EXIT_CODES.auth,
179
+ hints: [
180
+ {
181
+ command: 'notis auth login --jwt <token>',
182
+ reason: 'Configure a token for non-interactive use',
183
+ },
184
+ ],
185
+ });
186
+ }
187
+
188
+ return {
189
+ config,
190
+ profileName,
191
+ apiBase,
192
+ jwt,
193
+ agentMode,
194
+ nonInteractive,
195
+ outputMode,
196
+ timeoutMs,
197
+ };
198
+ }
199
+
200
+ export function workspacePath(appId) {
201
+ return join(WORKSPACE_DIR, appId);
202
+ }
@@ -0,0 +1,198 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { CliError, EXIT_CODES } from './errors.js';
3
+
4
+ const META_TOOL_NAMES = new Set([
5
+ 'notis_find_toolkits',
6
+ 'notis_find_tools',
7
+ 'notis_execute_tool',
8
+ 'notis_find_additional_tools',
9
+ ]);
10
+
11
+ function canonicalizeToolName(toolName) {
12
+ if (typeof toolName !== 'string' || !toolName.length) {
13
+ return toolName;
14
+ }
15
+
16
+ if (META_TOOL_NAMES.has(toolName) || toolName.startsWith('notis-default-')) {
17
+ return toolName;
18
+ }
19
+
20
+ if (toolName.startsWith('notis_')) {
21
+ return `notis-default-${toolName.slice('notis_'.length)}`;
22
+ }
23
+
24
+ if (toolName.includes('_notis_')) {
25
+ return `notis-default-${toolName.replace(/_notis_/g, '_')}`;
26
+ }
27
+
28
+ return toolName;
29
+ }
30
+
31
+ function normalizeBackendError(status, payload) {
32
+ const backendError = payload?.error;
33
+ const message =
34
+ backendError?.message ||
35
+ backendError ||
36
+ payload?.message ||
37
+ payload?.error ||
38
+ `Request failed with status ${status}`;
39
+
40
+ if (status === 401) {
41
+ return new CliError({
42
+ code: 'auth_invalid',
43
+ message,
44
+ exitCode: EXIT_CODES.auth,
45
+ hints: [
46
+ {
47
+ command: 'notis auth login --jwt <token>',
48
+ reason: 'Refresh the stored JWT for this profile',
49
+ },
50
+ ],
51
+ details: payload || {},
52
+ });
53
+ }
54
+
55
+ if (status === 409) {
56
+ return new CliError({
57
+ code: 'conflict',
58
+ message,
59
+ exitCode: EXIT_CODES.conflict,
60
+ retryable: false,
61
+ hints: payload?.hints || [],
62
+ details: payload || {},
63
+ });
64
+ }
65
+
66
+ if (status === 403) {
67
+ return new CliError({
68
+ code: 'forbidden',
69
+ message,
70
+ exitCode: EXIT_CODES.auth,
71
+ details: payload || {},
72
+ hints: [
73
+ { command: 'notis tools toolkits', reason: 'Check which toolkits are available' },
74
+ { command: 'notis whoami', reason: 'Verify your active profile and permissions' },
75
+ ...(payload?.hints || []),
76
+ ],
77
+ });
78
+ }
79
+
80
+ if (status >= 400 && status < 500) {
81
+ return new CliError({
82
+ code: 'usage_error',
83
+ message,
84
+ exitCode: EXIT_CODES.usage,
85
+ details: payload || {},
86
+ hints: payload?.hints || [],
87
+ });
88
+ }
89
+
90
+ return new CliError({
91
+ code: 'backend_error',
92
+ message,
93
+ exitCode: EXIT_CODES.backend,
94
+ retryable: status >= 500,
95
+ details: payload || {},
96
+ hints: payload?.hints || [],
97
+ });
98
+ }
99
+
100
+ export async function httpRequest({
101
+ runtime,
102
+ method = 'POST',
103
+ path,
104
+ body,
105
+ requireAuth = true,
106
+ }) {
107
+ const controller = new AbortController();
108
+ const timeout = setTimeout(() => controller.abort(), runtime.timeoutMs);
109
+ const requestId = `req_${randomUUID().replace(/-/g, '')}`;
110
+
111
+ const headers = {
112
+ 'Content-Type': 'application/json',
113
+ 'X-Notis-CLI-Version': runtime.cliVersion,
114
+ 'X-Notis-Request-Id': requestId,
115
+ };
116
+
117
+ if (requireAuth && runtime.jwt) {
118
+ headers.Authorization = `Bearer ${runtime.jwt}`;
119
+ }
120
+
121
+ try {
122
+ const response = await fetch(`${runtime.apiBase}${path}`, {
123
+ method,
124
+ headers,
125
+ body: body ? JSON.stringify(body) : undefined,
126
+ signal: controller.signal,
127
+ });
128
+ clearTimeout(timeout);
129
+
130
+ let payload = null;
131
+ try {
132
+ payload = await response.json();
133
+ } catch {
134
+ payload = null;
135
+ }
136
+
137
+ if (!response.ok) {
138
+ throw normalizeBackendError(response.status, payload);
139
+ }
140
+
141
+ return {
142
+ requestId,
143
+ payload: payload || {},
144
+ };
145
+ } catch (error) {
146
+ clearTimeout(timeout);
147
+
148
+ if (error instanceof CliError) {
149
+ throw error;
150
+ }
151
+
152
+ if (error?.name === 'AbortError') {
153
+ throw new CliError({
154
+ code: 'network_timeout',
155
+ message: `Request timed out after ${runtime.timeoutMs}ms`,
156
+ exitCode: EXIT_CODES.network,
157
+ retryable: true,
158
+ hints: [
159
+ { command: 'notis doctor', reason: 'Check API reachability' },
160
+ { command: `--timeout-ms ${runtime.timeoutMs * 2}`, reason: 'Retry with a longer timeout' },
161
+ ],
162
+ });
163
+ }
164
+
165
+ throw new CliError({
166
+ code: 'network_error',
167
+ message: error instanceof Error ? error.message : String(error),
168
+ exitCode: EXIT_CODES.network,
169
+ retryable: true,
170
+ cause: error,
171
+ });
172
+ }
173
+ }
174
+
175
+ export async function callTool({
176
+ runtime,
177
+ toolName,
178
+ arguments_: argumentsPayload = {},
179
+ idempotencyKey,
180
+ }) {
181
+ const requestId = idempotencyKey || (runtime.mutating ? randomUUID() : null);
182
+ return httpRequest({
183
+ runtime,
184
+ path: '/cli_tools',
185
+ body: {
186
+ tool_name: canonicalizeToolName(toolName),
187
+ arguments: argumentsPayload,
188
+ idempotency_key: requestId,
189
+ cli_context: {
190
+ output_mode: runtime.outputMode,
191
+ profile: runtime.profileName,
192
+ cwd: process.cwd(),
193
+ agent_mode: runtime.agentMode,
194
+ cli_version: runtime.cliVersion,
195
+ },
196
+ },
197
+ });
198
+ }
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
@@ -0,0 +1,7 @@
1
+ import { NotisProvider } from '@notis/sdk';
2
+ import '@notis/sdk/styles.css';
3
+ import './globals.css';
4
+
5
+ export default function AppShell({ children }: { children: React.ReactNode }) {
6
+ return <NotisProvider>{children}</NotisProvider>;
7
+ }
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import { useNotis, useDatabase } from '@notis/sdk';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
6
+
7
+ export default function HomePage() {
8
+ const { app, ready } = useNotis();
9
+ const { documents, loading } = useDatabase('items');
10
+
11
+ return (
12
+ <main className="notis-app-shell space-y-6">
13
+ <Card>
14
+ <CardHeader className="space-y-3">
15
+ <Badge variant="secondary" className="w-fit">Installed app</Badge>
16
+ <div className="space-y-2">
17
+ <CardTitle>{ready ? app?.name : 'Loading...'}</CardTitle>
18
+ <CardDescription>
19
+ {ready ? app?.description : 'Loading app metadata...'}
20
+ </CardDescription>
21
+ </div>
22
+ </CardHeader>
23
+ </Card>
24
+
25
+ <Card>
26
+ <CardHeader>
27
+ <CardTitle>Items</CardTitle>
28
+ <CardDescription>Use shadcn surfaces and portal tokens so the app feels native inside Notis.</CardDescription>
29
+ </CardHeader>
30
+ <CardContent>
31
+ {loading ? (
32
+ <p className="text-sm text-muted-foreground">Loading...</p>
33
+ ) : documents.length === 0 ? (
34
+ <div className="rounded-xl border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground">
35
+ No items yet. Deploy the app and create some.
36
+ </div>
37
+ ) : (
38
+ <div className="space-y-3">
39
+ {documents.map((doc) => (
40
+ <div key={doc.id} className="rounded-xl border border-border bg-background px-4 py-3">
41
+ <div className="flex items-center justify-between gap-3">
42
+ <p className="font-medium">{doc.title}</p>
43
+ {doc.properties.status ? (
44
+ <Badge variant="outline">{String(doc.properties.status)}</Badge>
45
+ ) : null}
46
+ </div>
47
+ </div>
48
+ ))}
49
+ </div>
50
+ )}
51
+ </CardContent>
52
+ </Card>
53
+ </main>
54
+ );
55
+ }
@@ -0,0 +1,28 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+
4
+ import { cn } from '@/lib/utils';
5
+
6
+ const badgeVariants = cva(
7
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'border-transparent bg-primary/10 text-primary',
12
+ secondary: 'border-transparent bg-secondary text-secondary-foreground',
13
+ outline: 'text-foreground',
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: 'default',
18
+ },
19
+ },
20
+ );
21
+
22
+ export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
23
+
24
+ function Badge({ className, variant, ...props }: BadgeProps) {
25
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
26
+ }
27
+
28
+ export { Badge, badgeVariants };
@@ -0,0 +1,53 @@
1
+ import * as React from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+
5
+ import { cn } from '@/lib/utils';
6
+
7
+ const buttonVariants = cva(
8
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
14
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
15
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
16
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
17
+ link: 'text-primary underline-offset-4 hover:underline',
18
+ },
19
+ size: {
20
+ default: 'h-10 px-4 py-2',
21
+ sm: 'h-9 rounded-md px-3',
22
+ lg: 'h-11 rounded-md px-8',
23
+ icon: 'h-10 w-10',
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ variant: 'default',
28
+ size: 'default',
29
+ },
30
+ },
31
+ );
32
+
33
+ export interface ButtonProps
34
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
35
+ VariantProps<typeof buttonVariants> {
36
+ asChild?: boolean;
37
+ }
38
+
39
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
41
+ const Comp = asChild ? Slot : 'button';
42
+ return (
43
+ <Comp
44
+ className={cn(buttonVariants({ variant, size, className }))}
45
+ ref={ref}
46
+ {...props}
47
+ />
48
+ );
49
+ },
50
+ );
51
+ Button.displayName = 'Button';
52
+
53
+ export { Button, buttonVariants };
@@ -0,0 +1,56 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '@/lib/utils';
4
+
5
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
+ ({ className, ...props }, ref) => (
7
+ <div
8
+ ref={ref}
9
+ className={cn('rounded-2xl border bg-card text-card-foreground shadow-sm', className)}
10
+ {...props}
11
+ />
12
+ ),
13
+ );
14
+ Card.displayName = 'Card';
15
+
16
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
17
+ ({ className, ...props }, ref) => (
18
+ <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
19
+ ),
20
+ );
21
+ CardHeader.displayName = 'CardHeader';
22
+
23
+ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
24
+ ({ className, ...props }, ref) => (
25
+ <h3
26
+ ref={ref}
27
+ className={cn('text-xl font-semibold leading-none tracking-tight', className)}
28
+ {...props}
29
+ />
30
+ ),
31
+ );
32
+ CardTitle.displayName = 'CardTitle';
33
+
34
+ const CardDescription = React.forwardRef<
35
+ HTMLParagraphElement,
36
+ React.HTMLAttributes<HTMLParagraphElement>
37
+ >(({ className, ...props }, ref) => (
38
+ <p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
39
+ ));
40
+ CardDescription.displayName = 'CardDescription';
41
+
42
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
43
+ ({ className, ...props }, ref) => (
44
+ <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
45
+ ),
46
+ );
47
+ CardContent.displayName = 'CardContent';
48
+
49
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
50
+ ({ className, ...props }, ref) => (
51
+ <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
52
+ ),
53
+ );
54
+ CardFooter.displayName = 'CardFooter';
55
+
56
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ }
20
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,18 @@
1
+ import { defineNotisApp } from '@notis/sdk/config';
2
+
3
+ export default defineNotisApp({
4
+ name: 'My Notis App',
5
+ description: 'A new Notis app',
6
+ icon: 'lucide:layout-dashboard',
7
+
8
+ databases: ['items'],
9
+
10
+ routes: [
11
+ { path: '/', name: 'Home', icon: 'lucide:home', default: true },
12
+ ],
13
+
14
+ tools: [
15
+ 'notis_query_database',
16
+ 'notis_upsert_document',
17
+ ],
18
+ });
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "my-notis-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@notis/sdk": "file:./packages/notis-sdk",
13
+ "@radix-ui/react-slot": "^1.1.0",
14
+ "class-variance-authority": "^0.7.0",
15
+ "clsx": "^2.1.1",
16
+ "lucide-react": "^0.474.0",
17
+ "react": "^19.0.0",
18
+ "react-dom": "^19.0.0",
19
+ "tailwind-merge": "^2.6.0",
20
+ "tailwindcss-animate": "^1.0.7"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.0.0",
24
+ "@types/react": "^19.0.0",
25
+ "@types/react-dom": "^19.0.0",
26
+ "@vitejs/plugin-react": "^4.0.0",
27
+ "postcss": "^8.0.0",
28
+ "tailwindcss": "^3.4.0",
29
+ "typescript": "^5.0.0",
30
+ "vite": "^6.0.0"
31
+ }
32
+ }