@notis_ai/cli 0.2.0-beta.107.1 → 0.2.0-beta.113.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +2 -0
  2. package/dist/scaffolds/notis-affiliate-prospects/CHANGELOG.md +7 -0
  3. package/dist/scaffolds/notis-affiliate-prospects/README.md +20 -0
  4. package/dist/scaffolds/notis-affiliate-prospects/app/globals.css +42 -0
  5. package/dist/scaffolds/notis-affiliate-prospects/app/layout.tsx +7 -0
  6. package/dist/scaffolds/notis-affiliate-prospects/app/page.tsx +248 -0
  7. package/dist/scaffolds/notis-affiliate-prospects/app/prospects/page.tsx +134 -0
  8. package/dist/scaffolds/notis-affiliate-prospects/app/segments/page.tsx +108 -0
  9. package/dist/scaffolds/notis-affiliate-prospects/components/ui/card.tsx +35 -0
  10. package/dist/scaffolds/notis-affiliate-prospects/components.json +20 -0
  11. package/dist/scaffolds/notis-affiliate-prospects/index.html +12 -0
  12. package/dist/scaffolds/notis-affiliate-prospects/lib/affiliate-data.ts +218 -0
  13. package/dist/scaffolds/notis-affiliate-prospects/lib/demo-prospects.ts +52 -0
  14. package/dist/scaffolds/notis-affiliate-prospects/lib/notis-tools.ts +100 -0
  15. package/dist/scaffolds/notis-affiliate-prospects/lib/utils.ts +25 -0
  16. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-1.png +0 -0
  17. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-2.png +0 -0
  18. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-3.png +0 -0
  19. package/dist/scaffolds/notis-affiliate-prospects/metadata/screenshot-fixtures.json +187 -0
  20. package/dist/scaffolds/notis-affiliate-prospects/notis.config.ts +65 -0
  21. package/dist/scaffolds/notis-affiliate-prospects/package.json +32 -0
  22. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/package.json +32 -0
  23. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectActionBar.tsx +273 -0
  24. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  25. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  26. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/config.ts +90 -0
  27. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useBackend.ts +41 -0
  28. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useDatabase.ts +76 -0
  29. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useMultiSelect.ts +503 -0
  30. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotis.ts +34 -0
  31. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  32. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTool.ts +64 -0
  33. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTools.ts +56 -0
  34. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  35. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/hooks/useUpsertDocument.ts +50 -0
  36. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/index.ts +54 -0
  37. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/provider.tsx +43 -0
  38. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/runtime.ts +161 -0
  39. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/styles.css +38 -0
  40. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/ui.ts +15 -0
  41. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/src/vite.ts +56 -0
  42. package/dist/scaffolds/notis-affiliate-prospects/packages/sdk/tsconfig.json +15 -0
  43. package/dist/scaffolds/notis-affiliate-prospects/postcss.config.mjs +6 -0
  44. package/dist/scaffolds/notis-affiliate-prospects/src/dev-main.tsx +72 -0
  45. package/dist/scaffolds/notis-affiliate-prospects/src/mock-runtime.ts +242 -0
  46. package/dist/scaffolds/notis-affiliate-prospects/tailwind.config.ts +51 -0
  47. package/dist/scaffolds/notis-affiliate-prospects/tsconfig.json +23 -0
  48. package/dist/scaffolds/notis-affiliate-prospects/vite.config.ts +11 -0
  49. package/dist/scaffolds.json +11 -0
  50. package/package.json +1 -1
  51. package/src/cli.js +36 -7
  52. package/src/command-specs/diagnostics.js +674 -0
  53. package/src/command-specs/helpers.js +4 -1
  54. package/src/command-specs/index.js +6 -0
  55. package/src/command-specs/smoke.js +386 -0
  56. package/src/command-specs/tools.js +187 -7
  57. package/src/runtime/output.js +21 -3
  58. package/src/runtime/profiles.js +198 -13
  59. package/src/runtime/transport.js +17 -1
@@ -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';
@@ -94,6 +101,14 @@ export class OutputManager {
94
101
  process.stderr.write(`${message}\n`);
95
102
  }
96
103
 
104
+ emitProgress({ phase, message, requestId = null }) {
105
+ if (this.runtime.quiet) {
106
+ return;
107
+ }
108
+ const suffix = requestId ? ` (${requestId})` : '';
109
+ process.stderr.write(`[${phase}] ${message}${suffix}\n`);
110
+ }
111
+
97
112
  emitSuccess({
98
113
  ok = true,
99
114
  command,
@@ -116,7 +131,10 @@ export class OutputManager {
116
131
  meta: {
117
132
  profile: this.runtime.profileName,
118
133
  api_base: this.runtime.apiBase,
119
- mutating: Boolean(meta.mutating),
134
+ mutating:
135
+ meta.mutating === true
136
+ ? true
137
+ : (meta.mutating === false ? false : null),
120
138
  ...meta,
121
139
  },
122
140
  };
@@ -142,7 +160,7 @@ export class OutputManager {
142
160
  }
143
161
 
144
162
  if (hints.length) {
145
- const lines = hints.map((hint) => ` ${hint.command} ${hint.reason}`);
163
+ const lines = hints.map(formatHint);
146
164
  process.stdout.write(`\nNext:\n${lines.join('\n')}\n`);
147
165
  }
148
166
 
@@ -171,7 +189,7 @@ export class OutputManager {
171
189
 
172
190
  process.stderr.write(`Error: ${error.message}\n`);
173
191
  for (const hint of error.hints || []) {
174
- process.stderr.write(` ${hint.command} ${hint.reason}\n`);
192
+ process.stderr.write(`${formatHint(hint)}\n`);
175
193
  }
176
194
  return error.exitCode || EXIT_CODES.unexpected;
177
195
  }
@@ -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,121 @@ 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
+ return normalizeConfig(JSON.parse(readFileSync(configFile, 'utf-8')));
86
191
  }
87
192
 
88
- export function saveConfig(config) {
89
- mkdirSync(CONFIG_DIR, { recursive: true });
90
- writeFileSync(CONFIG_FILE, JSON.stringify(normalizeConfig(config), null, 2));
193
+ export function saveConfig(config, runtime = null) {
194
+ const configFile = resolveConfigFile(runtime);
195
+ mkdirSync(dirname(configFile), { recursive: true });
196
+ writeFileSync(configFile, JSON.stringify(normalizeConfig(config), null, 2), { mode: 0o600 });
91
197
  }
92
198
 
93
199
  export function getProfile(config, profileName) {
@@ -194,16 +300,65 @@ export function resolveTimeoutMs(globalOptions = {}) {
194
300
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 30000;
195
301
  }
196
302
 
197
- export function resolveRuntimeProfile(globalOptions = {}, { requireAuth = true } = {}) {
198
- const config = loadConfig();
303
+ export function parseDebugEntitlementOverride(value = process.env.NOTIS_DEBUG_ENTITLEMENT_OVERRIDE) {
304
+ if (!value) {
305
+ return null;
306
+ }
307
+ const candidates = [value];
308
+ try {
309
+ candidates.push(Buffer.from(value, 'base64url').toString('utf-8'));
310
+ } catch {
311
+ // The plain JSON candidate below will provide the actionable error.
312
+ }
313
+ for (const candidate of candidates) {
314
+ try {
315
+ const parsed = JSON.parse(candidate);
316
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
317
+ return parsed;
318
+ }
319
+ } catch {
320
+ // Try the next supported encoding.
321
+ }
322
+ }
323
+ throw new CliError({
324
+ code: 'debug_entitlement_override_invalid',
325
+ message: 'NOTIS_DEBUG_ENTITLEMENT_OVERRIDE must be a JSON object or base64url-encoded JSON object.',
326
+ exitCode: EXIT_CODES.usage,
327
+ });
328
+ }
329
+
330
+ export function resolveRuntimeProfile(
331
+ globalOptions = {},
332
+ { requireAuth = true, includeDebugEntitlementOverride = true } = {},
333
+ ) {
334
+ const worktreeRuntime = resolveWorktreeRuntime();
335
+ const config = loadConfig(worktreeRuntime);
199
336
  const profileName = getCurrentProfileName(config, globalOptions.profile);
200
- const apiBase = getApiBase(config, profileName, globalOptions.apiBase);
201
- const jwt = getJwt(config, profileName);
337
+ const requestedApiBase = globalOptions.apiBase;
338
+ if (
339
+ worktreeRuntime &&
340
+ requestedApiBase &&
341
+ requestedApiBase.replace(/\/+$/, '') !== worktreeRuntime.api_base
342
+ ) {
343
+ throw new CliError({
344
+ code: 'dev_runtime_route_mismatch',
345
+ message: `This local-only worktree cannot route to ${requestedApiBase}`,
346
+ exitCode: EXIT_CODES.usage,
347
+ hints: [{ message: `Expected local API: ${worktreeRuntime.api_base}` }],
348
+ });
349
+ }
350
+ const apiBase = worktreeRuntime
351
+ ? worktreeRuntime.api_base
352
+ : getApiBase(config, profileName, globalOptions.apiBase);
353
+ const jwt = worktreeRuntime ? getProfile(config, profileName).jwt : getJwt(config, profileName);
202
354
  const profile = getProfile(config, profileName);
203
355
  const agentMode = isAgentMode(globalOptions);
204
356
  const nonInteractive = isNonInteractive(globalOptions);
205
357
  const outputMode = resolveOutputMode(globalOptions);
206
358
  const timeoutMs = resolveTimeoutMs(globalOptions);
359
+ const debugEntitlementOverride = includeDebugEntitlementOverride
360
+ ? parseDebugEntitlementOverride()
361
+ : null;
207
362
 
208
363
  if (requireAuth && !jwt) {
209
364
  const recovery = getDesktopAuthRecovery({
@@ -218,22 +373,38 @@ export function resolveRuntimeProfile(globalOptions = {}, { requireAuth = true }
218
373
  hints: recovery.hints,
219
374
  });
220
375
  }
376
+ if (
377
+ worktreeRuntime?.expected_user_id &&
378
+ getJwtSubject(jwt) !== worktreeRuntime.expected_user_id
379
+ ) {
380
+ throw new CliError({
381
+ code: 'dev_runtime_identity_mismatch',
382
+ message: 'The scoped dev credential does not belong to this worktree test user',
383
+ exitCode: EXIT_CODES.auth,
384
+ hints: [
385
+ { message: 'Restart ./dev.sh to restore the approved worktree identity.' },
386
+ { message: `Expected user: ${worktreeRuntime.expected_user_id}` },
387
+ ],
388
+ });
389
+ }
221
390
 
222
391
  // An explicit NOTIS_JWT is a complete credential override. Use it verbatim
223
392
  // and never replace it with a token later synced by the desktop profile.
224
- const usingEnvJwt = Boolean(process.env.NOTIS_JWT);
393
+ const usingEnvJwt = !worktreeRuntime && Boolean(process.env.NOTIS_JWT);
225
394
  return {
226
395
  config,
227
396
  profileName,
228
397
  apiBase,
229
398
  jwt,
230
- credentialSource: usingEnvJwt ? 'env' : 'profile',
399
+ credentialSource: worktreeRuntime ? 'worktree' : usingEnvJwt ? 'env' : 'profile',
231
400
  desktopAppName: usingEnvJwt ? undefined : profile.desktop_app_name,
232
401
  desktopPid: usingEnvJwt ? undefined : profile.desktop_pid,
233
402
  agentMode,
234
403
  nonInteractive,
235
404
  outputMode,
236
405
  timeoutMs,
406
+ debugEntitlementOverride,
407
+ worktreeRuntime,
237
408
  };
238
409
  }
239
410
 
@@ -251,6 +422,20 @@ export function getJwtExpiration(jwt) {
251
422
  }
252
423
  }
253
424
 
425
+ export function getJwtSubject(jwt) {
426
+ if (typeof jwt !== 'string' || !jwt) {
427
+ return null;
428
+ }
429
+ try {
430
+ const parts = jwt.split('.');
431
+ if (parts.length !== 3) return null;
432
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
433
+ return typeof payload.sub === 'string' && payload.sub ? payload.sub : null;
434
+ } catch {
435
+ return null;
436
+ }
437
+ }
438
+
254
439
  export function isJwtExpired(jwt, nowSeconds = Math.floor(Date.now() / 1000)) {
255
440
  const expiration = getJwtExpiration(jwt);
256
441
  return expiration !== null && expiration <= nowSeconds;
@@ -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';
@@ -149,7 +150,7 @@ function reloadJwtFromConfig(runtime) {
149
150
  const profileName = runtime.profileName || DEFAULT_PROFILE;
150
151
  let profile;
151
152
  try {
152
- profile = getProfile(loadConfig(), profileName);
153
+ profile = getProfile(loadConfig(runtime.worktreeRuntime), profileName);
153
154
  } catch {
154
155
  return false;
155
156
  }
@@ -157,6 +158,18 @@ function reloadJwtFromConfig(runtime) {
157
158
  if (!nextJwt || nextJwt === runtime.jwt) {
158
159
  return false;
159
160
  }
161
+ const expectedUserId = runtime.worktreeRuntime?.expected_user_id;
162
+ if (expectedUserId && getJwtSubject(nextJwt) !== expectedUserId) {
163
+ throw new CliError({
164
+ code: 'dev_runtime_identity_mismatch',
165
+ message: 'The refreshed scoped dev credential does not belong to this worktree test user',
166
+ exitCode: EXIT_CODES.auth,
167
+ hints: [
168
+ { message: 'Restart ./dev.sh to restore the approved worktree identity.' },
169
+ { message: `Expected user: ${expectedUserId}` },
170
+ ],
171
+ });
172
+ }
160
173
  runtime.jwt = nextJwt;
161
174
  runtime.desktopAppName = profile.desktop_app_name;
162
175
  runtime.desktopPid = profile.desktop_pid;
@@ -318,6 +331,9 @@ export async function callTool({
318
331
  cwd: process.cwd(),
319
332
  agent_mode: runtime.agentMode,
320
333
  cli_version: runtime.cliVersion,
334
+ ...(runtime.debugEntitlementOverride
335
+ ? { debug_entitlement_override: runtime.debugEntitlementOverride }
336
+ : {}),
321
337
  },
322
338
  };
323
339