@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
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
- import { join } from 'node:path';
3
+ import { dirname, join, parse, resolve } from 'node:path';
4
4
  import { CliError, EXIT_CODES } from './errors.js';
5
5
  import { getDesktopAuthRecovery } from './desktop-auth.js';
6
6
 
@@ -9,6 +9,8 @@ export const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
9
9
  export const WORKSPACE_DIR = join(CONFIG_DIR, 'workspace');
10
10
  export const DEFAULT_API_BASE = 'https://api.notis.ai';
11
11
  export const DEFAULT_PROFILE = 'default';
12
+ const WORKTREE_RUNTIME_FILENAME = join('.context', 'notis-runtime.json');
13
+ const WORKTREE_ROUTING_FILENAME = join('.context', 'notis-routing.json');
12
14
  const LOCAL_DEFAULT_API_BASES = new Set([
13
15
  'http://localhost:3001',
14
16
  'http://127.0.0.1:3001',
@@ -77,17 +79,129 @@ export function normalizeConfig(rawConfig = {}) {
77
79
  };
78
80
  }
79
81
 
80
- export function loadConfig() {
81
- if (!existsSync(CONFIG_FILE)) {
82
+ function readJsonFile(path) {
83
+ try {
84
+ return JSON.parse(readFileSync(path, 'utf-8'));
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ function findUp(filename, startDir = process.cwd()) {
91
+ let current = resolve(startDir);
92
+ const root = parse(current).root;
93
+ while (true) {
94
+ const candidate = join(current, filename);
95
+ if (existsSync(candidate)) {
96
+ return candidate;
97
+ }
98
+ if (current === root) {
99
+ return null;
100
+ }
101
+ current = dirname(current);
102
+ }
103
+ }
104
+
105
+ function processIsAlive(pid) {
106
+ if (!Number.isInteger(pid) || pid <= 0) {
107
+ return false;
108
+ }
109
+ try {
110
+ process.kill(pid, 0);
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ export function resolveWorktreeRuntime(startDir = process.cwd()) {
118
+ if (
119
+ process.env.NODE_ENV === 'test' &&
120
+ process.env.NOTIS_TEST_DISABLE_WORKTREE_ROUTING === '1'
121
+ ) {
122
+ return null;
123
+ }
124
+ const runtimePath = findUp(WORKTREE_RUNTIME_FILENAME, startDir);
125
+ const routingPath = findUp(WORKTREE_ROUTING_FILENAME, startDir);
126
+ const routing = routingPath ? readJsonFile(routingPath) : null;
127
+
128
+ if (!runtimePath) {
129
+ if (routing?.mode === 'local-only') {
130
+ throw new CliError({
131
+ code: 'dev_runtime_unavailable',
132
+ message: `This worktree is local-only, but its dev.sh runtime is not active`,
133
+ exitCode: EXIT_CODES.network,
134
+ hints: [
135
+ { message: 'Start ./dev.sh in this worktree, then retry the command.' },
136
+ { message: `Routing policy: ${routingPath}` },
137
+ ],
138
+ });
139
+ }
140
+ return null;
141
+ }
142
+
143
+ const runtime = readJsonFile(runtimePath);
144
+ const apiBase = typeof runtime?.api_base === 'string' ? runtime.api_base.replace(/\/+$/, '') : '';
145
+ const configFile = typeof runtime?.config_file === 'string' ? runtime.config_file : '';
146
+ const pid = Number(runtime?.dev_pid);
147
+ if (
148
+ runtime?.mode !== 'local-only' ||
149
+ !isLocalApiBase(apiBase) ||
150
+ !configFile ||
151
+ !processIsAlive(pid)
152
+ ) {
153
+ throw new CliError({
154
+ code: 'dev_runtime_unavailable',
155
+ message: 'The local-only worktree runtime is stale or invalid',
156
+ exitCode: EXIT_CODES.network,
157
+ hints: [
158
+ { message: 'Restart ./dev.sh in this worktree, then retry the command.' },
159
+ { message: `Runtime lease: ${runtimePath}` },
160
+ ],
161
+ });
162
+ }
163
+
164
+ return {
165
+ ...runtime,
166
+ api_base: apiBase,
167
+ config_file: resolve(dirname(runtimePath), configFile),
168
+ runtime_path: runtimePath,
169
+ routing_path: routingPath,
170
+ };
171
+ }
172
+
173
+ export function resolveConfigFile(runtime = null) {
174
+ if (runtime?.config_file) {
175
+ return runtime.config_file;
176
+ }
177
+ const envConfigFile = process.env.NOTIS_CLI_CONFIG_FILE;
178
+ if (envConfigFile) {
179
+ return resolve(envConfigFile);
180
+ }
181
+ return CONFIG_FILE;
182
+ }
183
+
184
+ export function loadConfig(runtime = null) {
185
+ const configFile = resolveConfigFile(runtime);
186
+ if (!existsSync(configFile)) {
82
187
  return normalizeConfig({});
83
188
  }
84
189
 
85
- return normalizeConfig(JSON.parse(readFileSync(CONFIG_FILE, '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
+ }
86
199
  }
87
200
 
88
- export function saveConfig(config) {
89
- mkdirSync(CONFIG_DIR, { recursive: true });
90
- writeFileSync(CONFIG_FILE, JSON.stringify(normalizeConfig(config), null, 2));
201
+ export function saveConfig(config, runtime = null) {
202
+ const configFile = resolveConfigFile(runtime);
203
+ mkdirSync(dirname(configFile), { recursive: true });
204
+ writeFileSync(configFile, JSON.stringify(normalizeConfig(config), null, 2), { mode: 0o600 });
91
205
  }
92
206
 
93
207
  export function getProfile(config, profileName) {
@@ -225,10 +339,26 @@ export function resolveRuntimeProfile(
225
339
  globalOptions = {},
226
340
  { requireAuth = true, includeDebugEntitlementOverride = true } = {},
227
341
  ) {
228
- const config = loadConfig();
342
+ const worktreeRuntime = resolveWorktreeRuntime();
343
+ const config = loadConfig(worktreeRuntime);
229
344
  const profileName = getCurrentProfileName(config, globalOptions.profile);
230
- const apiBase = getApiBase(config, profileName, globalOptions.apiBase);
231
- const jwt = getJwt(config, profileName);
345
+ const requestedApiBase = globalOptions.apiBase;
346
+ if (
347
+ worktreeRuntime &&
348
+ requestedApiBase &&
349
+ requestedApiBase.replace(/\/+$/, '') !== worktreeRuntime.api_base
350
+ ) {
351
+ throw new CliError({
352
+ code: 'dev_runtime_route_mismatch',
353
+ message: `This local-only worktree cannot route to ${requestedApiBase}`,
354
+ exitCode: EXIT_CODES.usage,
355
+ hints: [{ message: `Expected local API: ${worktreeRuntime.api_base}` }],
356
+ });
357
+ }
358
+ const apiBase = worktreeRuntime
359
+ ? worktreeRuntime.api_base
360
+ : getApiBase(config, profileName, globalOptions.apiBase);
361
+ const jwt = worktreeRuntime ? getProfile(config, profileName).jwt : getJwt(config, profileName);
232
362
  const profile = getProfile(config, profileName);
233
363
  const agentMode = isAgentMode(globalOptions);
234
364
  const nonInteractive = isNonInteractive(globalOptions);
@@ -239,11 +369,14 @@ export function resolveRuntimeProfile(
239
369
  : null;
240
370
 
241
371
  if (requireAuth && !jwt) {
242
- const recovery = getDesktopAuthRecovery({
243
- apiBase,
244
- desktopAppName: profile.desktop_app_name,
245
- desktopPid: profile.desktop_pid,
246
- });
372
+ const recovery = getDesktopAuthRecovery(
373
+ {
374
+ apiBase,
375
+ desktopAppName: profile.desktop_app_name,
376
+ desktopPid: profile.desktop_pid,
377
+ },
378
+ { mode: 'missing' },
379
+ );
247
380
  throw new CliError({
248
381
  code: 'auth_missing',
249
382
  message: `No JWT configured for profile ${profileName}`,
@@ -251,16 +384,30 @@ export function resolveRuntimeProfile(
251
384
  hints: recovery.hints,
252
385
  });
253
386
  }
387
+ if (
388
+ worktreeRuntime?.expected_user_id &&
389
+ getJwtSubject(jwt) !== worktreeRuntime.expected_user_id
390
+ ) {
391
+ throw new CliError({
392
+ code: 'dev_runtime_identity_mismatch',
393
+ message: 'The scoped dev credential does not belong to this worktree test user',
394
+ exitCode: EXIT_CODES.auth,
395
+ hints: [
396
+ { message: 'Restart ./dev.sh to restore the approved worktree identity.' },
397
+ { message: `Expected user: ${worktreeRuntime.expected_user_id}` },
398
+ ],
399
+ });
400
+ }
254
401
 
255
402
  // An explicit NOTIS_JWT is a complete credential override. Use it verbatim
256
403
  // and never replace it with a token later synced by the desktop profile.
257
- const usingEnvJwt = Boolean(process.env.NOTIS_JWT);
404
+ const usingEnvJwt = !worktreeRuntime && Boolean(process.env.NOTIS_JWT);
258
405
  return {
259
406
  config,
260
407
  profileName,
261
408
  apiBase,
262
409
  jwt,
263
- credentialSource: usingEnvJwt ? 'env' : 'profile',
410
+ credentialSource: worktreeRuntime ? 'worktree' : usingEnvJwt ? 'env' : 'profile',
264
411
  desktopAppName: usingEnvJwt ? undefined : profile.desktop_app_name,
265
412
  desktopPid: usingEnvJwt ? undefined : profile.desktop_pid,
266
413
  agentMode,
@@ -268,6 +415,7 @@ export function resolveRuntimeProfile(
268
415
  outputMode,
269
416
  timeoutMs,
270
417
  debugEntitlementOverride,
418
+ worktreeRuntime,
271
419
  };
272
420
  }
273
421
 
@@ -285,6 +433,20 @@ export function getJwtExpiration(jwt) {
285
433
  }
286
434
  }
287
435
 
436
+ export function getJwtSubject(jwt) {
437
+ if (typeof jwt !== 'string' || !jwt) {
438
+ return null;
439
+ }
440
+ try {
441
+ const parts = jwt.split('.');
442
+ if (parts.length !== 3) return null;
443
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
444
+ return typeof payload.sub === 'string' && payload.sub ? payload.sub : null;
445
+ } catch {
446
+ return null;
447
+ }
448
+ }
449
+
288
450
  export function isJwtExpired(jwt, nowSeconds = Math.floor(Date.now() / 1000)) {
289
451
  const expiration = getJwtExpiration(jwt);
290
452
  return expiration !== null && expiration <= nowSeconds;
@@ -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
+ }
@@ -4,6 +4,7 @@ import { CliError, EXIT_CODES } from './errors.js';
4
4
  import {
5
5
  DEFAULT_PROFILE,
6
6
  getProfile,
7
+ getJwtSubject,
7
8
  isJwtExpired,
8
9
  loadConfig,
9
10
  } from './profiles.js';
@@ -122,6 +123,18 @@ function normalizeBackendError(status, payload, runtime) {
122
123
  });
123
124
  }
124
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
+
125
138
  if (status >= 400 && status < 500) {
126
139
  return new CliError({
127
140
  code: 'usage_error',
@@ -149,7 +162,7 @@ function reloadJwtFromConfig(runtime) {
149
162
  const profileName = runtime.profileName || DEFAULT_PROFILE;
150
163
  let profile;
151
164
  try {
152
- profile = getProfile(loadConfig(), profileName);
165
+ profile = getProfile(loadConfig(runtime.worktreeRuntime), profileName);
153
166
  } catch {
154
167
  return false;
155
168
  }
@@ -157,6 +170,18 @@ function reloadJwtFromConfig(runtime) {
157
170
  if (!nextJwt || nextJwt === runtime.jwt) {
158
171
  return false;
159
172
  }
173
+ const expectedUserId = runtime.worktreeRuntime?.expected_user_id;
174
+ if (expectedUserId && getJwtSubject(nextJwt) !== expectedUserId) {
175
+ throw new CliError({
176
+ code: 'dev_runtime_identity_mismatch',
177
+ message: 'The refreshed scoped dev credential does not belong to this worktree test user',
178
+ exitCode: EXIT_CODES.auth,
179
+ hints: [
180
+ { message: 'Restart ./dev.sh to restore the approved worktree identity.' },
181
+ { message: `Expected user: ${expectedUserId}` },
182
+ ],
183
+ });
184
+ }
160
185
  runtime.jwt = nextJwt;
161
186
  runtime.desktopAppName = profile.desktop_app_name;
162
187
  runtime.desktopPid = profile.desktop_pid;
@@ -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
  },
@@ -62,6 +62,20 @@ export const NOTIS_APP_CATEGORIES = [
62
62
 
63
63
  export type NotisAppCategory = typeof NOTIS_APP_CATEGORIES[number];
64
64
 
65
+ export interface NotisAppDatabaseConfig {
66
+ /** Database slug, as declared by the app. */
67
+ slug: string;
68
+ /**
69
+ * Ship this database's rows to everyone who installs the app.
70
+ *
71
+ * Off by default: declaring a database publishes its STRUCTURE, never its
72
+ * content. Turn it on only for starter content that every installer should
73
+ * receive - a default folder tree, a set of templates - and never for a
74
+ * database that accumulates the author's own data.
75
+ */
76
+ seedDocuments?: boolean;
77
+ }
78
+
65
79
  export interface NotisAppAuthor {
66
80
  name: string;
67
81
  handle?: string;
@@ -106,6 +120,19 @@ export const NOTIS_APP_ACCENTS = [
106
120
 
107
121
  export type NotisAppAccent = typeof NOTIS_APP_ACCENTS[number];
108
122
 
123
+ export interface NotisAppCapabilities {
124
+ /**
125
+ * Read every database in the workspace, not only the ones this app declares
126
+ * in `databases` or created itself.
127
+ *
128
+ * An app runtime is otherwise sandboxed to its own databases, so a catalog or
129
+ * explorer app sees an empty list without this. `'read'` is the only accepted
130
+ * value and it never grants writes: `LOCAL_NOTIS_DATABASE_UPSERT_*` stays
131
+ * bound to the app's own databases.
132
+ */
133
+ workspaceDatabases?: 'read';
134
+ }
135
+
109
136
  export interface NotisAppConfig {
110
137
  /** URL-safe app slug. Existing apps may still use a display name here. */
111
138
  name: string;
@@ -128,7 +155,17 @@ export interface NotisAppConfig {
128
155
  /** @deprecated Add release entries to the root CHANGELOG.md instead. */
129
156
  versionNotes?: string;
130
157
  screenshots?: NotisAppScreenshotConfig[];
131
- databases?: string[];
158
+ /**
159
+ * Databases this app owns. A bare string publishes structure only; use the
160
+ * object form to opt a database into shipping its rows to installers.
161
+ */
162
+ databases?: (string | NotisAppDatabaseConfig)[];
163
+ /**
164
+ * Extra permissions the app asks for at install time. Everything here widens
165
+ * what the app can reach beyond its own data, so each one is surfaced to the
166
+ * user before they install.
167
+ */
168
+ capabilities?: NotisAppCapabilities;
132
169
  routes?: NotisRouteConfig[];
133
170
  tools?: string[];
134
171
  skills?: NotisAppSkillConfig[];
@@ -1,7 +0,0 @@
1
- # Changelog
2
-
3
- ## [Affiliate recruiting control center] - 2026-07-23
4
-
5
- - Added the campaign dashboard, measurable affiliate pipeline, and priority queue.
6
- - Added responsive prospect and segment views.
7
- - Added local fixtures for every recruiting segment so the app can be tested before installation.
@@ -1,20 +0,0 @@
1
- # Affiliate Prospects
2
-
3
- Local-first Notis app for monitoring the affiliate recruiting pipeline.
4
-
5
- The app is intentionally a read-only campaign surface. Notis automations and Codex workers own
6
- discovery, enrichment, outreach preparation, analytics reconciliation, and record updates. This
7
- keeps one source of truth while avoiding business logic in the UI.
8
-
9
- ## Routes
10
-
11
- - Dashboard: KPIs, pipeline, priority queue, and segment coverage.
12
- - Prospects: responsive reference table for every lead and next action.
13
- - Segments: scorecards linked to the source-controlled segment playbooks.
14
-
15
- ## Data
16
-
17
- The deployed app references `affiliate_prospects_1` and `affiliate_outreach_events_1`. Their
18
- canonical schema is documented in `campaign/affiliate-network/data-schema.md`. During local
19
- development the app uses deterministic mock prospects so it can be reviewed before any workspace
20
- installation.
@@ -1,42 +0,0 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
4
-
5
- .affiliate-app-shell {
6
- container-type: inline-size;
7
- padding: clamp(1rem, 2.5cqi, 2rem);
8
- max-width: 1440px;
9
- margin-inline: auto;
10
- }
11
-
12
- .affiliate-stat-grid {
13
- display: grid;
14
- grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
15
- }
16
-
17
- .affiliate-dashboard-grid {
18
- display: grid;
19
- grid-template-columns: minmax(0, 1fr);
20
- }
21
-
22
- @container (min-width: 940px) {
23
- .affiliate-dashboard-grid {
24
- grid-template-columns: minmax(0, 1.55fr) minmax(300px, 0.75fr);
25
- }
26
- }
27
-
28
- .affiliate-data-table {
29
- min-width: 900px;
30
- }
31
-
32
- @media (max-width: 767px) {
33
- .affiliate-desktop-table {
34
- display: none;
35
- }
36
- }
37
-
38
- @media (min-width: 768px) {
39
- .affiliate-mobile-cards {
40
- display: none;
41
- }
42
- }
@@ -1,7 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- import '@notis/sdk/styles.css';
3
- import './globals.css';
4
-
5
- export default function AppShell({ children }: { children: ReactNode }) {
6
- return children;
7
- }