@notis_ai/cli 0.2.0-beta.175.1 → 0.2.0-beta.178.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 (59) hide show
  1. package/README.md +25 -39
  2. package/dist/agent-hooks/notis-agent-hook.mjs +197 -155
  3. package/{template/packages → dist}/sdk/src/config.ts +4 -0
  4. package/{template/packages → dist}/sdk/src/hooks/useNotis.ts +3 -1
  5. package/{template/packages → dist}/sdk/src/index.ts +1 -0
  6. package/{template/packages → dist}/sdk/src/runtime.ts +13 -0
  7. package/{template/packages → dist}/sdk/src/styles.css +72 -74
  8. package/package.json +1 -1
  9. package/skills/notis-apps/cli.md +23 -37
  10. package/src/command-specs/apps.js +18 -14
  11. package/src/command-specs/diagnostics.js +15 -14
  12. package/src/command-specs/reports.js +61 -50
  13. package/src/runtime/agent-browser.js +10 -5
  14. package/src/runtime/app-platform.js +27 -3
  15. package/src/runtime/app-test-server.js +2 -1
  16. package/template/.harness/index.html.tmpl +12 -4
  17. package/template/packages/sdk/tsconfig.json +0 -16
  18. /package/{template/packages → dist}/sdk/package.json +0 -0
  19. /package/{template/packages → dist}/sdk/src/agentContext.ts +0 -0
  20. /package/{template/packages → dist}/sdk/src/components/Dialog.tsx +0 -0
  21. /package/{template/packages → dist}/sdk/src/components/DocumentEditor.tsx +0 -0
  22. /package/{template/packages → dist}/sdk/src/components/Markdown.tsx +0 -0
  23. /package/{template/packages → dist}/sdk/src/components/MarkdownEditor.tsx +0 -0
  24. /package/{template/packages → dist}/sdk/src/components/MultiSelectActionBar.tsx +0 -0
  25. /package/{template/packages → dist}/sdk/src/components/MultiSelectCheckbox.tsx +0 -0
  26. /package/{template/packages → dist}/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
  27. /package/{template/packages → dist}/sdk/src/components/NotisCommentBoundary.tsx +0 -0
  28. /package/{template/packages → dist}/sdk/src/components/NotisSelectionBoundary.tsx +0 -0
  29. /package/{template/packages → dist}/sdk/src/components/ShortcutHints.tsx +0 -0
  30. /package/{template/packages → dist}/sdk/src/components/Skeleton.tsx +0 -0
  31. /package/{template/packages → dist}/sdk/src/documents.ts +0 -0
  32. /package/{template/packages → dist}/sdk/src/hooks/useActiveResource.ts +0 -0
  33. /package/{template/packages → dist}/sdk/src/hooks/useAgentContext.ts +0 -0
  34. /package/{template/packages → dist}/sdk/src/hooks/useBackend.ts +0 -0
  35. /package/{template/packages → dist}/sdk/src/hooks/useCloudComputer.ts +0 -0
  36. /package/{template/packages → dist}/sdk/src/hooks/useCollectionInteractions.ts +0 -0
  37. /package/{template/packages → dist}/sdk/src/hooks/useDatabaseSchema.ts +0 -0
  38. /package/{template/packages → dist}/sdk/src/hooks/useDatabaseSubscription.ts +0 -0
  39. /package/{template/packages → dist}/sdk/src/hooks/useDocument.ts +0 -0
  40. /package/{template/packages → dist}/sdk/src/hooks/useDocuments.ts +0 -0
  41. /package/{template/packages → dist}/sdk/src/hooks/useHandover.ts +0 -0
  42. /package/{template/packages → dist}/sdk/src/hooks/useLongPressSelection.ts +0 -0
  43. /package/{template/packages → dist}/sdk/src/hooks/useMultiSelect.ts +0 -0
  44. /package/{template/packages → dist}/sdk/src/hooks/useNotisNavigation.ts +0 -0
  45. /package/{template/packages → dist}/sdk/src/hooks/useQuery.ts +0 -0
  46. /package/{template/packages → dist}/sdk/src/hooks/useTool.ts +0 -0
  47. /package/{template/packages → dist}/sdk/src/hooks/useToolQuery.ts +0 -0
  48. /package/{template/packages → dist}/sdk/src/hooks/useTools.ts +0 -0
  49. /package/{template/packages → dist}/sdk/src/hooks/useTopBarSearch.ts +0 -0
  50. /package/{template/packages → dist}/sdk/src/hooks/useUpsertDocument.ts +0 -0
  51. /package/{template/packages → dist}/sdk/src/interactions/actions.ts +0 -0
  52. /package/{template/packages → dist}/sdk/src/interactions/shortcuts.tsx +0 -0
  53. /package/{template/packages → dist}/sdk/src/interactions/visibility.ts +0 -0
  54. /package/{template/packages → dist}/sdk/src/interactions.ts +0 -0
  55. /package/{template/packages → dist}/sdk/src/provider.tsx +0 -0
  56. /package/{template/packages → dist}/sdk/src/queryCache.ts +0 -0
  57. /package/{template/packages → dist}/sdk/src/tailwind.ts +0 -0
  58. /package/{template/packages → dist}/sdk/src/ui.ts +0 -0
  59. /package/{template/packages → dist}/sdk/src/vite.ts +0 -0
@@ -196,6 +196,10 @@ export interface NotisAppToolBinding {
196
196
  }
197
197
 
198
198
  export interface NotisAppConfig {
199
+ /** Standalone reports share the view engine without app installation. */
200
+ kind?: 'app' | 'report';
201
+ /** Existing data sources, never owned or materialized by a report. */
202
+ databaseAccess?: Array<{ id: string; access: 'read' | 'write' }>;
199
203
  /** URL-safe app slug. Existing apps may still use a display name here. */
200
204
  name: string;
201
205
  /** Human display title, Raycast-style. Falls back to `name`. */
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import { useNotisRuntime } from '../provider';
4
- import type { AppDescriptor, CollectionItemDetail, DatabaseDescriptor, RouteDescriptor } from '../runtime';
4
+ import type { RuntimeResource, AppDescriptor, CollectionItemDetail, DatabaseDescriptor, RouteDescriptor } from '../runtime';
5
5
 
6
6
  interface NotisContext {
7
+ resource: RuntimeResource | null;
7
8
  /** App metadata (id, name, icon, description). Null before runtime loads. */
8
9
  app: AppDescriptor | null;
9
10
  /** Current route descriptor. Null before runtime loads. */
@@ -27,6 +28,7 @@ export function useNotis(): NotisContext {
27
28
  const runtime = useNotisRuntime();
28
29
 
29
30
  return {
31
+ resource: runtime?.resource ?? null,
30
32
  app: runtime?.app ?? null,
31
33
  route: runtime?.route ?? null,
32
34
  databases: runtime?.databases ?? [],
@@ -139,6 +139,7 @@ export type {
139
139
  NotisMarkdownEditorSavePayload,
140
140
  NotisMarkdownEditorSaveResult,
141
141
  NotisRuntime,
142
+ RuntimeResource,
142
143
  NotisRuntimeContext,
143
144
  NotisRuntimeUI,
144
145
  QueryFilter,
@@ -364,7 +364,20 @@ export interface CloudComputerFacts {
364
364
  cli_auth: { gh: CloudComputerCliAuthFacts };
365
365
  }
366
366
 
367
+ export interface RuntimeResource {
368
+ kind: 'app' | 'report';
369
+ /** Host-provided adoption eligibility, never report-authored authority. */
370
+ analytics_eligible?: boolean;
371
+ id: string;
372
+ revision: number;
373
+ name?: string;
374
+ icon?: string | null;
375
+ description?: string | null;
376
+ }
377
+
367
378
  export interface NotisRuntime {
379
+ /** Authenticated runtime identity; app below is presentation metadata only. */
380
+ resource?: RuntimeResource;
368
381
  /** Optional host-scoped in-memory read cache. Older hosts remain supported. */
369
382
  queryClient?: NotisQueryClient;
370
383
  app: AppDescriptor;
@@ -30,93 +30,91 @@ dialog.notis-dialog::backdrop { background: rgb(0 0 0 / 0.45); }
30
30
  .notis-dialog-description { margin: 0.5rem 0 0; color: hsl(var(--muted-foreground)); font-size: 0.875rem; line-height: 1.5; }
31
31
 
32
32
  /* Framework-neutral styles: the app owns its Tailwind entry point. This file
33
- * is also consumed in shadow roots and by apps on older Tailwind versions. */
34
- @layer base {
35
- * {
36
- border-color: hsl(var(--border));
37
- }
38
- [data-notis-app-root] {
39
- color: hsl(var(--foreground));
40
- display: block;
41
- min-height: 100%;
42
- }
33
+ * is also consumed in shadow roots and by apps on older Tailwind versions.
34
+ * Keep rules outside Tailwind-owned @layer blocks: Vite processes this import
35
+ * separately from the app stylesheet, so Tailwind 3 has no matching directives. */
36
+ * {
37
+ border-color: hsl(var(--border));
38
+ }
39
+ [data-notis-app-root] {
40
+ color: hsl(var(--foreground));
41
+ display: block;
42
+ min-height: 100%;
43
43
  }
44
44
 
45
- @layer components {
46
- .notis-app-shell {
47
- margin-inline: auto;
48
- width: 100%;
49
- max-width: var(--portal-page-max-width);
50
- padding: 1.5rem 1rem;
51
- }
45
+ .notis-app-shell {
46
+ margin-inline: auto;
47
+ width: 100%;
48
+ max-width: var(--portal-page-max-width);
49
+ padding: 1.5rem 1rem;
50
+ }
52
51
 
53
- /* Flat panel: no border or shadow around items. */
54
- .notis-app-surface {
55
- border-radius: 1rem;
56
- background-color: hsl(var(--muted));
57
- color: hsl(var(--card-foreground));
58
- }
52
+ /* Flat panel: no border or shadow around items. */
53
+ .notis-app-surface {
54
+ border-radius: 1rem;
55
+ background-color: hsl(var(--muted));
56
+ color: hsl(var(--card-foreground));
57
+ }
59
58
 
60
- .list-row {
61
- border-radius: 0.75rem;
62
- background-color: hsl(var(--muted));
63
- padding: 0.75rem 1rem;
64
- transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
65
- transition-duration: 150ms;
66
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
67
- }
59
+ .list-row {
60
+ border-radius: 0.75rem;
61
+ background-color: hsl(var(--muted));
62
+ padding: 0.75rem 1rem;
63
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
64
+ transition-duration: 150ms;
65
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
66
+ }
68
67
 
69
- .list-row-selected {
70
- background-color: hsl(var(--primary) / 0.06);
71
- }
68
+ .list-row-selected {
69
+ background-color: hsl(var(--primary) / 0.06);
70
+ }
72
71
 
73
- /* Split pages are full-bleed; only the list pane carries a hairline. */
74
- .notis-app-split {
75
- display: flex;
76
- height: 100%;
77
- min-height: 0;
78
- width: 100%;
79
- max-width: none;
80
- flex-direction: column;
81
- }
72
+ /* Split pages are full-bleed; only the list pane carries a hairline. */
73
+ .notis-app-split {
74
+ display: flex;
75
+ height: 100%;
76
+ min-height: 0;
77
+ width: 100%;
78
+ max-width: none;
79
+ flex-direction: column;
80
+ }
82
81
 
83
- .notis-app-pane-list {
84
- width: 100%;
85
- flex-shrink: 0;
86
- background-color: hsl(var(--muted) / 0.4);
87
- }
82
+ .notis-app-pane-list {
83
+ width: 100%;
84
+ flex-shrink: 0;
85
+ background-color: hsl(var(--muted) / 0.4);
86
+ }
88
87
 
89
- .notis-app-pane-detail {
90
- min-width: 0;
91
- flex: 1 1 0%;
92
- background-color: hsl(var(--background));
93
- }
88
+ .notis-app-pane-detail {
89
+ min-width: 0;
90
+ flex: 1 1 0%;
91
+ background-color: hsl(var(--background));
92
+ }
94
93
 
95
- .notis-app-section {
96
- padding: 1.25rem;
97
- }
94
+ .notis-app-section {
95
+ padding: 1.25rem;
96
+ }
98
97
 
99
- @media (min-width: 768px) {
100
- .notis-app-shell { padding-block: 2rem; }
101
- .notis-app-section { padding: 1.5rem; }
102
- }
98
+ @media (min-width: 768px) {
99
+ .notis-app-shell { padding-block: 2rem; }
100
+ .notis-app-section { padding: 1.5rem; }
101
+ }
103
102
 
104
- @media (min-width: 1024px) {
105
- .list-row { background-color: transparent; }
106
- .list-row:hover { background-color: hsl(var(--muted) / 0.6); }
107
- .list-row-selected,
108
- .list-row-selected:hover { background-color: hsl(var(--primary) / 0.06); }
109
- .notis-app-split { flex-direction: row; }
110
- .notis-app-pane-list {
111
- width: 20rem;
112
- border-right-width: 1px;
113
- border-right-color: hsl(var(--border));
114
- }
103
+ @media (min-width: 1024px) {
104
+ .list-row { background-color: transparent; }
105
+ .list-row:hover { background-color: hsl(var(--muted) / 0.6); }
106
+ .list-row-selected,
107
+ .list-row-selected:hover { background-color: hsl(var(--primary) / 0.06); }
108
+ .notis-app-split { flex-direction: row; }
109
+ .notis-app-pane-list {
110
+ width: 20rem;
111
+ border-right-width: 1px;
112
+ border-right-color: hsl(var(--border));
115
113
  }
114
+ }
116
115
 
117
- @media (min-width: 1280px) {
118
- .notis-app-pane-list { width: 24rem; }
119
- }
116
+ @media (min-width: 1280px) {
117
+ .notis-app-pane-list { width: 24rem; }
120
118
  }
121
119
 
122
120
  /*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notis_ai/cli",
3
- "version": "0.2.0-beta.175.1",
3
+ "version": "0.2.0-beta.178.1",
4
4
  "description": "Agent-first Notis CLI for apps and generic tool execution",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@ change the package release label, check resource compatibility and deploy as a n
28
28
 
29
29
  ## Reports
30
30
 
31
- For a record-owned report, use `reports init → build → verify/preview → save` instead of the app deployment workflow above. Author exactly one route, select an existing app-owned database, and supply a readable context file. Revisions preserve the record ID and require its freshly read revision. See the product `notis-reports` skill for source recovery, ownership and readback. These commands do not deploy the owning app or publish a Store listing.
31
+ For a standalone live report, use `reports init → build → verify/preview → save` instead of the app deployment workflow above. Author exactly one route with kind: report, declare its tools, and supply a readable context file. No installed app or database is required. Revisions preserve the record ID and require its freshly read revision. See the product `notis-reports` skill for source recovery, ownership and readback. These commands do not deploy an app or publish a Store listing.
32
32
 
33
33
  ## Commands
34
34
 
@@ -229,85 +229,71 @@ Examples:
229
229
 
230
230
  ### `npx --package @notis_ai/cli@latest -- notis reports init <name> [dir]`
231
231
 
232
- Init a record-owned SDK report locally.
232
+ Init a standalone live SDK report.
233
233
 
234
- When to use: Author an independent report without deploying its owning app.
235
-
236
- Options:
237
- - `--from <slug>` — Start from a published Store app listed by `notis apps scaffolds list`. Downloads its source from the public app registry.
234
+ When to use: Author a private standalone document using the shared view runtime; no app or database is required.
238
235
 
239
236
  Examples:
240
- - `npx --package @notis_ai/cli@latest -- notis apps scaffolds list`
241
- - `npx --package @notis_ai/cli@latest -- notis reports init "Mind the Flo"`
242
- - `npx --package @notis_ai/cli@latest -- notis reports init "My CRM" --from databases`
243
- - `npx --package @notis_ai/cli@latest -- notis reports init "My App" ~/code/my-app`
237
+ - `npx --package @notis_ai/cli@latest -- notis reports init "Weekly report" ./weekly-report`
244
238
 
245
239
  ### `npx --package @notis_ai/cli@latest -- notis reports build [dir]`
246
240
 
247
- Build a record-owned SDK report locally.
241
+ Build a standalone live SDK report.
248
242
 
249
- When to use: Author an independent report without deploying its owning app.
243
+ When to use: Author a private standalone document using the shared view runtime; no app or database is required.
250
244
 
251
245
  Examples:
252
- - `npx --package @notis_ai/cli@latest -- notis reports build`
253
- - `npx --package @notis_ai/cli@latest -- notis reports build ./my-app`
246
+ - `npx --package @notis_ai/cli@latest -- notis reports build ./weekly-report`
254
247
 
255
248
  ### `npx --package @notis_ai/cli@latest -- notis reports verify [dir]`
256
249
 
257
- Verify a record-owned SDK report locally.
250
+ Verify a standalone live SDK report.
258
251
 
259
- When to use: Author an independent report without deploying its owning app.
252
+ When to use: Author a private standalone document using the shared view runtime; no app or database is required.
260
253
 
261
254
  Options:
262
255
  - `--routes <slugs>` — Comma-separated route slugs. Default: every route in manifest.
263
256
  - `--port <n>` — Loopback port. Default: auto-pick.
264
257
  - `--skip-build` — Skip notis apps build; reuse existing .notis/output/.
265
258
  - `--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.
266
- - `--listing` — Ignored for reports; saving a report does not publish a Store listing.
267
259
  - `--no-browser` — Start the harness server and print URLs; do not drive agent-browser.
268
260
  - `--keep-open` — Leave server + browser session running after report (for manual triage).
261
+ - `--document-id <id>` — Saved report for live verification.
262
+ - `--expected-revision <revision>` — Saved report revision for live verification.
269
263
 
270
264
  Examples:
271
- - `npx --package @notis_ai/cli@latest -- notis reports verify`
272
- - `npx --package @notis_ai/cli@latest -- notis reports verify --routes notes`
273
- - `npx --package @notis_ai/cli@latest -- notis reports verify --mode live`
274
- - `npx --package @notis_ai/cli@latest -- notis reports verify --no-browser # start the harness, drive agent-browser yourself`
265
+ - `npx --package @notis_ai/cli@latest -- notis reports verify ./weekly-report`
275
266
 
276
267
  ### `npx --package @notis_ai/cli@latest -- notis reports preview [dir]`
277
268
 
278
- Preview a record-owned SDK report locally. Keeps the preview server and browser session open.
269
+ Preview a standalone live SDK report.
279
270
 
280
- When to use: Author an independent report without deploying its owning app.
271
+ When to use: Author a private standalone document using the shared view runtime; no app or database is required.
281
272
 
282
273
  Options:
283
274
  - `--routes <slugs>` — Comma-separated route slugs. Default: every route in manifest.
284
275
  - `--port <n>` — Loopback port. Default: auto-pick.
285
276
  - `--skip-build` — Skip notis apps build; reuse existing .notis/output/.
286
277
  - `--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.
287
- - `--listing` — Ignored for reports; saving a report does not publish a Store listing.
288
278
  - `--no-browser` — Start the harness server and print URLs; do not drive agent-browser.
289
279
  - `--keep-open` — Leave server + browser session running after report (for manual triage).
280
+ - `--document-id <id>` — Saved report for live verification.
281
+ - `--expected-revision <revision>` — Saved report revision for live verification.
290
282
 
291
283
  Examples:
292
- - `npx --package @notis_ai/cli@latest -- notis reports preview`
293
- - `npx --package @notis_ai/cli@latest -- notis reports preview --routes notes`
294
- - `npx --package @notis_ai/cli@latest -- notis reports preview --mode live`
295
- - `npx --package @notis_ai/cli@latest -- notis reports preview --no-browser # start the harness, drive agent-browser yourself`
284
+ - `npx --package @notis_ai/cli@latest -- notis reports preview ./weekly-report`
296
285
 
297
286
  ### `npx --package @notis_ai/cli@latest -- notis reports save [dir]`
298
287
 
299
- Build, verify and save a report into an app-owned database record.
288
+ Build, verify and save a standalone live report document.
300
289
 
301
- When to use: Persist an independently authored report, not an app release.
290
+ When to use: Persist an independently owned report, not an app release.
302
291
 
303
292
  Options:
304
- - `--database-id <id>` — Required. Owning app database.
305
- - `--document-id <id>` — Existing record to update or attach to.
306
- - `--attach` — Attach to an existing non-view record.
307
- - `--expected-revision <revision>` — Fresh view revision (0 for a record without a view).
308
- - `--title <title>` — Required, including updates. Record title.
293
+ - `--document-id <id>` — Existing report document to update.
294
+ - `--expected-revision <revision>` — Current saved report revision; required for updates.
295
+ - `--title <title>` — Required, including updates. Document title.
309
296
  - `--context-file <file>` — Required. UTF-8 readable report content and structure.
310
- - `--properties-file <file>` — JSON database property values keyed by name.
311
297
 
312
298
  Examples:
313
- - `npx --package @notis_ai/cli@latest -- notis reports save ./weekly-report --database-id <id> --title "Weekly review" --context-file ./context.md`
299
+ - `npx --package @notis_ai/cli@latest -- notis reports save ./weekly-report --title "Weekly review" --context-file ./context.md`
@@ -276,6 +276,9 @@ function assertHarnessResult(result, route, databaseSlugs, mode = 'stub', capabi
276
276
  });
277
277
  }
278
278
  if (mode === 'live') {
279
+ if (runtimeCalls.some(call => call?.ok == null)) {
280
+ assertions.push({ ok: false, code: 'runtime_calls_pending', message: 'Live verification ended before every started runtime call settled.' });
281
+ }
279
282
  // In live mode an app that catches every failed call and renders its error
280
283
  // state still mounts cleanly, so the render assertions above all pass. Only
281
284
  // the recorded outcomes reveal that nothing real came back.
@@ -379,7 +382,7 @@ async function appsListHandler(ctx) {
379
382
  });
380
383
  }
381
384
 
382
- async function appsInitHandler(ctx) {
385
+ export async function appsInitHandler(ctx) {
383
386
  const projectDir = ctx.args.dir
384
387
  ? resolveProjectDir(ctx.args.dir)
385
388
  : defaultAppProjectDir(slugify(ctx.args.name));
@@ -504,7 +507,7 @@ async function appsCreateHandler(ctx) {
504
507
  });
505
508
  }
506
509
 
507
- async function appsBuildHandler(ctx) {
510
+ export async function appsBuildHandler(ctx) {
508
511
  const projectDir = resolveProjectDir(ctx.args.dir || '.');
509
512
  const problems = detectProjectProblems(projectDir);
510
513
  if (problems.length) {
@@ -563,13 +566,15 @@ async function closeHarnessResources(sessionNames, testServer, rawOutputDir = nu
563
566
  }
564
567
  }
565
568
 
566
- async function appsVerifyHandler(ctx) {
569
+ export async function appsVerifyHandler(ctx) {
567
570
  const projectDir = resolveProjectDir(ctx.args.dir || '.');
568
571
  const problems = detectProjectProblems(projectDir);
569
572
  if (problems.length) {
570
573
  throw usageError(`Project has problems:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
571
574
  }
572
575
 
576
+ const appConfig = await loadAppConfig(projectDir);
577
+ const isReport = appConfig.kind === 'report';
573
578
  const mode = ctx.options.mode || 'stub';
574
579
  if (!['stub', 'live'].includes(mode)) {
575
580
  throw usageError('--mode must be either "stub" or "live".');
@@ -586,8 +591,10 @@ async function appsVerifyHandler(ctx) {
586
591
  if (!ctx.runtime.jwt) {
587
592
  throw usageError('Live verify mode requires CLI auth. Run notis login and retry.');
588
593
  }
589
- linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
590
- if (!linkedState?.app_id) {
594
+ if (isReport) {
595
+ if (!ctx.options.documentId || !/^\d+$/.test(String(ctx.options.expectedRevision ?? ''))) throw usageError('Live report verification requires --document-id and --expected-revision of the saved report.');
596
+ } else linkedState = readLinkedState(projectDir, linkedStateProfileKey(ctx.runtime));
597
+ if (!isReport && !linkedState?.app_id) {
591
598
  throw usageError('Live verify mode requires a linked app. Run `notis apps link <app-id> .` first.');
592
599
  }
593
600
  }
@@ -599,8 +606,7 @@ async function appsVerifyHandler(ctx) {
599
606
  }
600
607
 
601
608
  const manifest = readManifest(projectDir);
602
- const appConfig = await loadAppConfig(projectDir);
603
- const listing = inspectListingReadiness(projectDir, appConfig);
609
+ const listing = isReport ? { ready: true, errors: [], warnings: [] } : inspectListingReadiness(projectDir, appConfig);
604
610
  // Store readiness is a publish concern, not a render concern. Verify reports
605
611
  // it so the gaps stay visible while the app is still being built; only
606
612
  // --listing (and `apps publish`) turn it back into a hard gate.
@@ -609,7 +615,7 @@ async function appsVerifyHandler(ctx) {
609
615
  }
610
616
  const listingWarnings = [
611
617
  ...[...listing.errors, ...listing.warnings].map((message) => `Store readiness: ${message}`),
612
- ...findUnknownScreenshotScenarios(projectDir, resolveListingScreenshots(projectDir, appConfig)),
618
+ ...(isReport ? [] : findUnknownScreenshotScenarios(projectDir, resolveListingScreenshots(projectDir, appConfig))),
613
619
  ];
614
620
  const routes = routeSelection(manifest, parseRouteSlugs(ctx.options.routes));
615
621
  const port = parsePort(ctx.options.port) || await getAvailablePort();
@@ -635,6 +641,7 @@ async function appsVerifyHandler(ctx) {
635
641
  slug: appSlug,
636
642
  projectDir,
637
643
  appId: linkedState?.app_id || 'harness-app',
644
+ resource: isReport ? { kind: 'report', id: ctx.options.documentId || 'preview', revision: Number(ctx.options.expectedRevision || 0) } : undefined,
638
645
  }],
639
646
  port,
640
647
  harness: {
@@ -708,7 +715,8 @@ async function appsVerifyHandler(ctx) {
708
715
  const result = await runHarnessRoute({
709
716
  url,
710
717
  sessionName: browserSessionName,
711
- timeoutMs: Number.parseInt(ctx.globalOptions.timeoutMs || '', 10) || 10_000,
718
+ timeoutMs: Number.parseInt(ctx.globalOptions.timeoutMs || '', 10) || (mode === 'live' ? 90_000 : 10_000),
719
+ waitForRuntime: mode === 'live',
712
720
  snapshotPath,
713
721
  });
714
722
  const assertions = assertHarnessResult(
@@ -766,11 +774,7 @@ async function appsVerifyHandler(ctx) {
766
774
  },
767
775
  summary,
768
776
  results,
769
- listing: {
770
- ready: listing.ready,
771
- gated: ctx.options.listing === true,
772
- problems: listing.errors,
773
- },
777
+ ...(!isReport ? { listing: { ready: listing.ready, gated: ctx.options.listing === true, problems: listing.errors } } : {}),
774
778
  };
775
779
 
776
780
  if (!keepOpen) await cleanup();
@@ -58,7 +58,8 @@ async function discoverSupabaseSqlTool(runtime) {
58
58
 
59
59
  export function unwrapToolExecutionPayload(payload) {
60
60
  return (
61
- payload?.data?.results?.[0]?.response?.data
61
+ payload?.data?.data?.results?.[0]?.response?.data
62
+ ?? payload?.data?.results?.[0]?.response?.data
62
63
  ?? payload?.results?.[0]?.response?.data
63
64
  ?? payload?.data?.result
64
65
  ?? payload?.result
@@ -74,30 +75,30 @@ function parseJsonCandidate(value) {
74
75
  return JSON.parse(trimmed);
75
76
  } catch {
76
77
  const boundaryEnd = trimmed.lastIndexOf('</untrusted-data');
77
- const boundaryStart = boundaryEnd >= 0
78
- ? trimmed.lastIndexOf('<untrusted-data', boundaryEnd)
79
- : -1;
80
- const contentStart = boundaryStart >= 0 ? trimmed.indexOf('>', boundaryStart) + 1 : -1;
81
- if (contentStart > 0 && boundaryEnd > contentStart) {
78
+ const boundaryName = boundaryEnd >= 0
79
+ ? trimmed.slice(boundaryEnd + 2, trimmed.indexOf('>', boundaryEnd)) : null;
80
+ const opening = boundaryName ? `<${boundaryName}>` : null;
81
+ for (let boundaryStart = opening ? trimmed.indexOf(opening) : -1;
82
+ boundaryStart >= 0 && boundaryStart < boundaryEnd;
83
+ boundaryStart = trimmed.indexOf(opening, boundaryStart + opening.length)) {
84
+ const contentStart = boundaryStart + opening.length;
82
85
  // Supabase MCP sometimes returns the prose envelope with literal "\\n"
83
86
  // separators. Strip those only at the boundary: globally replacing them
84
87
  // corrupts valid escaped newlines inside JSON string values.
85
88
  const inner = trimmed
86
89
  .slice(contentStart, boundaryEnd)
87
90
  .trim()
88
- .replace(/^(?:\\r?\\n)+/, '')
89
- .replace(/(?:\\r?\\n)+$/, '')
91
+ .replace(/^(?:(?:\\r)?\\n)+/, '')
92
+ .replace(/(?:(?:\\r)?\\n)+$/, '')
90
93
  .trim();
91
94
  try {
92
95
  return JSON.parse(inner);
93
- } catch (error) {
94
- const nested = parseJsonCandidate(inner);
95
- if (nested !== null) return nested;
96
- throw usageError(
97
- `Supabase diagnostic rows were not valid JSON: ${error instanceof Error ? error.message : String(error)}; prefix=${JSON.stringify(inner.slice(0, 100))}`,
98
- );
96
+ } catch {
97
+ // The provider also mentions this marker in its introductory prose.
98
+ // Try its actual opening, never a nested tool result's different marker.
99
99
  }
100
100
  }
101
+ if (boundaryEnd >= 0) throw usageError('Supabase diagnostic rows were not valid JSON.');
101
102
  const arrayMatch = trimmed.match(/(\[[\s\S]*\])/);
102
103
  if (arrayMatch) {
103
104
  try {