@notis_ai/cli 0.2.0-beta.113.1 → 0.2.0-beta.114.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 (176) 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 +16 -0
  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/profiles.js +17 -6
  131. package/src/runtime/telemetry.js +92 -0
  132. package/src/runtime/transport.js +12 -0
  133. package/template/.harness/index.html.tmpl +30 -2
  134. package/template/packages/sdk/src/config.ts +38 -1
  135. package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +0 -7
  136. package/dist/scaffolds/notis-affiliate-prospects/README.md +0 -20
  137. package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +0 -42
  138. package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +0 -7
  139. package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +0 -248
  140. package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +0 -134
  141. package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +0 -108
  142. package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +0 -35
  143. package/dist/scaffolds/notis-affiliate-prospects/index.html +0 -12
  144. package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +0 -218
  145. package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +0 -52
  146. package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +0 -100
  147. package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +0 -25
  148. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
  149. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
  150. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
  151. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +0 -187
  152. package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +0 -65
  153. package/dist/scaffolds/notis-affiliate-prospects/package.json +0 -32
  154. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +0 -90
  155. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useDatabase.ts +0 -76
  156. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +0 -50
  157. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +0 -38
  158. package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +0 -6
  159. package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +0 -72
  160. package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +0 -242
  161. package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +0 -51
  162. package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +0 -23
  163. package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +0 -11
  164. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -0
  165. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
  166. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useBackend.ts +0 -0
  167. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useNotis.ts +0 -0
  168. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useNotisNavigation.ts +0 -0
  169. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTool.ts +0 -0
  170. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTools.ts +0 -0
  171. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/hooks/useTopBarSearch.ts +0 -0
  172. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/provider.tsx +0 -0
  173. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/ui.ts +0 -0
  174. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/src/vite.ts +0 -0
  175. /package/dist/scaffolds/{notis-affiliate-prospects → notis-database}/packages/sdk/tsconfig.json +0 -0
  176. /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 {
@@ -187,7 +187,15 @@ export function loadConfig(runtime = null) {
187
187
  return normalizeConfig({});
188
188
  }
189
189
 
190
- return normalizeConfig(JSON.parse(readFileSync(configFile, 'utf-8')));
190
+ // The desktop app rewrites this file while the CLI may be reading it, so a
191
+ // torn or corrupt read is expected rather than exceptional. Degrade to
192
+ // "unauthenticated", which surfaces the actionable auth_missing error instead
193
+ // of a raw SyntaxError from deep inside the runtime.
194
+ try {
195
+ return normalizeConfig(JSON.parse(readFileSync(configFile, 'utf-8')));
196
+ } catch {
197
+ return normalizeConfig({});
198
+ }
191
199
  }
192
200
 
193
201
  export function saveConfig(config, runtime = null) {
@@ -361,11 +369,14 @@ export function resolveRuntimeProfile(
361
369
  : null;
362
370
 
363
371
  if (requireAuth && !jwt) {
364
- const recovery = getDesktopAuthRecovery({
365
- apiBase,
366
- desktopAppName: profile.desktop_app_name,
367
- desktopPid: profile.desktop_pid,
368
- });
372
+ const recovery = getDesktopAuthRecovery(
373
+ {
374
+ apiBase,
375
+ desktopAppName: profile.desktop_app_name,
376
+ desktopPid: profile.desktop_pid,
377
+ },
378
+ { mode: 'missing' },
379
+ );
369
380
  throw new CliError({
370
381
  code: 'auth_missing',
371
382
  message: `No JWT configured for profile ${profileName}`,
@@ -0,0 +1,92 @@
1
+ const ERROR_CATEGORIES = new Map([
2
+ [2, 'usage'],
3
+ [3, 'auth'],
4
+ [4, 'network'],
5
+ [5, 'conflict'],
6
+ [6, 'backend'],
7
+ [7, 'unexpected'],
8
+ [8, 'payment'],
9
+ ]);
10
+
11
+ export function cliDurationBucket(durationMs) {
12
+ if (durationMs < 1_000) return 'under_1s';
13
+ if (durationMs < 5_000) return '1s_to_5s';
14
+ if (durationMs < 30_000) return '5s_to_30s';
15
+ return 'over_30s';
16
+ }
17
+
18
+ export function cliBackendKind(spec) {
19
+ const value = String(spec?.backend_call?.type || '').toLowerCase();
20
+ if (value === 'tool') return 'tool';
21
+ if (value === 'http') return 'http';
22
+ if (value === 'local') return 'local';
23
+ if (value.includes('health')) return 'health';
24
+ if (value.includes('local')) return 'local';
25
+ return 'other';
26
+ }
27
+
28
+ export function buildCliTelemetryPayload({
29
+ spec,
30
+ runtime,
31
+ result,
32
+ durationMs,
33
+ error,
34
+ }) {
35
+ return {
36
+ command_id: spec.command_path.join('.'),
37
+ backend_kind: cliBackendKind(spec),
38
+ result,
39
+ error_category: result === 'failed'
40
+ ? (ERROR_CATEGORIES.get(Number(error?.exitCode)) || 'unexpected')
41
+ : 'none',
42
+ duration_bucket: cliDurationBucket(durationMs),
43
+ cli_version: runtime.cliVersion,
44
+ agent_mode: Boolean(runtime.agentMode),
45
+ };
46
+ }
47
+
48
+ export async function reportCliCommand({
49
+ spec,
50
+ runtime,
51
+ result,
52
+ durationMs,
53
+ error = null,
54
+ fetchImpl = globalThis.fetch,
55
+ }) {
56
+ // Backend tool calls emit authoritative surface/tool telemetry server-side.
57
+ // This client event exists only for local commands that otherwise leave no
58
+ // observable runtime boundary.
59
+ if (
60
+ cliBackendKind(spec) !== 'local'
61
+ || !runtime?.jwt
62
+ || !runtime?.apiBase
63
+ || typeof fetchImpl !== 'function'
64
+ ) {
65
+ return false;
66
+ }
67
+ const controller = new AbortController();
68
+ const timeout = setTimeout(() => controller.abort(), 1_500);
69
+ try {
70
+ const response = await fetchImpl(`${runtime.apiBase}/cli_telemetry`, {
71
+ method: 'POST',
72
+ headers: {
73
+ Authorization: `Bearer ${runtime.jwt}`,
74
+ 'Content-Type': 'application/json',
75
+ 'X-Notis-CLI-Version': runtime.cliVersion,
76
+ },
77
+ body: JSON.stringify(buildCliTelemetryPayload({
78
+ spec,
79
+ runtime,
80
+ result,
81
+ durationMs,
82
+ error,
83
+ })),
84
+ signal: controller.signal,
85
+ });
86
+ return response.ok;
87
+ } catch {
88
+ return false;
89
+ } finally {
90
+ clearTimeout(timeout);
91
+ }
92
+ }
@@ -123,6 +123,18 @@ function normalizeBackendError(status, payload, runtime) {
123
123
  });
124
124
  }
125
125
 
126
+ if (status === 402) {
127
+ // Distinct from auth on purpose: an agent that sees an auth exit code will
128
+ // loop trying to re-authenticate, when the actual fix is to add credit.
129
+ return new CliError({
130
+ code: 'payment_required',
131
+ message,
132
+ exitCode: EXIT_CODES.payment,
133
+ details: payload || {},
134
+ hints: payload?.hints || [],
135
+ });
136
+ }
137
+
126
138
  if (status >= 400 && status < 500) {
127
139
  return new CliError({
128
140
  code: 'usage_error',
@@ -107,6 +107,33 @@
107
107
  const apiBase = {{API_BASE}};
108
108
  const jwt = {{JWT}};
109
109
  const fixtures = {{FIXTURES}};
110
+
111
+ /**
112
+ * Resolves the canned response for one tool call.
113
+ *
114
+ * A plain `"TOOL_NAME"` key answers every call to that tool. An app that
115
+ * reaches several databases through a single tool needs to vary the
116
+ * payload per call, so a key may also be scoped to one argument with
117
+ * `"TOOL_NAME#arg=value"` — for example
118
+ * `"LOCAL_NOTIS_DATABASE_QUERY#database_slug=notes"`. Scoped keys are
119
+ * checked first; the unscoped key stays the fallback.
120
+ */
121
+ function lookupToolFixture(name, args) {
122
+ const tools = fixtures && fixtures.tools;
123
+ if (!tools) return undefined;
124
+ for (const key of Object.keys(tools)) {
125
+ const sep = key.indexOf('#');
126
+ if (sep === -1 || key.slice(0, sep) !== name) continue;
127
+ const scope = key.slice(sep + 1);
128
+ const eq = scope.indexOf('=');
129
+ if (eq === -1) continue;
130
+ if (String(args[scope.slice(0, eq)]) === scope.slice(eq + 1)) {
131
+ return tools[key];
132
+ }
133
+ }
134
+ return Object.prototype.hasOwnProperty.call(tools, name) ? tools[name] : undefined;
135
+ }
136
+
110
137
  const status = document.getElementById('harness-status');
111
138
  const setStatus = (msg, color = '#0f0') => {
112
139
  status.textContent = `harness: ${msg}`;
@@ -197,8 +224,9 @@
197
224
  },
198
225
  callTool: async (name, args) => {
199
226
  record('callTool', { name, arguments: args || {} });
200
- if (fixtures && fixtures.tools && Object.prototype.hasOwnProperty.call(fixtures.tools, name)) {
201
- return structuredClone(fixtures.tools[name]);
227
+ const fixture = lookupToolFixture(name, args || {});
228
+ if (fixture !== undefined) {
229
+ return structuredClone(fixture);
202
230
  }
203
231
  return { ok: true, result: null };
204
232
  },