@notis_ai/cli 0.2.12 → 0.2.14
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.
- package/README.md +56 -3
- package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +40 -2
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +21 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +17 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +132 -1
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +40 -2
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +21 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +17 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +132 -1
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +2 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +40 -2
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +21 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +17 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +132 -1
- package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +40 -2
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +21 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +17 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +132 -1
- package/package.json +1 -1
- package/skills/notis-apps/SKILL.md +11 -7
- package/skills/notis-apps/cli.md +8 -3
- package/skills/notis-cli/SKILL.md +2 -0
- package/src/cli.js +158 -0
- package/src/command-specs/apps.js +238 -50
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/index.js +3 -0
- package/src/command-specs/meta.js +53 -0
- package/src/command-specs/tools.js +6 -0
- package/src/runtime/app-dev-server.js +17 -8
- package/src/runtime/app-platform.js +218 -6
- package/src/runtime/auth-recovery.js +13 -3
- package/src/runtime/channel.js +133 -0
- package/src/runtime/delegated-context.js +68 -0
- package/src/runtime/git.js +233 -0
- package/src/runtime/oauth.js +36 -4
- package/src/runtime/profiles.js +17 -1
- package/src/runtime/transport.js +19 -2
- package/template/.harness/index.html.tmpl +116 -47
- package/template/packages/sdk/src/config.ts +52 -0
- package/template/packages/sdk/src/documents.ts +21 -0
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/template/packages/sdk/src/index.ts +17 -0
- package/template/packages/sdk/src/runtime.ts +132 -1
- package/template/metadata/screenshot-1.png +0 -0
- package/template/metadata/screenshot-2.png +0 -0
- package/template/metadata/screenshot-3.png +0 -0
package/README.md
CHANGED
|
@@ -20,6 +20,17 @@ npx --package @notis_ai/cli@latest -- notis tools search "list Notis databases"
|
|
|
20
20
|
|
|
21
21
|
Use `notis login --paste-code` for the HTTPS copy-paste fallback on a remote machine.
|
|
22
22
|
|
|
23
|
+
## Release channels
|
|
24
|
+
|
|
25
|
+
`@latest` is the only tag worth documenting, for beta accounts too.
|
|
26
|
+
|
|
27
|
+
The npm tag has to be chosen before the CLI starts, and the CLI only learns which Notis it talks to once it reads the profile — so no single install command can be right for both environments on its own. Instead the deployment answers the question: `/.well-known/oauth-protected-resource/cli` reports its channel, `notis login` pins it on the profile, and any later run that finds itself on the wrong build hands the whole invocation to the right one before it does anything else.
|
|
28
|
+
|
|
29
|
+
- `notis doctor` reports `release_channel`, `cli_version`, and a `channel` check.
|
|
30
|
+
- `--api-base <url>` decides the build for that one run, so a one-off call against another environment uses the matching CLI.
|
|
31
|
+
- `./dev.sh` profiles and source checkouts are never re-executed: whatever you started stays in control.
|
|
32
|
+
- `NOTIS_CLI_AUTO_CHANNEL=0` disables the hand-off; `doctor` then reports the mismatch instead of correcting it.
|
|
33
|
+
|
|
23
34
|
## Profiles
|
|
24
35
|
|
|
25
36
|
A profile is one account paired with one API endpoint. Every profile keeps its own credential, so switching between them never signs any of them out.
|
|
@@ -144,11 +155,14 @@ When to use: Run this inside a single app or a monorepo root with apps/<name>/no
|
|
|
144
155
|
Options:
|
|
145
156
|
- `--port <number>` — Local bundle server port (default: 5173).
|
|
146
157
|
- `--no-open` — Do not auto-open the desktop Portal local development app.
|
|
158
|
+
- `--live-data` — Read and write the installed app's real databases instead of empty dev copies. Applies to this session only; warns and falls back when the app is not installed yet.
|
|
159
|
+
- `--grant-cloud-shell` — Approve a cloudComputer: 'shell' declaration without the interactive prompt. The grant persists for this dev app; authorship alone never grants it.
|
|
147
160
|
|
|
148
161
|
Examples:
|
|
149
162
|
- `npx --package @notis_ai/cli@latest -- notis apps dev`
|
|
150
163
|
- `npx --package @notis_ai/cli@latest -- notis apps dev ./my-app`
|
|
151
164
|
- `npx --package @notis_ai/cli@latest -- notis apps dev ./workspace --port 5200`
|
|
165
|
+
- `npx --package @notis_ai/cli@latest -- notis apps dev --live-data # iterate on a view over the installed app's real rows`
|
|
152
166
|
|
|
153
167
|
### `npx --package @notis_ai/cli@latest -- notis apps build [dir]`
|
|
154
168
|
|
|
@@ -162,15 +176,16 @@ Examples:
|
|
|
162
176
|
|
|
163
177
|
### `npx --package @notis_ai/cli@latest -- notis apps verify [dir]`
|
|
164
178
|
|
|
165
|
-
Validate every route and
|
|
179
|
+
Validate that every route renders and reports Store listing readiness.
|
|
166
180
|
|
|
167
|
-
When to use:
|
|
181
|
+
When to use: Any time after notis apps build, and before deploy. Catches render-time crashes and missing runtime calls. Incomplete listing media is reported as a warning; pass --listing to fail on it instead.
|
|
168
182
|
|
|
169
183
|
Options:
|
|
170
184
|
- `--routes <slugs>` — Comma-separated route slugs. Default: every route in manifest.
|
|
171
185
|
- `--port <n>` — Loopback port. Default: auto-pick.
|
|
172
186
|
- `--skip-build` — Skip notis apps build; reuse existing .notis/output/.
|
|
173
|
-
- `--mode <mode>` — stub | live. Default stub. Live posts to /portal_views/runtime_query with the CLI JWT.
|
|
187
|
+
- `--mode <mode>` — stub | live. Default stub. Live posts to /portal_views/runtime_query with the CLI JWT and fails routes whose runtime calls all errored.
|
|
188
|
+
- `--listing` — Fail instead of warn when the Store listing (tagline, categories, screenshots, changelog) is incomplete.
|
|
174
189
|
- `--no-browser` — Start the harness server and print URLs; do not drive agent-browser.
|
|
175
190
|
- `--keep-open` — Leave server + browser session running after report (for manual triage).
|
|
176
191
|
|
|
@@ -178,6 +193,7 @@ Examples:
|
|
|
178
193
|
- `npx --package @notis_ai/cli@latest -- notis apps verify`
|
|
179
194
|
- `npx --package @notis_ai/cli@latest -- notis apps verify --routes notes`
|
|
180
195
|
- `npx --package @notis_ai/cli@latest -- notis apps verify --mode live`
|
|
196
|
+
- `npx --package @notis_ai/cli@latest -- notis apps verify --listing # gate on Store listing readiness before publish`
|
|
181
197
|
- `npx --package @notis_ai/cli@latest -- notis apps verify --no-browser # start the harness, drive agent-browser yourself`
|
|
182
198
|
|
|
183
199
|
### `npx --package @notis_ai/cli@latest -- notis apps screenshot [dir]`
|
|
@@ -283,6 +299,43 @@ Examples:
|
|
|
283
299
|
- `npx --package @notis_ai/cli@latest -- notis apps doctor ./my-app`
|
|
284
300
|
|
|
285
301
|
|
|
302
|
+
## Hand-over
|
|
303
|
+
|
|
304
|
+
Give the branch you are on to a Notis agent, which continues the work in a git worktree on the Notis cloud computer. `--route` picks the agent: the hosted Notis agent, or the user's own Codex/Claude Code in the cloud sandbox or on their Mac. `--branch-mode same` makes the agent commit onto your branch; the default cuts a new branch from it.
|
|
305
|
+
|
|
306
|
+
### `npx --package @notis_ai/cli@latest -- notis handover start <task>`
|
|
307
|
+
|
|
308
|
+
Hand the current branch to a Notis agent and keep working.
|
|
309
|
+
|
|
310
|
+
When to use: Use this when you want Notis to continue work on the branch you are on -- long refactors, test fixing, or anything that should keep running after you close the laptop. Pick the agent with --route.
|
|
311
|
+
|
|
312
|
+
Options:
|
|
313
|
+
- `--branch-mode <mode>` — same = the agent commits onto your branch. new = the agent cuts a new branch from it (default).
|
|
314
|
+
- `--route <target>` — Which agent runs it: notis (hosted, default), codex_cloud, claude_cloud, codex_local, claude_local, or auto.
|
|
315
|
+
- `--repo <slug>` — Configured repository slug on the cloud computer, when you know it.
|
|
316
|
+
- `--no-wip` — Refuse on a dirty tree instead of committing the changes first.
|
|
317
|
+
|
|
318
|
+
Examples:
|
|
319
|
+
- `npx --package @notis_ai/cli@latest -- notis handover start "fix the failing auth tests"`
|
|
320
|
+
- `npx --package @notis_ai/cli@latest -- notis handover start "finish the migration" --branch-mode same --route codex_cloud`
|
|
321
|
+
- `npx --package @notis_ai/cli@latest -- notis handover start "add integration tests" --route claude_cloud`
|
|
322
|
+
- `npx --package @notis_ai/cli@latest -- notis handover start "review and clean up this branch" --route claude_local`
|
|
323
|
+
|
|
324
|
+
### `npx --package @notis_ai/cli@latest -- notis handover status`
|
|
325
|
+
|
|
326
|
+
Show the coding-agent threads Notis is running for you.
|
|
327
|
+
|
|
328
|
+
When to use: Use this after a hand-over to see whether the agent is still working.
|
|
329
|
+
|
|
330
|
+
Options:
|
|
331
|
+
- `--provider <provider>` — Filter to codex or claude_code.
|
|
332
|
+
- `--refresh` — Force a live refresh instead of cached state.
|
|
333
|
+
|
|
334
|
+
Examples:
|
|
335
|
+
- `npx --package @notis_ai/cli@latest -- notis handover status`
|
|
336
|
+
- `npx --package @notis_ai/cli@latest -- notis handover status --provider codex --refresh`
|
|
337
|
+
|
|
338
|
+
|
|
286
339
|
## Generic Tools
|
|
287
340
|
|
|
288
341
|
### `npx --package @notis_ai/cli@latest -- notis tools toolkits`
|
|
@@ -85,7 +85,12 @@ export interface NotisAppAuthor {
|
|
|
85
85
|
export interface NotisAppSkillConfig {
|
|
86
86
|
/** Stable source-owned key used by other app declarations. */
|
|
87
87
|
key: string;
|
|
88
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Path to the skill, relative to notis.config.ts. Either a Markdown file
|
|
90
|
+
* (`./skills/onboarding.md`) or a directory holding SKILL.md plus its
|
|
91
|
+
* supporting files (`./skills/onboarding/`), which are packaged on deploy
|
|
92
|
+
* and materialized next to SKILL.md in the sandbox.
|
|
93
|
+
*/
|
|
89
94
|
path: string;
|
|
90
95
|
/** User-facing name used for the installed skill. */
|
|
91
96
|
name: string;
|
|
@@ -106,7 +111,13 @@ export interface NotisAppScreenshotConfig {
|
|
|
106
111
|
alt: string;
|
|
107
112
|
/** Route slug captured by `notis apps screenshot`. */
|
|
108
113
|
route?: string;
|
|
109
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* Named scenario from metadata/screenshot-fixtures.json. Its `tools` and
|
|
116
|
+
* `requests` override the file-level ones key by key for this capture, and
|
|
117
|
+
* its `actions` run once the route has mounted -- so one route can be shown
|
|
118
|
+
* in several states (populated, empty, a panel opened) without the states
|
|
119
|
+
* leaking into each other.
|
|
120
|
+
*/
|
|
110
121
|
scenario?: string;
|
|
111
122
|
/** Optional CSS selector captured as the truthful focal region for this Store image. */
|
|
112
123
|
focus?: string;
|
|
@@ -142,6 +153,25 @@ export interface NotisAppCapabilities {
|
|
|
142
153
|
* bound to the app's own databases.
|
|
143
154
|
*/
|
|
144
155
|
workspaceDatabases?: 'read';
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Read a few facts about the user's cloud computer: whether a sandbox exists
|
|
159
|
+
* and is running, and whether the GitHub CLI is signed in there.
|
|
160
|
+
*
|
|
161
|
+
* Without this an app has to infer them — the Workspaces app treated a
|
|
162
|
+
* configured repository as proof that `gh auth login` had happened, which
|
|
163
|
+
* cannot show an account name and cannot notice a revoked credential.
|
|
164
|
+
* `'read'` never creates, resumes or commands a sandbox. Read it with
|
|
165
|
+
* `useCloudComputer()`.
|
|
166
|
+
*
|
|
167
|
+
* `'shell'` additionally asks to command the cloud computer: it unlocks
|
|
168
|
+
* `LOCAL_NOTIS_RUN_SANDBOX_SHELL` and the sandbox file tools from this app's
|
|
169
|
+
* views (they are denied to every view otherwise), and implies the read
|
|
170
|
+
* facts. This is the same authority the user's own agent has on the sandbox,
|
|
171
|
+
* so the user is asked for it explicitly at install or in the Store grant
|
|
172
|
+
* step; declare it only when the app's core actions genuinely run there.
|
|
173
|
+
*/
|
|
174
|
+
cloudComputer?: 'read' | 'shell';
|
|
145
175
|
}
|
|
146
176
|
|
|
147
177
|
export interface NotisAppConfig {
|
|
@@ -179,6 +209,14 @@ export interface NotisAppConfig {
|
|
|
179
209
|
*/
|
|
180
210
|
capabilities?: NotisAppCapabilities;
|
|
181
211
|
routes?: NotisRouteConfig[];
|
|
212
|
+
/**
|
|
213
|
+
* Final tool names this app can call at runtime, enforced server-side. Use
|
|
214
|
+
* names returned by shared discovery, including native `LOCAL_NOTIS_*`,
|
|
215
|
+
* connected-service names such as `GMAIL_SEND_EMAIL`,
|
|
216
|
+
* `LOCAL_POSTFORME_*`, and `LOCAL_MCP_<SERVER>_<TOOL>`. App code calls
|
|
217
|
+
* each declared name directly with `useTool`; metered calls use the shared
|
|
218
|
+
* credit-cap and usage-billing path.
|
|
219
|
+
*/
|
|
182
220
|
tools?: string[];
|
|
183
221
|
/** Skills shipped from this app's source tree. */
|
|
184
222
|
skills?: NotisAppSkillConfig[];
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
DatabasePropertyType,
|
|
13
13
|
DocumentContentType,
|
|
14
14
|
DocumentRecord,
|
|
15
|
+
SecretPropertyValue,
|
|
15
16
|
} from './runtime';
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -45,6 +46,23 @@ export function extractRichText(value: unknown): string {
|
|
|
45
46
|
.join('');
|
|
46
47
|
}
|
|
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
|
+
|
|
48
66
|
/** Extracts the ids of a normalized relation property value. */
|
|
49
67
|
export function getRelationIds(value: unknown): string[] {
|
|
50
68
|
if (!Array.isArray(value)) return [];
|
|
@@ -81,6 +99,9 @@ export function normalizePropertyValue(value: unknown): unknown {
|
|
|
81
99
|
return items.map((item) => optionalString(asRecord(item)?.id) ?? item).filter(Boolean);
|
|
82
100
|
}
|
|
83
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);
|
|
84
105
|
if (type in record) return record[type];
|
|
85
106
|
return value;
|
|
86
107
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import type { CloudComputerFacts } from '../runtime';
|
|
6
|
+
|
|
7
|
+
export interface UseCloudComputerResult {
|
|
8
|
+
/**
|
|
9
|
+
* The facts, or null while the first read is in flight. `facts.available`
|
|
10
|
+
* is false when this host cannot answer — render the app's own fallback.
|
|
11
|
+
*/
|
|
12
|
+
facts: CloudComputerFacts | null;
|
|
13
|
+
loading: boolean;
|
|
14
|
+
error: Error | null;
|
|
15
|
+
/** Re-read the facts. The platform caches them for a few minutes. */
|
|
16
|
+
refresh: () => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const UNAVAILABLE: CloudComputerFacts = {
|
|
20
|
+
available: false,
|
|
21
|
+
reason: 'unsupported_host',
|
|
22
|
+
sandbox: null,
|
|
23
|
+
cli_auth: {
|
|
24
|
+
gh: { authenticated: null, account: null, checked_at: null, reason: 'unsupported_host' },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read-only facts about the user's cloud computer.
|
|
30
|
+
*
|
|
31
|
+
* ```tsx
|
|
32
|
+
* const { facts } = useCloudComputer();
|
|
33
|
+
* const gh = facts?.available ? facts.cli_auth.gh : null;
|
|
34
|
+
*
|
|
35
|
+
* return gh?.authenticated
|
|
36
|
+
* ? <p>Signed in as {gh.account}</p>
|
|
37
|
+
* : <GithubConnect />;
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* Requires `capabilities.cloudComputer: 'read'` in `notis.config.ts` and the
|
|
41
|
+
* user's approval at install time. It answers with state the platform already
|
|
42
|
+
* holds: reading it never creates, resumes or commands a sandbox, and the GitHub
|
|
43
|
+
* probe only runs when the sandbox is already awake. `authenticated: null`
|
|
44
|
+
* therefore means *unknown*, not *signed out* — keep the app's own fallback for
|
|
45
|
+
* that case and for hosts that answer `{ available: false }` (the dev harness,
|
|
46
|
+
* the vite preview).
|
|
47
|
+
*/
|
|
48
|
+
export function useCloudComputer(): UseCloudComputerResult {
|
|
49
|
+
const runtime = useNotisRuntime();
|
|
50
|
+
const [facts, setFacts] = useState<CloudComputerFacts | null>(null);
|
|
51
|
+
// True from the first committed render: the initial read is already queued
|
|
52
|
+
// in an effect, and `{ loading: false, facts: null }` would flash a
|
|
53
|
+
// consumer's fallback branch before the answer arrives.
|
|
54
|
+
const [loading, setLoading] = useState(true);
|
|
55
|
+
const [error, setError] = useState<Error | null>(null);
|
|
56
|
+
const mounted = useRef(true);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
mounted.current = true;
|
|
60
|
+
return () => {
|
|
61
|
+
mounted.current = false;
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const read = useCallback(async (options?: { refresh?: boolean }) => {
|
|
66
|
+
if (!runtime?.cloudComputerFacts) {
|
|
67
|
+
setFacts(UNAVAILABLE);
|
|
68
|
+
setLoading(false);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setLoading(true);
|
|
73
|
+
setError(null);
|
|
74
|
+
try {
|
|
75
|
+
const next = await runtime.cloudComputerFacts(options);
|
|
76
|
+
if (mounted.current) setFacts(next);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
79
|
+
if (mounted.current) {
|
|
80
|
+
setError(e);
|
|
81
|
+
// A refused or failed read is the same product state as a host that
|
|
82
|
+
// cannot answer: the app shows its fallback instead of an error.
|
|
83
|
+
setFacts(UNAVAILABLE);
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
if (mounted.current) setLoading(false);
|
|
87
|
+
}
|
|
88
|
+
}, [runtime]);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
void read();
|
|
92
|
+
}, [read]);
|
|
93
|
+
|
|
94
|
+
const refresh = useCallback(() => read({ refresh: true }), [read]);
|
|
95
|
+
|
|
96
|
+
return { facts, loading, error, refresh };
|
|
97
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import { useDocuments, type UseDocumentsOptions, type UseDocumentsResult } from './useDocuments';
|
|
6
|
+
import type { DocumentRecord } from '../runtime';
|
|
7
|
+
|
|
8
|
+
export interface UseDatabaseSubscriptionOptions extends UseDocumentsOptions {
|
|
9
|
+
/** Set to false to keep the query but skip the change feed. */
|
|
10
|
+
subscribe?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseDatabaseSubscriptionResult extends UseDocumentsResult {
|
|
14
|
+
/** Alias of `documents`, for views that think in rows. */
|
|
15
|
+
rows: DocumentRecord[];
|
|
16
|
+
/** True while a live change feed is attached to this database. */
|
|
17
|
+
live: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Query a Notis database and keep it fresh without polling.
|
|
22
|
+
*
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const { rows, live, refetch } = useDatabaseSubscription('workspaces');
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* A change on the database wakes the hook, which then refetches through the
|
|
28
|
+
* usual `LOCAL_NOTIS_DATABASE_QUERY` path — the change feed is a signal only
|
|
29
|
+
* and never carries row data. Hosts without a change feed (the dev harness,
|
|
30
|
+
* the screenshot stub, the vite preview) still return rows; `live` is false
|
|
31
|
+
* there and the app should keep offering its manual refresh.
|
|
32
|
+
*/
|
|
33
|
+
export function useDatabaseSubscription(
|
|
34
|
+
databaseSlug: string,
|
|
35
|
+
options: UseDatabaseSubscriptionOptions = {},
|
|
36
|
+
): UseDatabaseSubscriptionResult {
|
|
37
|
+
const runtime = useNotisRuntime();
|
|
38
|
+
const { subscribe = true, ...documentOptions } = options;
|
|
39
|
+
const { documents, loading, error, refetch } = useDocuments(databaseSlug, documentOptions);
|
|
40
|
+
const [live, setLive] = useState(false);
|
|
41
|
+
|
|
42
|
+
const refetchRef = useRef(refetch);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
refetchRef.current = refetch;
|
|
45
|
+
}, [refetch]);
|
|
46
|
+
|
|
47
|
+
const enabled = options.enabled !== false && subscribe;
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!runtime?.subscribeDatabase || !enabled || !databaseSlug) {
|
|
51
|
+
setLive(false);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
const unsubscribe = runtime.subscribeDatabase(
|
|
57
|
+
databaseSlug,
|
|
58
|
+
() => {
|
|
59
|
+
refetchRef.current();
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
onStatusChange: (isLive) => {
|
|
63
|
+
if (!cancelled) setLive(isLive);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
cancelled = true;
|
|
70
|
+
setLive(false);
|
|
71
|
+
unsubscribe?.();
|
|
72
|
+
};
|
|
73
|
+
}, [runtime, databaseSlug, enabled]);
|
|
74
|
+
|
|
75
|
+
return { documents, rows: documents, loading, error, refetch, live };
|
|
76
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import type { HandoverPayload, HandoverResult } from '../runtime';
|
|
6
|
+
|
|
7
|
+
export interface UseHandoverResult {
|
|
8
|
+
/** Hand the work over. Rejects when the host has no manager chat. */
|
|
9
|
+
handover: (payload: HandoverPayload) => Promise<HandoverResult>;
|
|
10
|
+
/** True while the manager chat is being prepared. */
|
|
11
|
+
pending: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
/**
|
|
14
|
+
* False when the host cannot hand work over (dev harness, vite preview).
|
|
15
|
+
* Render the app's own fallback — a copyable prompt, say — when it is false.
|
|
16
|
+
*/
|
|
17
|
+
available: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hand a piece of work from app code to the Notis manager chat.
|
|
22
|
+
*
|
|
23
|
+
* An app displays work; the manager runs it. `handover` puts the message in
|
|
24
|
+
* the chat surface that already owns streaming progress, billing, cancellation
|
|
25
|
+
* and the transcript, and the app watches its own databases for the result.
|
|
26
|
+
*
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const { handover, pending, available } = useHandover();
|
|
29
|
+
*
|
|
30
|
+
* return available ? (
|
|
31
|
+
* <Button
|
|
32
|
+
* disabled={pending}
|
|
33
|
+
* onClick={() => { void handover({ prompt: 'Create a workspace on notis to ...' }); }}
|
|
34
|
+
* >
|
|
35
|
+
* Send to Notis
|
|
36
|
+
* </Button>
|
|
37
|
+
* ) : (
|
|
38
|
+
* <CopyablePrompt prompt="Create a workspace on notis to ..." />
|
|
39
|
+
* );
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* Pass `skill` to bind the work to a skill declared in `notis.config.ts`; the
|
|
43
|
+
* host rejects a key the app does not declare. `autoSend` is accepted for
|
|
44
|
+
* forward compatibility; today's hosts always return `drafted` and let the
|
|
45
|
+
* user press send.
|
|
46
|
+
*/
|
|
47
|
+
export function useHandover(): UseHandoverResult {
|
|
48
|
+
const runtime = useNotisRuntime();
|
|
49
|
+
const [pending, setPending] = useState(false);
|
|
50
|
+
const [error, setError] = useState<Error | null>(null);
|
|
51
|
+
|
|
52
|
+
const handover = useCallback(
|
|
53
|
+
async (payload: HandoverPayload): Promise<HandoverResult> => {
|
|
54
|
+
if (!runtime?.handover) {
|
|
55
|
+
throw new Error('This Notis host cannot hand work to the manager chat.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setPending(true);
|
|
59
|
+
setError(null);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return await runtime.handover(payload);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
65
|
+
setError(e);
|
|
66
|
+
throw e;
|
|
67
|
+
} finally {
|
|
68
|
+
setPending(false);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[runtime],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return { handover, pending, error, available: Boolean(runtime?.handover) };
|
|
75
|
+
}
|
|
@@ -13,6 +13,11 @@ export { NotisProvider, useNotisRuntime } from './provider';
|
|
|
13
13
|
export { useNotis } from './hooks/useNotis';
|
|
14
14
|
export { useDocuments } from './hooks/useDocuments';
|
|
15
15
|
export type { UseDocumentsOptions, UseDocumentsResult } from './hooks/useDocuments';
|
|
16
|
+
export { useDatabaseSubscription } from './hooks/useDatabaseSubscription';
|
|
17
|
+
export type {
|
|
18
|
+
UseDatabaseSubscriptionOptions,
|
|
19
|
+
UseDatabaseSubscriptionResult,
|
|
20
|
+
} from './hooks/useDatabaseSubscription';
|
|
16
21
|
export { useDocument } from './hooks/useDocument';
|
|
17
22
|
export type { UseDocumentOptions, UseDocumentResult } from './hooks/useDocument';
|
|
18
23
|
export { useUpsertDocument } from './hooks/useUpsertDocument';
|
|
@@ -22,6 +27,10 @@ export type { UseDatabaseSchemaResult } from './hooks/useDatabaseSchema';
|
|
|
22
27
|
export { useTool } from './hooks/useTool';
|
|
23
28
|
export type { ToolCallState, UseToolResult } from './hooks/useTool';
|
|
24
29
|
export { useTools } from './hooks/useTools';
|
|
30
|
+
export { useHandover } from './hooks/useHandover';
|
|
31
|
+
export type { UseHandoverResult } from './hooks/useHandover';
|
|
32
|
+
export { useCloudComputer } from './hooks/useCloudComputer';
|
|
33
|
+
export type { UseCloudComputerResult } from './hooks/useCloudComputer';
|
|
25
34
|
export { useNotisNavigation } from './hooks/useNotisNavigation';
|
|
26
35
|
export { useTopBarSearch } from './hooks/useTopBarSearch';
|
|
27
36
|
export { useBackend } from './hooks/useBackend';
|
|
@@ -37,6 +46,7 @@ export {
|
|
|
37
46
|
extractRichText,
|
|
38
47
|
getDocumentPreview,
|
|
39
48
|
getRelationIds,
|
|
49
|
+
getSecretValue,
|
|
40
50
|
isPresentString,
|
|
41
51
|
markdownToPlainText,
|
|
42
52
|
normalizeDatabaseProperty,
|
|
@@ -64,6 +74,9 @@ export type { MultiSelectDragOverlayProps } from './components/MultiSelectDragOv
|
|
|
64
74
|
// Types (re-exported for convenience)
|
|
65
75
|
export type {
|
|
66
76
|
AppDescriptor,
|
|
77
|
+
CloudComputerCliAuthFacts,
|
|
78
|
+
CloudComputerFacts,
|
|
79
|
+
CloudComputerSandboxFacts,
|
|
67
80
|
CollectionItem,
|
|
68
81
|
CollectionItemDetail,
|
|
69
82
|
DatabaseDescriptor,
|
|
@@ -72,12 +85,16 @@ export type {
|
|
|
72
85
|
DatabasePropertyType,
|
|
73
86
|
DocumentContentType,
|
|
74
87
|
DocumentRecord,
|
|
88
|
+
HandoverPayload,
|
|
89
|
+
HandoverResult,
|
|
75
90
|
NotisDocumentEditorProps,
|
|
76
91
|
NotisRuntime,
|
|
77
92
|
NotisRuntimeContext,
|
|
78
93
|
NotisRuntimeUI,
|
|
79
94
|
QueryFilter,
|
|
80
95
|
RouteDescriptor,
|
|
96
|
+
SecretPropertyValue,
|
|
97
|
+
SubscribeDatabaseOptions,
|
|
81
98
|
ToolDescriptor,
|
|
82
99
|
ToolInputSchema,
|
|
83
100
|
} from './runtime';
|
|
@@ -25,7 +25,21 @@ export type DatabasePropertyType =
|
|
|
25
25
|
| 'status'
|
|
26
26
|
| 'relation'
|
|
27
27
|
| 'formula'
|
|
28
|
-
| 'files'
|
|
28
|
+
| 'files'
|
|
29
|
+
| 'secret';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The value of a `secret` property. The platform stores a pointer to a
|
|
33
|
+
* credential held elsewhere and never the credential itself, so there is
|
|
34
|
+
* deliberately nothing here to read the secret material from — only whether
|
|
35
|
+
* one is attached, which credential it is, and its lifecycle state.
|
|
36
|
+
*/
|
|
37
|
+
export interface SecretPropertyValue {
|
|
38
|
+
present: boolean;
|
|
39
|
+
reference: string | null;
|
|
40
|
+
status: string | null;
|
|
41
|
+
metadata: Record<string, unknown> | null;
|
|
42
|
+
}
|
|
29
43
|
|
|
30
44
|
export interface DatabasePropertyOption {
|
|
31
45
|
id?: string | null;
|
|
@@ -188,6 +202,84 @@ export interface NotisRuntimeUI {
|
|
|
188
202
|
// NotisRuntime
|
|
189
203
|
// ---------------------------------------------------------------------------
|
|
190
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Options for `NotisRuntime.subscribeDatabase`.
|
|
207
|
+
*/
|
|
208
|
+
export interface SubscribeDatabaseOptions {
|
|
209
|
+
/**
|
|
210
|
+
* Called with `true` once a live change feed is attached, and with `false`
|
|
211
|
+
* when it drops or is torn down. Hosts without a change feed (dev harness,
|
|
212
|
+
* screenshot stub, vite preview) never call it, so `live` stays false there.
|
|
213
|
+
*/
|
|
214
|
+
onStatusChange?: (live: boolean) => void;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Work an app hands to the Notis manager chat through `NotisRuntime.handover`.
|
|
219
|
+
*/
|
|
220
|
+
export interface HandoverPayload {
|
|
221
|
+
/** The message the manager should act on. */
|
|
222
|
+
prompt: string;
|
|
223
|
+
/**
|
|
224
|
+
* Key of a skill declared in `notis.config.ts` -> `skills[].key`. The host
|
|
225
|
+
* rejects a key this app does not declare. Omit to hand over plain work.
|
|
226
|
+
*/
|
|
227
|
+
skill?: string;
|
|
228
|
+
/**
|
|
229
|
+
* Accepted for forward compatibility. The portal never submits a composer on
|
|
230
|
+
* the user's behalf today, so every handover resolves `drafted`; `sent` is
|
|
231
|
+
* reserved for a host that can genuinely dispatch the run.
|
|
232
|
+
*/
|
|
233
|
+
autoSend?: boolean;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface HandoverResult {
|
|
237
|
+
/** `drafted` when the user still has to press send, `sent` when it went straight through. */
|
|
238
|
+
status: 'drafted' | 'sent';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The user's cloud computer, as far as an app may see it.
|
|
243
|
+
*
|
|
244
|
+
* `exists` is false when the user has never had a sandbox provisioned. A
|
|
245
|
+
* `status` of anything other than `'running'` means the VM is asleep; reading
|
|
246
|
+
* these facts never wakes it.
|
|
247
|
+
*/
|
|
248
|
+
export interface CloudComputerSandboxFacts {
|
|
249
|
+
exists: boolean;
|
|
250
|
+
status: string | null;
|
|
251
|
+
provider: string | null;
|
|
252
|
+
created_at: string | null;
|
|
253
|
+
updated_at: string | null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Whether a CLI inside the cloud computer is signed in.
|
|
258
|
+
*
|
|
259
|
+
* `authenticated: null` means *unknown*, never *signed out*: the sandbox was
|
|
260
|
+
* not running, or the probe could not answer. `reason` says which
|
|
261
|
+
* (`'sandbox_not_running'`, `'no_sandbox'`, `'sandbox_status_unknown'`,
|
|
262
|
+
* `'probe_failed'`, `'not_signed_in'`).
|
|
263
|
+
*/
|
|
264
|
+
export interface CloudComputerCliAuthFacts {
|
|
265
|
+
authenticated: boolean | null;
|
|
266
|
+
account: string | null;
|
|
267
|
+
checked_at: string | null;
|
|
268
|
+
reason: string | null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface CloudComputerFacts {
|
|
272
|
+
/**
|
|
273
|
+
* False when this host cannot answer at all — no cloud computer on the user's
|
|
274
|
+
* plan, or the platform could not resolve the facts. Render whatever the app
|
|
275
|
+
* did before rather than an error.
|
|
276
|
+
*/
|
|
277
|
+
available: boolean;
|
|
278
|
+
reason?: string | null;
|
|
279
|
+
sandbox: CloudComputerSandboxFacts | null;
|
|
280
|
+
cli_auth: { gh: CloudComputerCliAuthFacts };
|
|
281
|
+
}
|
|
282
|
+
|
|
191
283
|
export interface NotisRuntime {
|
|
192
284
|
app: AppDescriptor;
|
|
193
285
|
route: RouteDescriptor;
|
|
@@ -195,6 +287,45 @@ export interface NotisRuntime {
|
|
|
195
287
|
context: NotisRuntimeContext;
|
|
196
288
|
ui?: NotisRuntimeUI;
|
|
197
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Subscribe to changes on an app-owned database. Returns an unsubscribe.
|
|
292
|
+
*
|
|
293
|
+
* The change notification is only a signal — it carries no rows. Consumers
|
|
294
|
+
* react by refetching through the normal tool path, so app scoping,
|
|
295
|
+
* permissions and billing are unchanged. Use the `useDatabaseSubscription`
|
|
296
|
+
* hook rather than calling this directly.
|
|
297
|
+
*/
|
|
298
|
+
subscribeDatabase?(
|
|
299
|
+
slug: string,
|
|
300
|
+
onChange: () => void,
|
|
301
|
+
options?: SubscribeDatabaseOptions,
|
|
302
|
+
): () => void;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Hand a piece of work to the Notis manager chat. The app cannot run an
|
|
306
|
+
* agent itself: it describes the job, and the manager surface owns progress,
|
|
307
|
+
* billing, cancellation and the transcript. Results come back to the app
|
|
308
|
+
* through its own databases (see `useDatabaseSubscription`).
|
|
309
|
+
*
|
|
310
|
+
* Use the `useHandover` hook rather than calling this directly. Hosts
|
|
311
|
+
* without a manager chat (the dev harness, the vite preview) leave it
|
|
312
|
+
* undefined, so keep whatever fallback the app already offers.
|
|
313
|
+
*/
|
|
314
|
+
handover?(payload: HandoverPayload): Promise<HandoverResult>;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Read-only facts about the user's cloud computer. Requires
|
|
318
|
+
* `capabilities.cloudComputer: 'read'` in `notis.config.ts` plus the user's
|
|
319
|
+
* approval; resolving it never creates, resumes or commands a sandbox.
|
|
320
|
+
*
|
|
321
|
+
* Use the `useCloudComputer` hook rather than calling this directly. Hosts
|
|
322
|
+
* without a cloud computer (the dev harness, the vite preview) answer
|
|
323
|
+
* `{ available: false }`, so keep whatever fallback the app already has.
|
|
324
|
+
* `{ refresh: true }` bypasses the host's short answer cache — the hook's
|
|
325
|
+
* refresh() sends it so a just-completed sign-in becomes visible.
|
|
326
|
+
*/
|
|
327
|
+
cloudComputerFacts?(options?: { refresh?: boolean }): Promise<CloudComputerFacts>;
|
|
328
|
+
|
|
198
329
|
navigate?: (payload: { kind: string; [key: string]: unknown }) => void;
|
|
199
330
|
|
|
200
331
|
registerTopBarSearch?: (
|