@notis_ai/cli 0.2.7 → 0.2.8

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 (177) hide show
  1. package/README.md +15 -0
  2. package/dist/scaffolds/notis-database/app/page.tsx +38 -41
  3. package/dist/scaffolds/notis-database/lib/types.ts +5 -7
  4. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  5. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  6. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  7. package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
  8. package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
  9. package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +1839 -0
  10. package/dist/scaffolds/notis-database/notis.config.ts +48 -1
  11. package/dist/scaffolds/notis-database/package-lock.json +3935 -0
  12. package/dist/scaffolds/notis-database/package.json +2 -1
  13. package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/package.json +4 -0
  14. package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  15. package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +60 -0
  16. package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectActionBar.tsx +7 -2
  17. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +196 -0
  18. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +229 -0
  19. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  20. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +78 -0
  21. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +121 -0
  22. package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useMultiSelect.ts +38 -2
  23. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  24. package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/index.ts +30 -1
  25. package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/runtime.ts +76 -17
  26. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +186 -0
  27. package/dist/scaffolds/notis-database/src/mock-runtime.ts +4 -1
  28. package/dist/scaffolds/notis-journal/CHANGELOG.md +4 -0
  29. package/dist/scaffolds/notis-journal/app/page.tsx +1 -0
  30. package/dist/scaffolds/notis-journal/package-lock.json +4615 -0
  31. package/dist/scaffolds/notis-journal/package.json +2 -2
  32. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +52 -1
  33. package/dist/scaffolds/notis-notes/app/page.tsx +39 -46
  34. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +596 -0
  35. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +144 -0
  36. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  37. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  38. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  39. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  40. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  41. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +752 -0
  42. package/dist/scaffolds/notis-notes/notis.config.ts +48 -3
  43. package/dist/scaffolds/notis-notes/package-lock.json +4636 -0
  44. package/dist/scaffolds/notis-notes/package.json +7 -3
  45. package/dist/scaffolds/notis-notes/packages/sdk/package.json +36 -0
  46. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  47. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +60 -0
  48. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  49. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  50. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  51. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +196 -0
  52. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +229 -0
  53. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +41 -0
  54. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  55. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +78 -0
  56. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +121 -0
  57. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  58. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +34 -0
  59. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  60. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +64 -0
  61. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +56 -0
  62. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  63. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  64. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +83 -0
  65. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +43 -0
  66. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +220 -0
  67. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +186 -0
  68. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +15 -0
  69. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +56 -0
  70. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +15 -0
  71. package/dist/scaffolds/notis-random/CHANGELOG.md +12 -2
  72. package/dist/scaffolds/notis-random/app/history/page.tsx +5 -4
  73. package/dist/scaffolds/notis-random/app/page.tsx +141 -74
  74. package/dist/scaffolds/notis-random/components/ui/button.tsx +50 -0
  75. package/dist/scaffolds/notis-random/components/ui/card.tsx +16 -0
  76. package/dist/scaffolds/notis-random/components/ui/input.tsx +23 -0
  77. package/dist/scaffolds/notis-random/lib/notis-tools.ts +26 -7
  78. package/dist/scaffolds/notis-random/lib/rng.ts +179 -19
  79. package/dist/scaffolds/notis-random/lib/roll-record.ts +103 -16
  80. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  81. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  82. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  83. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  84. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  85. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +753 -0
  86. package/dist/scaffolds/notis-random/notis.config.ts +47 -3
  87. package/dist/scaffolds/notis-random/package-lock.json +4513 -0
  88. package/dist/scaffolds/notis-random/package.json +6 -2
  89. package/dist/scaffolds/notis-random/packages/sdk/package.json +36 -0
  90. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  91. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +60 -0
  92. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  93. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  94. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  95. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +196 -0
  96. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +229 -0
  97. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +41 -0
  98. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  99. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +78 -0
  100. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +121 -0
  101. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  102. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +34 -0
  103. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  104. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +64 -0
  105. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +56 -0
  106. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  107. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  108. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +83 -0
  109. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +43 -0
  110. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +220 -0
  111. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +186 -0
  112. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +15 -0
  113. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +56 -0
  114. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +15 -0
  115. package/dist/scaffolds/notis-random/tailwind.config.ts +8 -1
  116. package/dist/scaffolds.json +8 -17
  117. package/package.json +2 -1
  118. package/skills/notis-apps/SKILL.md +449 -144
  119. package/skills/notis-apps/cli.md +15 -0
  120. package/skills/notis-cli/SKILL.md +8 -4
  121. package/skills/notis-onboarding/BRIEF.md +93 -0
  122. package/src/cli.js +50 -12
  123. package/src/command-specs/apps.js +154 -21
  124. package/src/command-specs/index.js +2 -0
  125. package/src/command-specs/onboarding.js +216 -0
  126. package/src/command-specs/tools.js +8 -4
  127. package/src/runtime/app-platform.js +65 -4
  128. package/src/runtime/desktop-auth.js +66 -17
  129. package/src/runtime/errors.js +1 -0
  130. package/src/runtime/output.js +9 -2
  131. package/src/runtime/profiles.js +179 -17
  132. package/src/runtime/telemetry.js +92 -0
  133. package/src/runtime/transport.js +26 -1
  134. package/template/.harness/index.html.tmpl +30 -2
  135. package/template/packages/sdk/src/config.ts +38 -1
  136. package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +0 -7
  137. package/dist/scaffolds/notis-affiliate-prospects/README.md +0 -20
  138. package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +0 -42
  139. package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +0 -7
  140. package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +0 -248
  141. package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +0 -134
  142. package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +0 -108
  143. package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +0 -35
  144. package/dist/scaffolds/notis-affiliate-prospects/index.html +0 -12
  145. package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +0 -218
  146. package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +0 -52
  147. package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +0 -100
  148. package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +0 -25
  149. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
  150. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
  151. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
  152. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +0 -187
  153. package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +0 -65
  154. package/dist/scaffolds/notis-affiliate-prospects/package.json +0 -32
  155. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +0 -90
  156. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useDatabase.ts +0 -76
  157. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +0 -50
  158. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +0 -38
  159. package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +0 -6
  160. package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +0 -72
  161. package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +0 -242
  162. package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +0 -51
  163. package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +0 -23
  164. package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +0 -11
  165. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -0
  166. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
  167. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useBackend.ts +0 -0
  168. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useNotis.ts +0 -0
  169. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useNotisNavigation.ts +0 -0
  170. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTool.ts +0 -0
  171. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTools.ts +0 -0
  172. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTopBarSearch.ts +0 -0
  173. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/provider.tsx +0 -0
  174. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/ui.ts +0 -0
  175. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/vite.ts +0 -0
  176. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/tsconfig.json +0 -0
  177. /package/dist/scaffolds/{notis-affiliate-prospects → notis-random}/components.json +0 -0
@@ -0,0 +1,216 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { CliError, EXIT_CODES } from '../runtime/errors.js';
6
+ import { getDesktopAuthRecovery, waitForDesktopAuth } from '../runtime/desktop-auth.js';
7
+ import { getJwt, isJwtExpired, loadConfig } from '../runtime/profiles.js';
8
+
9
+ const HERE = dirname(fileURLToPath(import.meta.url));
10
+ const BUNDLED_BRIEF_PATH = join(HERE, '..', '..', 'skills', 'notis-onboarding', 'BRIEF.md');
11
+
12
+ /**
13
+ * The brief is served rather than bundled so it tracks the deployed server. The
14
+ * bundled copy is a fallback for an offline or unreachable API, and the payload
15
+ * says which one the caller got so a stale brief is diagnosable.
16
+ */
17
+ async function fetchBrief(apiBase, timeoutMs) {
18
+ try {
19
+ const controller = new AbortController();
20
+ const timer = setTimeout(() => controller.abort(), Math.min(timeoutMs || 30_000, 30_000));
21
+ try {
22
+ const response = await fetch(`${apiBase.replace(/\/$/, '')}/signup/onboarding-brief`, {
23
+ signal: controller.signal,
24
+ });
25
+ if (response.ok) {
26
+ const payload = await response.json();
27
+ if (payload?.markdown) {
28
+ return { markdown: payload.markdown, source: 'server' };
29
+ }
30
+ }
31
+ } finally {
32
+ clearTimeout(timer);
33
+ }
34
+ } catch {
35
+ // fall through to the bundled copy
36
+ }
37
+
38
+ try {
39
+ return { markdown: readFileSync(BUNDLED_BRIEF_PATH, 'utf-8'), source: 'bundled' };
40
+ } catch {
41
+ return { markdown: null, source: null };
42
+ }
43
+ }
44
+
45
+ async function requestSignupLink(apiBase, { email, useCases }) {
46
+ const response = await fetch(`${apiBase.replace(/\/$/, '')}/signup/agent`, {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({
50
+ email,
51
+ ...(useCases && useCases.length ? { preferred_use_cases: useCases } : {}),
52
+ }),
53
+ });
54
+ const payload = await response.json().catch(() => null);
55
+
56
+ if (response.status === 429) {
57
+ // Requesting another link invalidates the previous one, so retrying is
58
+ // actively harmful. Surface the cooldown and stop.
59
+ throw new CliError({
60
+ code: 'signup_throttled',
61
+ message: payload?.message || 'A sign-in link was just sent to this address.',
62
+ exitCode: EXIT_CODES.usage,
63
+ details: payload || {},
64
+ hints: [
65
+ {
66
+ command: 'Open the newest Notis email on this machine',
67
+ reason: 'Requesting another link makes earlier links stop working',
68
+ },
69
+ ],
70
+ });
71
+ }
72
+
73
+ if (!response.ok) {
74
+ throw new CliError({
75
+ code: 'signup_failed',
76
+ message: payload?.message || `Signup failed with status ${response.status}`,
77
+ exitCode: EXIT_CODES.backend,
78
+ details: payload || {},
79
+ });
80
+ }
81
+
82
+ return payload || {};
83
+ }
84
+
85
+ async function startHandler(ctx) {
86
+ const { runtime, options, output } = ctx;
87
+ const apiBase = runtime.apiBase;
88
+ const authenticated = Boolean(runtime.jwt) && !isJwtExpired(runtime.jwt);
89
+
90
+ // Safe to re-run: an already-authenticated machine skips straight to the brief.
91
+ // Agents retry commands, and a second signup would invalidate the first email.
92
+ if (authenticated || options.briefOnly) {
93
+ if (!authenticated) {
94
+ const recovery = getDesktopAuthRecovery(runtime, { mode: 'missing' });
95
+ throw new CliError({
96
+ code: 'auth_missing',
97
+ message: 'This machine is not signed in to Notis yet.',
98
+ exitCode: EXIT_CODES.auth,
99
+ hints: recovery.hints,
100
+ });
101
+ }
102
+ const brief = await fetchBrief(apiBase, runtime.timeoutMs);
103
+ return output.emitSuccess({
104
+ command: 'start',
105
+ data: { authenticated: true, brief: brief.markdown, brief_source: brief.source },
106
+ humanSummary: 'Notis CLI is authenticated on this machine.',
107
+ renderHuman: () => brief.markdown || 'Notis CLI is authenticated.',
108
+ });
109
+ }
110
+
111
+ if (!options.email) {
112
+ // The one point in the flow that has to stop and talk to the human.
113
+ throw new CliError({
114
+ code: 'signup_email_required',
115
+ message: 'An email address is required to create or access a Notis account.',
116
+ exitCode: EXIT_CODES.usage,
117
+ hints: [
118
+ {
119
+ command: 'Ask the user for their email address, then rerun with --email <address>',
120
+ reason: 'Notis sends a sign-in link there to prove they own the address',
121
+ },
122
+ ],
123
+ });
124
+ }
125
+
126
+ const signup = await requestSignupLink(apiBase, {
127
+ email: options.email,
128
+ useCases: options.useCase,
129
+ });
130
+
131
+ // Commander stores the negatable `--no-wait` flag as `options.wait === false`
132
+ // and never sets `options.noWait` — the same footgun already fixed for
133
+ // `--no-open` in apps.js. Reading `noWait` made the flag a no-op, so an agent
134
+ // that asked not to wait blocked for the full timeout instead.
135
+ if (options.wait === false) {
136
+ return output.emitSuccess({
137
+ command: 'start',
138
+ data: { authenticated: false, next_action: 'open_email_link', ...signup },
139
+ humanSummary: `A sign-in link is on its way to ${options.email}.`,
140
+ });
141
+ }
142
+
143
+ const timeoutMs = Number(options.waitTimeoutMs) > 0 ? Number(options.waitTimeoutMs) : 300_000;
144
+ const jwt = await waitForDesktopAuth({
145
+ loadConfig,
146
+ getJwt,
147
+ isJwtExpired,
148
+ profileName: runtime.profileName,
149
+ timeoutMs,
150
+ onTick: (remaining) => {
151
+ if (runtime.outputMode !== 'json' && remaining % 15_000 < 2_000) {
152
+ output.note?.(`Waiting for Notis Desktop to sign in (${Math.round(remaining / 1000)}s left)...`);
153
+ }
154
+ },
155
+ });
156
+
157
+ if (!jwt) {
158
+ throw new CliError({
159
+ code: 'auth_timeout',
160
+ message: 'Timed out waiting for Notis Desktop to authenticate the CLI.',
161
+ exitCode: EXIT_CODES.auth,
162
+ details: { desktop_download_url: signup.desktop_download_url || null },
163
+ hints: [
164
+ {
165
+ command: `Install Notis Desktop: ${signup.desktop_download_url || 'https://notis.ai/channels/desktop-app'}`,
166
+ reason: 'The desktop app is what writes the CLI credential',
167
+ },
168
+ { command: 'notis start --brief-only', reason: 'Resume once the desktop app is signed in' },
169
+ { command: 'notis doctor', reason: 'Re-check config, auth, and API reachability' },
170
+ ],
171
+ });
172
+ }
173
+
174
+ const brief = await fetchBrief(apiBase, runtime.timeoutMs);
175
+ return output.emitSuccess({
176
+ command: 'start',
177
+ data: { authenticated: true, brief: brief.markdown, brief_source: brief.source },
178
+ humanSummary: 'Notis CLI is authenticated. Follow the onboarding brief below.',
179
+ renderHuman: () => brief.markdown || 'Notis CLI is authenticated.',
180
+ });
181
+ }
182
+
183
+ export const onboardingCommandSpecs = [
184
+ {
185
+ command_path: ['start'],
186
+ summary: 'Create or access a Notis account and wait for the desktop app to sign the CLI in.',
187
+ when_to_use:
188
+ 'Run this first on a new machine, before anything that needs auth. Safe to re-run: an already-signed-in machine just reprints the onboarding brief.',
189
+ args_schema: {
190
+ arguments: [],
191
+ options: [
192
+ { flags: '--email <email>', description: 'Email to create or sign in the Notis account with.' },
193
+ { flags: '--use-case <slug>', description: 'Primary use case (repeatable).', collect: true },
194
+ {
195
+ flags: '--wait-timeout-ms <n>',
196
+ description: 'How long to wait for the desktop sign-in (default 300000).',
197
+ },
198
+ { flags: '--no-wait', description: 'Send the link and exit without waiting.' },
199
+ { flags: '--brief-only', description: 'Print the onboarding brief for an already-signed-in profile.' },
200
+ ],
201
+ },
202
+ examples: [
203
+ 'notis start --email you@example.com',
204
+ 'notis start --brief-only',
205
+ 'notis start --email you@example.com --json',
206
+ ],
207
+ output_schema:
208
+ 'Returns {authenticated, brief, brief_source} once signed in, or {next_action, cooldown_seconds, desktop_download_url} while waiting.',
209
+ mutates: true,
210
+ idempotent: true,
211
+ require_auth: false,
212
+ related_commands: ['notis doctor', 'notis tools link'],
213
+ backend_call: { type: 'http', name: 'POST /signup/agent' },
214
+ handler: startHandler,
215
+ },
216
+ ];
@@ -453,8 +453,12 @@ async function toolsExecHandler(ctx) {
453
453
  arguments_: {
454
454
  tools: [{ tool_slug: requestedToolName, arguments: args }],
455
455
  },
456
- mutating: targetMutating !== false,
457
- idempotencyKey: targetMutating === false ? null : idempotencyKey,
456
+ // The server is authoritative about provider/MCP effects. A tool name that
457
+ // looks like a read is not proof of read-only behavior, so generic
458
+ // executions always carry a key and use mutation-safe transport retries.
459
+ // Proven reads are still executed in the read lane by hosted MCP.
460
+ mutating: true,
461
+ idempotencyKey,
458
462
  fileBindings,
459
463
  });
460
464
  ctx.output.emitProgress({
@@ -470,8 +474,8 @@ async function toolsExecHandler(ctx) {
470
474
  requestId: result.requestId,
471
475
  meta: {
472
476
  mutating: targetMutating,
473
- mutation_classification: targetMutating === null ? 'unknown' : 'inferred',
474
- idempotency_key: targetMutating === false ? null : idempotencyKey,
477
+ mutation_classification: targetMutating === null ? 'unknown' : 'name_hint_only',
478
+ idempotency_key: idempotencyKey,
475
479
  },
476
480
  renderHuman: () => JSON.stringify(result.payload, null, 2),
477
481
  });
@@ -231,7 +231,13 @@ export function detectProjectWarnings(projectDir, appConfig = null) {
231
231
  warnings.push('Missing Tailwind config.');
232
232
  }
233
233
 
234
- if (appConfig && (!Array.isArray(appConfig.databases) || appConfig.databases.length === 0)) {
234
+ const readsWorkspaceDatabases =
235
+ appConfig?.capabilities?.workspaceDatabases === 'read';
236
+ if (
237
+ appConfig
238
+ && !readsWorkspaceDatabases
239
+ && (!Array.isArray(appConfig.databases) || appConfig.databases.length === 0)
240
+ ) {
235
241
  warnings.push('No database references declared in notis.config.ts.');
236
242
  }
237
243
 
@@ -490,13 +496,24 @@ export async function runProjectScript({ projectDir, scriptName, env = {}, stdio
490
496
  stdio,
491
497
  env: { ...process.env, ...env },
492
498
  });
499
+ let capturedOutput = '';
500
+ for (const stream of [child.stdout, child.stderr]) {
501
+ stream?.on('data', (chunk) => {
502
+ capturedOutput += chunk.toString();
503
+ });
504
+ }
493
505
  child.on('error', rejectPromise);
494
506
  child.on('exit', (code) => {
495
507
  if (code === 0) {
496
508
  resolvePromise();
497
509
  return;
498
510
  }
499
- rejectPromise(new Error(`npm run ${scriptName} failed with exit code ${code}`));
511
+ const detail = capturedOutput.trim();
512
+ rejectPromise(
513
+ new Error(
514
+ `npm run ${scriptName} failed with exit code ${code}${detail ? `:\n${detail}` : ''}`,
515
+ ),
516
+ );
500
517
  });
501
518
  });
502
519
  }
@@ -707,7 +724,14 @@ export function generateManifest(appConfig, projectDir) {
707
724
  return entry;
708
725
  });
709
726
 
710
- const databases = appConfig.databases || [];
727
+ // Database entries may be a bare slug (structure only) or an object opting
728
+ // into shipping rows to installers. Normalize to the manifest's snake_case.
729
+ const databases = (appConfig.databases || []).map((entry) => {
730
+ if (typeof entry === 'string') return entry;
731
+ if (!entry || typeof entry !== 'object' || !entry.slug) return entry;
732
+ const seedDocuments = entry.seedDocuments ?? entry.seed_documents;
733
+ return seedDocuments === true ? { slug: entry.slug, seed_documents: true } : entry.slug;
734
+ });
711
735
  const categories = normalizeCategories(appConfig.categories || []);
712
736
  const metadata = { screenshots: resolveListingScreenshots(projectDir, appConfig) };
713
737
  const appSlug = safeKebab(appConfig.name);
@@ -786,12 +810,29 @@ export function generateManifest(appConfig, projectDir) {
786
810
  css: 'bundle/app.css',
787
811
  },
788
812
  databases,
813
+ capabilities: normalizeAppCapabilities(appConfig.capabilities),
789
814
  tools: appConfig.tools || [],
790
815
  skills,
791
816
  onboarding: appConfig.onboarding || null,
792
817
  };
793
818
  }
794
819
 
820
+ /**
821
+ * Keeps only capabilities the platform actually understands, at the exact
822
+ * values it accepts. An unknown key or value is dropped rather than passed
823
+ * through, so a typo can never reach the server as a permission grant.
824
+ */
825
+ export function normalizeAppCapabilities(capabilities) {
826
+ if (!capabilities || typeof capabilities !== 'object') {
827
+ return {};
828
+ }
829
+ const normalized = {};
830
+ if (capabilities.workspaceDatabases === 'read') {
831
+ normalized.workspaceDatabases = 'read';
832
+ }
833
+ return normalized;
834
+ }
835
+
795
836
  export function resolveConfiguredAppSkills(appConfig, projectDir) {
796
837
  const configured = Array.isArray(appConfig.skills) ? appConfig.skills : [];
797
838
  const projectRoot = resolve(projectDir);
@@ -831,13 +872,14 @@ export function resolveConfiguredAppSkills(appConfig, projectDir) {
831
872
  /**
832
873
  * Build the app bundle: generate entry file, run `vite build`, package into .notis/output/.
833
874
  */
834
- export async function buildArtifact(projectDir) {
875
+ export async function buildArtifact(projectDir, { stdio = 'inherit' } = {}) {
835
876
  await prepareArtifactBuild(projectDir);
836
877
 
837
878
  // Run Vite build
838
879
  await runProjectScript({
839
880
  projectDir,
840
881
  scriptName: 'build',
882
+ stdio,
841
883
  });
842
884
 
843
885
  // Verify the canonical `.notis/output/bundle` packaging contract.
@@ -948,8 +990,13 @@ export function scaffoldProject({ projectDir, appName, fromSlug = null }) {
948
990
  if (existsSync(pkgPath)) {
949
991
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
950
992
  pkg.name = appName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
993
+ // A scaffold creates a new app identity, even when its source comes from a
994
+ // versioned Store example. New apps must therefore start their own release
995
+ // history instead of inheriting the example app's registry version.
996
+ pkg.notisAppVersion = '0.1.0';
951
997
  ensureScaffoldLocalSdk(projectDir, pkg);
952
998
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
999
+ normalizeScaffoldLockfile(projectDir, pkg);
953
1000
  }
954
1001
 
955
1002
  // Update notis.config.ts with the app name
@@ -983,6 +1030,20 @@ export function scaffoldProject({ projectDir, appName, fromSlug = null }) {
983
1030
  return { projectDir };
984
1031
  }
985
1032
 
1033
+ function normalizeScaffoldLockfile(projectDir, pkg) {
1034
+ const lockPath = join(projectDir, 'package-lock.json');
1035
+ if (!existsSync(lockPath)) {
1036
+ return;
1037
+ }
1038
+
1039
+ const lockfile = JSON.parse(readFileSync(lockPath, 'utf-8'));
1040
+ lockfile.name = pkg.name;
1041
+ if (lockfile.packages?.['']) {
1042
+ lockfile.packages[''].name = pkg.name;
1043
+ }
1044
+ writeFileSync(lockPath, JSON.stringify(lockfile, null, 2) + '\n');
1045
+ }
1046
+
986
1047
  function ensureScaffoldLocalSdk(projectDir, pkg) {
987
1048
  let shouldInstallLocalSdk = false;
988
1049
  for (const dependencyGroup of ['dependencies', 'devDependencies']) {
@@ -24,7 +24,15 @@ function defaultDesktopAppName(apiBase) {
24
24
  }
25
25
  }
26
26
 
27
- export function getDesktopAuthRecovery(runtime) {
27
+ /**
28
+ * Recovery hints for an unusable desktop-managed credential.
29
+ *
30
+ * `mode` distinguishes the two cases an agent has to act on differently:
31
+ * "expired" means a session existed and the desktop app can renew it, while
32
+ * "missing" means this machine was never signed in — telling that caller to
33
+ * renew something is misleading.
34
+ */
35
+ export function getDesktopAuthRecovery(runtime, { mode = 'expired' } = {}) {
28
36
  const desktopRunning = isPidRunning(runtime.desktopPid);
29
37
  const appName = runtime.desktopAppName || defaultDesktopAppName(runtime.apiBase);
30
38
  let command = 'Start the Notis desktop app';
@@ -32,22 +40,63 @@ export function getDesktopAuthRecovery(runtime) {
32
40
  command = `open -a ${quoteShellArgument(appName)}`;
33
41
  }
34
42
 
35
- return {
36
- desktopRunning,
37
- appName,
38
- hints: [
39
- {
40
- command,
41
- reason: desktopRunning
42
- ? `Bring ${appName} forward so it can renew CLI authentication, then retry`
43
- : `Start ${appName} to renew expired CLI authentication, then retry`,
44
- },
45
- {
46
- command: 'notis doctor',
47
- reason: 'Retry the auth and API checks after the desktop app is ready',
48
- },
49
- ],
50
- };
43
+ let reason;
44
+ if (mode === 'missing') {
45
+ reason = desktopRunning
46
+ ? `Sign in to ${appName} to authenticate the CLI on this machine, then retry`
47
+ : `Install and sign in to ${appName} to authenticate the CLI on this machine, then retry`;
48
+ } else {
49
+ reason = desktopRunning
50
+ ? `Bring ${appName} forward so it can renew CLI authentication, then retry`
51
+ : `Start ${appName} to renew expired CLI authentication, then retry`;
52
+ }
53
+
54
+ const hints = [{ command, reason }];
55
+ if (mode === 'missing') {
56
+ // Leads the list: a machine that was never signed in may not even have an
57
+ // account yet, and `notis start` covers both cases.
58
+ hints.unshift({
59
+ command: 'notis start --email <address>',
60
+ reason: 'Create or access a Notis account and sign this machine in',
61
+ });
62
+ }
63
+ hints.push({
64
+ command: 'notis doctor',
65
+ reason: 'Retry the auth and API checks after the desktop app is ready',
66
+ });
67
+
68
+ return { desktopRunning, appName, hints };
69
+ }
70
+
71
+ /**
72
+ * Block until the desktop app writes an unexpired JWT into the CLI profile.
73
+ *
74
+ * Polls the local config file only — never the server. The desktop app is what
75
+ * mints the credential, and `transport.js` re-reads the profile before every
76
+ * request, so this loop exists purely so the caller's process can wait rather
77
+ * than fail and make the user re-run the command.
78
+ */
79
+ export async function waitForDesktopAuth({
80
+ loadConfig,
81
+ getJwt,
82
+ isJwtExpired,
83
+ profileName = 'default',
84
+ timeoutMs = 300_000,
85
+ intervalMs = 2_000,
86
+ onTick,
87
+ } = {}) {
88
+ const deadline = Date.now() + timeoutMs;
89
+ for (;;) {
90
+ const jwt = getJwt(loadConfig(), profileName);
91
+ if (jwt && !isJwtExpired(jwt)) {
92
+ return jwt;
93
+ }
94
+ if (Date.now() >= deadline) {
95
+ return null;
96
+ }
97
+ if (onTick) onTick(Math.max(0, deadline - Date.now()));
98
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
99
+ }
51
100
  }
52
101
 
53
102
  export function createExpiredAuthError(runtime) {
@@ -6,6 +6,7 @@ export const EXIT_CODES = {
6
6
  conflict: 5,
7
7
  backend: 6,
8
8
  unexpected: 7,
9
+ payment: 8,
9
10
  };
10
11
 
11
12
  export class CliError extends Error {
@@ -22,6 +22,13 @@ function formatTable(rows, columns) {
22
22
  return [header, divider, ...body].join('\n');
23
23
  }
24
24
 
25
+ function formatHint(hint) {
26
+ if (typeof hint?.message === 'string' && hint.message) {
27
+ return ` ${hint.message}`;
28
+ }
29
+ return ` ${hint?.command || ''} ${hint?.reason || ''}`.trimEnd();
30
+ }
31
+
25
32
  function yamlScalar(value) {
26
33
  if (value === null || value === undefined) {
27
34
  return 'null';
@@ -153,7 +160,7 @@ export class OutputManager {
153
160
  }
154
161
 
155
162
  if (hints.length) {
156
- const lines = hints.map((hint) => ` ${hint.command} ${hint.reason}`);
163
+ const lines = hints.map(formatHint);
157
164
  process.stdout.write(`\nNext:\n${lines.join('\n')}\n`);
158
165
  }
159
166
 
@@ -182,7 +189,7 @@ export class OutputManager {
182
189
 
183
190
  process.stderr.write(`Error: ${error.message}\n`);
184
191
  for (const hint of error.hints || []) {
185
- process.stderr.write(` ${hint.command} ${hint.reason}\n`);
192
+ process.stderr.write(`${formatHint(hint)}\n`);
186
193
  }
187
194
  return error.exitCode || EXIT_CODES.unexpected;
188
195
  }