@notis_ai/cli 0.2.0-beta.19.1 → 0.2.0-beta.191.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 (164) hide show
  1. package/README.md +421 -133
  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/dist/agent-hooks/notis-agent-hook.mjs +19299 -0
  7. package/dist/base-skills/notis-apps/SKILL.md +72 -0
  8. package/dist/base-skills/notis-apps/references/architecture.md +167 -0
  9. package/dist/base-skills/notis-apps/references/context.md +81 -0
  10. package/dist/base-skills/notis-apps/references/design.md +178 -0
  11. package/dist/base-skills/notis-apps/references/reading.md +89 -0
  12. package/dist/base-skills/notis-apps/references/release.md +145 -0
  13. package/dist/base-skills/notis-apps/references/sdk.md +63 -0
  14. package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
  15. package/dist/base-skills/notis-cli/SKILL.md +154 -0
  16. package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
  17. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  18. package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
  19. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  20. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  21. package/dist/base-skills/notis-query/SKILL.md +67 -0
  22. package/dist/base-skills/notis-query/references/database-discovery.md +70 -0
  23. package/dist/base-skills/notis-query/references/documents.md +50 -0
  24. package/dist/base-skills/notis-query/references/query.md +543 -0
  25. package/{template/packages/notis-sdk → dist/sdk}/package.json +14 -4
  26. package/dist/sdk/src/agentContext.ts +36 -0
  27. package/dist/sdk/src/components/Dialog.tsx +46 -0
  28. package/dist/sdk/src/components/DocumentEditor.tsx +103 -0
  29. package/dist/sdk/src/components/Markdown.tsx +60 -0
  30. package/dist/sdk/src/components/MarkdownEditor.tsx +121 -0
  31. package/dist/sdk/src/components/MultiSelectActionBar.tsx +285 -0
  32. package/dist/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
  33. package/dist/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  34. package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  35. package/dist/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
  36. package/dist/sdk/src/components/ShortcutHints.tsx +56 -0
  37. package/dist/sdk/src/components/Skeleton.tsx +24 -0
  38. package/dist/sdk/src/config.ts +261 -0
  39. package/dist/sdk/src/documents.ts +256 -0
  40. package/dist/sdk/src/hooks/useActiveResource.ts +19 -0
  41. package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
  42. package/dist/sdk/src/hooks/useCloudComputer.ts +64 -0
  43. package/dist/sdk/src/hooks/useCollectionInteractions.ts +838 -0
  44. package/dist/sdk/src/hooks/useDatabaseSchema.ts +49 -0
  45. package/dist/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  46. package/dist/sdk/src/hooks/useDocument.ts +43 -0
  47. package/dist/sdk/src/hooks/useDocuments.ts +84 -0
  48. package/dist/sdk/src/hooks/useHandover.ts +77 -0
  49. package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
  50. package/dist/sdk/src/hooks/useMultiSelect.ts +95 -0
  51. package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useNotis.ts +12 -4
  52. package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useNotisNavigation.ts +11 -8
  53. package/dist/sdk/src/hooks/useQuery.ts +71 -0
  54. package/dist/sdk/src/hooks/useTool.ts +65 -0
  55. package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
  56. package/dist/sdk/src/hooks/useTopBarSearch.ts +81 -0
  57. package/dist/sdk/src/hooks/useUpsertDocument.ts +95 -0
  58. package/dist/sdk/src/index.ts +164 -0
  59. package/dist/sdk/src/interactions/actions.ts +59 -0
  60. package/dist/sdk/src/interactions/shortcuts.tsx +726 -0
  61. package/dist/sdk/src/interactions/visibility.ts +13 -0
  62. package/dist/sdk/src/interactions.ts +45 -0
  63. package/dist/sdk/src/provider.tsx +44 -0
  64. package/dist/sdk/src/queryCache.ts +170 -0
  65. package/dist/sdk/src/runtime.ts +465 -0
  66. package/dist/sdk/src/styles.css +266 -0
  67. package/dist/sdk/src/tailwind.ts +66 -0
  68. package/dist/sdk/src/vite.ts +73 -0
  69. package/dist/skill-sync/index.js +1753 -0
  70. package/dist/skill-sync/index.js.map +7 -0
  71. package/dist/skill-sync-worker.mjs +3113 -0
  72. package/package.json +18 -7
  73. package/skills/notis-apps/cli.md +299 -0
  74. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  75. package/skills/notis-onboarding/BRIEF.md +129 -0
  76. package/skills/notis-query/cli.md +39 -0
  77. package/src/agent-hook-entry.js +5 -0
  78. package/src/cli.js +294 -25
  79. package/src/command-specs/agents.js +392 -0
  80. package/src/command-specs/apps.js +1483 -203
  81. package/src/command-specs/auth.js +114 -137
  82. package/src/command-specs/diagnostics.js +730 -0
  83. package/src/command-specs/handover.js +374 -0
  84. package/src/command-specs/helpers.js +85 -82
  85. package/src/command-specs/index.js +25 -6
  86. package/src/command-specs/meta.js +150 -18
  87. package/src/command-specs/onboarding.js +290 -0
  88. package/src/command-specs/profile.js +358 -0
  89. package/src/command-specs/reports.js +97 -0
  90. package/src/command-specs/skills.js +75 -0
  91. package/src/command-specs/smoke.js +386 -0
  92. package/src/command-specs/tools.js +455 -139
  93. package/src/runtime/agent-browser.js +677 -0
  94. package/src/runtime/agent-memory-state.js +126 -0
  95. package/src/runtime/agent-setup.js +383 -0
  96. package/src/runtime/app-boundary-validator.js +404 -0
  97. package/src/runtime/app-changelog.js +79 -0
  98. package/src/runtime/app-platform.js +2652 -205
  99. package/src/runtime/app-registry-scaffolds.js +367 -0
  100. package/src/runtime/app-test-server.js +293 -0
  101. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  102. package/src/runtime/auth-recovery.js +110 -0
  103. package/src/runtime/base-skills.d.ts +20 -0
  104. package/src/runtime/base-skills.js +167 -0
  105. package/src/runtime/channel.js +133 -0
  106. package/src/runtime/delegated-context.js +68 -0
  107. package/src/runtime/errors.js +1 -0
  108. package/src/runtime/git.js +233 -0
  109. package/src/runtime/login-listener.js +15 -0
  110. package/src/runtime/oauth.js +2622 -0
  111. package/src/runtime/output.js +37 -5
  112. package/src/runtime/ports.js +31 -0
  113. package/src/runtime/profiles.js +906 -55
  114. package/src/runtime/screenshot-framing.js +85 -0
  115. package/src/runtime/skill-sync/cloud-client.ts +99 -0
  116. package/src/runtime/skill-sync/index.ts +786 -0
  117. package/src/runtime/skill-sync/local-scanner.ts +1151 -0
  118. package/src/runtime/skill-sync/symlink-manager.ts +433 -0
  119. package/src/runtime/skill-sync/sync-plan.ts +47 -0
  120. package/src/runtime/skill-sync/types.ts +131 -0
  121. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  122. package/src/runtime/skill-sync-service.js +109 -0
  123. package/src/runtime/store-screenshot.js +146 -0
  124. package/src/runtime/sync-skills.d.ts +37 -0
  125. package/src/runtime/sync-skills.js +231 -0
  126. package/src/runtime/telemetry.js +92 -0
  127. package/src/runtime/transport.js +324 -45
  128. package/src/skill-sync-worker-entry.js +2 -0
  129. package/src/skill-sync-worker.js +50 -0
  130. package/template/.harness/index.html.tmpl +435 -0
  131. package/template/CHANGELOG.md +5 -0
  132. package/template/app/globals.css +28 -3
  133. package/template/app/layout.tsx +6 -3
  134. package/template/app/page.tsx +49 -42
  135. package/template/components/page-heading.tsx +23 -0
  136. package/template/components/ui/badge.tsx +7 -4
  137. package/template/components/ui/button.tsx +1 -1
  138. package/template/components/ui/card.tsx +24 -11
  139. package/template/components/ui/native-select.tsx +24 -0
  140. package/template/notis.config.ts +24 -6
  141. package/template/package-lock.json +3642 -0
  142. package/template/package.json +19 -16
  143. package/template/postcss.config.mjs +1 -1
  144. package/template/tailwind.config.ts +1 -6
  145. package/template/tsconfig.json +1 -0
  146. package/src/command-specs/db.js +0 -163
  147. package/src/runtime/app-preview-server.js +0 -312
  148. package/template/packages/notis-sdk/src/config.ts +0 -48
  149. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  150. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  151. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  152. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  153. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  154. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  155. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  156. package/template/packages/notis-sdk/src/index.ts +0 -47
  157. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  158. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  159. package/template/packages/notis-sdk/src/styles.css +0 -123
  160. package/template/packages/notis-sdk/src/vite.ts +0 -54
  161. package/template/packages/notis-sdk/tsconfig.json +0 -15
  162. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useBackend.ts +0 -0
  163. /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useTools.ts +0 -0
  164. /package/{template/packages/notis-sdk → dist/sdk}/src/ui.ts +0 -0
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Document data helpers: normalize raw tool results (Notion-shaped property
3
+ * values, snake_case fields) into the SDK's `DocumentRecord`/`DatabaseProperty`
4
+ * shapes, and derive plain-text projections from typed content.
5
+ *
6
+ * These are pure functions — safe in any context (portal, dev harness, tests).
7
+ */
8
+
9
+ import type {
10
+ DatabaseProperty,
11
+ DatabasePropertyOption,
12
+ DatabasePropertyType,
13
+ DocumentContentType,
14
+ DocumentRecord,
15
+ SecretPropertyValue,
16
+ } from './runtime';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Value primitives
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export function isPresentString(value: unknown): value is string {
23
+ return typeof value === 'string' && value.trim().length > 0;
24
+ }
25
+
26
+ export function asRecord(value: unknown): Record<string, unknown> | null {
27
+ return value && typeof value === 'object' && !Array.isArray(value)
28
+ ? (value as Record<string, unknown>)
29
+ : null;
30
+ }
31
+
32
+ export function optionalString(value: unknown): string | null {
33
+ return isPresentString(value) ? value.trim() : null;
34
+ }
35
+
36
+ /** Flattens a Notion-shaped rich_text array (or plain string) to text. */
37
+ export function extractRichText(value: unknown): string {
38
+ if (typeof value === 'string') return value;
39
+ if (!Array.isArray(value)) return '';
40
+ return value
41
+ .map((item) => {
42
+ const record = asRecord(item);
43
+ const text = asRecord(record?.text);
44
+ return optionalString(text?.content) ?? optionalString(record?.plain_text) ?? '';
45
+ })
46
+ .join('');
47
+ }
48
+
49
+ /**
50
+ * Reads a `secret` property value. The platform only ever sends the pointer
51
+ * ({present, reference, status, metadata}), so this rebuilds it field by field
52
+ * rather than passing the payload through — an app can never surface secret
53
+ * material through this helper, whatever the server sent.
54
+ */
55
+ export function getSecretValue(value: unknown): SecretPropertyValue {
56
+ const record = asRecord(value);
57
+ const metadata = asRecord(record?.metadata);
58
+ return {
59
+ present: record?.present === true,
60
+ reference: optionalString(record?.reference),
61
+ status: optionalString(record?.status),
62
+ metadata,
63
+ };
64
+ }
65
+
66
+ /** Extracts the ids of a normalized relation property value. */
67
+ export function getRelationIds(value: unknown): string[] {
68
+ if (!Array.isArray(value)) return [];
69
+ return value.filter((item): item is string => isPresentString(item));
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Property + document normalization
74
+ // ---------------------------------------------------------------------------
75
+
76
+ /**
77
+ * Collapses a Notion-shaped property value ({type: 'select', select: {...}})
78
+ * into a plain JS value (string, string[], number, boolean, ...). Values that
79
+ * are already plain pass through unchanged.
80
+ */
81
+ export function normalizePropertyValue(value: unknown): unknown {
82
+ const record = asRecord(value);
83
+ if (!record) return value;
84
+
85
+ const type = optionalString(record.type);
86
+ if (!type) return value;
87
+
88
+ if (type === 'title') return extractRichText(record.title);
89
+ if (type === 'rich_text') return extractRichText(record.rich_text);
90
+ if (type === 'select' || type === 'status') {
91
+ return optionalString(asRecord(record[type])?.name) ?? record[type] ?? null;
92
+ }
93
+ if (type === 'multi_select') {
94
+ const items = Array.isArray(record.multi_select) ? record.multi_select : [];
95
+ return items.map((item) => optionalString(asRecord(item)?.name) ?? item).filter(Boolean);
96
+ }
97
+ if (type === 'relation') {
98
+ const items = Array.isArray(record.relation) ? record.relation : [];
99
+ return items.map((item) => optionalString(asRecord(item)?.id) ?? item).filter(Boolean);
100
+ }
101
+ if (type === 'date') return optionalString(asRecord(record.date)?.start) ?? record.date ?? null;
102
+ // Before the `type in record` fallthrough: a secret value has no `secret`
103
+ // key, so passing it through would hand the caller the raw payload.
104
+ if (type === 'secret') return getSecretValue(record);
105
+ if (type in record) return record[type];
106
+ return value;
107
+ }
108
+
109
+ function normalizeContentType(value: unknown): DocumentContentType | null {
110
+ return value === 'markdown' || value === 'file' || value === 'view' ? value : null;
111
+ }
112
+
113
+ /**
114
+ * Normalizes a raw document from a Notis database tool result
115
+ * (LOCAL_NOTIS_DATABASE_QUERY / GET_DOCUMENT / UPSERT_*) into a
116
+ * `DocumentRecord`: camelCases fields and collapses property values.
117
+ */
118
+ export function normalizeDocumentRecord(value: unknown): DocumentRecord {
119
+ const record = asRecord(value) ?? {};
120
+ const rawProperties = asRecord(record.properties) ?? {};
121
+ const properties: Record<string, unknown> = {};
122
+ for (const [key, propertyValue] of Object.entries(rawProperties)) {
123
+ properties[key] = normalizePropertyValue(propertyValue);
124
+ }
125
+
126
+ return {
127
+ id: optionalString(record.id) ?? '',
128
+ title: optionalString(record.title) ?? 'Untitled',
129
+ url: optionalString(record.url),
130
+ properties,
131
+ icon: optionalString(record.icon),
132
+ cover: optionalString(record.cover),
133
+ databaseSlug: optionalString(record.databaseSlug) ?? optionalString(record.database_slug) ?? undefined,
134
+ contentType: normalizeContentType(record.contentType ?? record.content_type),
135
+ fileType: optionalString(record.fileType) ?? optionalString(record.file_type),
136
+ contentBlocknote: Array.isArray(record.contentBlocknote)
137
+ ? (record.contentBlocknote as Array<Record<string, unknown>>)
138
+ : Array.isArray(record.content_blocknote)
139
+ ? (record.content_blocknote as Array<Record<string, unknown>>)
140
+ : null,
141
+ contentMarkdown: optionalString(record.contentMarkdown) ?? optionalString(record.content_markdown),
142
+ plainText: optionalString(record.plainText) ?? optionalString(record.plain_text),
143
+ viewType: optionalString(record.viewType) ?? optionalString(record.view_type),
144
+ viewState: asRecord(record.viewState) ?? asRecord(record.view_state),
145
+ viewRevision:
146
+ typeof (record.viewRevision ?? record.view_revision) === 'number'
147
+ ? Number(record.viewRevision ?? record.view_revision)
148
+ : null,
149
+ createdAt:
150
+ optionalString(record.createdAt)
151
+ ?? optionalString(record.created_at)
152
+ ?? optionalString(record.created_time),
153
+ lastEditedTime:
154
+ optionalString(record.lastEditedTime)
155
+ ?? optionalString(record.last_edited_time)
156
+ ?? optionalString(record.updated_at),
157
+ };
158
+ }
159
+
160
+ /** Normalizes a raw schema property from LOCAL_NOTIS_DATABASE_GET_DATABASE. */
161
+ export function normalizeDatabaseProperty(value: unknown): DatabaseProperty | null {
162
+ const record = asRecord(value);
163
+ const name = optionalString(record?.name);
164
+ if (!record || !name) return null;
165
+
166
+ const rawOptions = Array.isArray(record.options) ? record.options : [];
167
+ const options = rawOptions.flatMap((option): DatabasePropertyOption[] => {
168
+ const optionRecord = asRecord(option);
169
+ const optionName = optionalString(optionRecord?.name);
170
+ if (!optionRecord || !optionName) return [];
171
+ return [{
172
+ id: optionalString(optionRecord.id),
173
+ name: optionName,
174
+ color: optionalString(optionRecord.color),
175
+ order: typeof optionRecord.order === 'number' ? optionRecord.order : undefined,
176
+ }];
177
+ });
178
+
179
+ return {
180
+ id: optionalString(record.id),
181
+ name,
182
+ type: (optionalString(record.type) ?? 'rich_text') as DatabasePropertyType,
183
+ description: optionalString(record.description),
184
+ options,
185
+ };
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Plain-text projections
190
+ // ---------------------------------------------------------------------------
191
+
192
+ /** Strips markdown syntax to readable plain text (previews, search). */
193
+ export function markdownToPlainText(markdown: string): string {
194
+ return markdown
195
+ // fenced code blocks: keep the code, drop the fences
196
+ .replace(/```[^\n]*\n?/g, '')
197
+ // images: keep alt text
198
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
199
+ // links: keep link text
200
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
201
+ // html tags
202
+ .replace(/<[^>]+>/g, ' ')
203
+ // headings, blockquotes, list markers, task boxes
204
+ .replace(/^\s{0,3}(#{1,6}\s+|>\s?|[-*+]\s+(\[[ xX]\]\s+)?|\d+\.\s+)/gm, '')
205
+ // emphasis / strikethrough / inline code
206
+ .replace(/(\*\*|__|[*_~`])/g, '')
207
+ // table separators and pipes
208
+ .replace(/^\s*\|?[-:| ]+\|?\s*$/gm, '')
209
+ .replace(/\|/g, ' ')
210
+ // horizontal rules
211
+ .replace(/^\s*([-*_]\s*){3,}$/gm, '')
212
+ .replace(/\s+/g, ' ')
213
+ .trim();
214
+ }
215
+
216
+ /** Flattens BlockNote block JSON to plain text. */
217
+ export function blockNoteToPlainText(blocks: unknown): string {
218
+ const segments: string[] = [];
219
+
220
+ function visit(node: unknown): void {
221
+ if (typeof node === 'string') {
222
+ if (node.trim()) segments.push(node.trim());
223
+ return;
224
+ }
225
+ if (Array.isArray(node)) {
226
+ node.forEach(visit);
227
+ return;
228
+ }
229
+ const record = asRecord(node);
230
+ if (!record) return;
231
+ if (typeof record.text === 'string' && record.text.trim()) {
232
+ segments.push(record.text.trim());
233
+ }
234
+ visit(record.content);
235
+ visit(record.children);
236
+ }
237
+
238
+ visit(blocks);
239
+ return segments.join(' ').replace(/\s+/g, ' ').trim();
240
+ }
241
+
242
+ /**
243
+ * Best-available plain-text preview for a document:
244
+ * plainText -> contentMarkdown -> contentBlocknote -> title.
245
+ */
246
+ export function getDocumentPreview(document: DocumentRecord): string {
247
+ if (isPresentString(document.plainText)) {
248
+ return document.plainText.replace(/\s+/g, ' ').trim();
249
+ }
250
+ if (isPresentString(document.contentMarkdown)) {
251
+ return markdownToPlainText(document.contentMarkdown);
252
+ }
253
+ const fromBlocks = blockNoteToPlainText(document.contentBlocknote);
254
+ if (fromBlocks) return fromBlocks;
255
+ return isPresentString(document.title) ? document.title : 'Untitled';
256
+ }
@@ -0,0 +1,19 @@
1
+ import { useEffect, useMemo } from 'react';
2
+ import { useNotisRuntime } from '../provider';
3
+ import type { ContextResource } from '../runtime';
4
+
5
+ function stableResource(resource: ContextResource | null): string {
6
+ return JSON.stringify(resource);
7
+ }
8
+
9
+ /** Keep Notis aware of the resource currently open inside an app view. */
10
+ export function useActiveResource(resource: ContextResource | null): void {
11
+ const runtime = useNotisRuntime();
12
+ const signature = stableResource(resource);
13
+ const stable = useMemo(() => resource, [signature]);
14
+
15
+ useEffect(() => {
16
+ runtime?.publishActiveResource?.(stable);
17
+ return () => runtime?.publishActiveResource?.(null);
18
+ }, [runtime, stable]);
19
+ }
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+ import { useMemo } from 'react';
3
+ import { useNotisRuntime } from '../provider';
4
+ import type { AgentContextItem } from '../agentContext';
5
+
6
+ /** Share context without owning storage, opening a new thread, or sending a message. */
7
+ export function useAgentContext() {
8
+ const runtime = useNotisRuntime();
9
+ return useMemo(() => ({
10
+ add(item: AgentContextItem): Promise<boolean> {
11
+ if (!runtime?.addContext) return Promise.reject(new Error('Chat context is unavailable in this host.'));
12
+ return runtime.addContext(item);
13
+ },
14
+ update(item: AgentContextItem): Promise<boolean> {
15
+ if (!runtime?.updateContext) return Promise.reject(new Error('Chat context is unavailable in this host.'));
16
+ return runtime.updateContext(item);
17
+ },
18
+ remove(id: string): Promise<boolean> {
19
+ if (!runtime?.removeContext) return Promise.reject(new Error('Chat context is unavailable in this host.'));
20
+ return runtime.removeContext(id);
21
+ },
22
+ }), [runtime]);
23
+ }
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useRef } from 'react';
4
+ import { useQuery } from './useQuery';
5
+ import { useNotisRuntime } from '../provider';
6
+ import type { CloudComputerFacts } from '../runtime';
7
+
8
+ export interface UseCloudComputerResult {
9
+ /**
10
+ * The facts, or null while the first read is in flight. `facts.available`
11
+ * is false when this host cannot answer — render the app's own fallback.
12
+ */
13
+ facts: CloudComputerFacts | null;
14
+ loading: boolean;
15
+ isFetching: boolean;
16
+ error: Error | null;
17
+ /** Re-read the facts. The platform caches them for a few minutes. */
18
+ refresh: () => Promise<void>;
19
+ }
20
+
21
+ const UNAVAILABLE: CloudComputerFacts = {
22
+ available: false,
23
+ reason: 'unsupported_host',
24
+ sandbox: null,
25
+ cli_auth: {
26
+ gh: { authenticated: null, account: null, checked_at: null, reason: 'unsupported_host' },
27
+ },
28
+ };
29
+
30
+ /**
31
+ * Read-only facts about the user's cloud computer.
32
+ *
33
+ * ```tsx
34
+ * const { facts } = useCloudComputer();
35
+ * const gh = facts?.available ? facts.cli_auth.gh : null;
36
+ *
37
+ * return gh?.authenticated
38
+ * ? <p>Signed in as {gh.account}</p>
39
+ * : <GithubConnect />;
40
+ * ```
41
+ *
42
+ * Requires `capabilities.cloudComputer: 'read'` in `notis.config.ts` and the
43
+ * user's approval at install time. It answers with state the platform already
44
+ * holds: reading it never creates, resumes or commands a sandbox, and the GitHub
45
+ * probe only runs when the sandbox is already awake. `authenticated: null`
46
+ * therefore means *unknown*, not *signed out* — keep the app's own fallback for
47
+ * that case and for hosts that answer `{ available: false }` (the dev harness,
48
+ * the vite preview).
49
+ */
50
+ export function useCloudComputer(): UseCloudComputerResult {
51
+ const runtime = useNotisRuntime();
52
+ const explicitRefresh = useRef(false);
53
+ const query = useQuery<CloudComputerFacts>(['cloud-computer-facts'], async () => {
54
+ const refresh = explicitRefresh.current;
55
+ explicitRefresh.current = false;
56
+ return runtime?.cloudComputerFacts ? runtime.cloudComputerFacts(refresh ? { refresh: true } : undefined) : UNAVAILABLE;
57
+ }, { readOnly: true });
58
+ const refresh = useCallback(async () => {
59
+ explicitRefresh.current = true;
60
+ await query.refetch();
61
+ }, [query.refetch]);
62
+ return { facts: query.data ?? (query.error ? UNAVAILABLE : null), loading: query.loading,
63
+ isFetching: query.isFetching, error: query.error, refresh };
64
+ }