@notis_ai/cli 0.2.0-beta.121.1 → 0.2.0-beta.124.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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "source": "apps/*/notis.config.ts",
2
+ "source": "scaffolds/*/notis.config.ts",
3
3
  "scaffolds": [
4
4
  {
5
5
  "slug": "notis-database",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notis_ai/cli",
3
- "version": "0.2.0-beta.121.1",
3
+ "version": "0.2.0-beta.124.1",
4
4
  "description": "Agent-first Notis CLI for apps and generic tool execution",
5
5
  "type": "module",
6
6
  "bin": {
@@ -470,13 +470,13 @@ Runs the real desktop-local development workflow. The CLI should discover all ap
470
470
 
471
471
  ## Deploy Without Backend Server
472
472
 
473
- If the backend server at `localhost:3001` is not running, use `--direct`:
473
+ If the live API is unreachable, use `--direct`:
474
474
 
475
475
  ```bash
476
476
  npx --package @notis_ai/cli@latest -- notis apps deploy --direct
477
477
  ```
478
478
 
479
- This uploads the bundle and editable source snapshot directly to Supabase storage and updates the app manifest in the database, bypassing the backend server entirely. The CLI auto-falls back to direct mode on network errors.
479
+ This uploads the bundle and editable source snapshot directly to Supabase storage and updates the app manifest in the database, bypassing the API server. The CLI auto-falls back to direct mode on network errors. Localhost backends are reserved for `/notis-tests` via `./dev.sh`; do not retarget the personal CLI lane at loopback from this skill.
480
480
 
481
481
  ## Testing
482
482
 
@@ -134,13 +134,13 @@ This is the main escape hatch for:
134
134
  ### Tool access workflow
135
135
 
136
136
  1. List available toolkit namespaces:
137
- - `npx --package @notis_ai/cli@latest -- notis tools toolkits`
137
+ - `npx --package @notis_ai/cli@latest -- notis tools toolkits --timeout-ms 90000`
138
138
  2. Search for the capability you need using natural language:
139
- - `npx --package @notis_ai/cli@latest -- notis tools search "<query>"`
139
+ - `npx --package @notis_ai/cli@latest -- notis tools search "<query>" --timeout-ms 90000`
140
140
  - optionally add known field hints with `--known-fields "<key:value>"`
141
141
  3. If needed, inspect the exact tool and parameter schema:
142
- - `npx --package @notis_ai/cli@latest -- notis tools describe <tool-name>`
143
- - `npx --package @notis_ai/cli@latest -- notis tools exec <tool-name> --get-schema`
142
+ - `npx --package @notis_ai/cli@latest -- notis tools describe <tool-name> --timeout-ms 90000`
143
+ - `npx --package @notis_ai/cli@latest -- notis tools exec <tool-name> --get-schema --timeout-ms 90000`
144
144
  4. Validate arguments before execution when the tool is mutating or the schema is non-trivial:
145
145
  - `npx --package @notis_ai/cli@latest -- notis tools exec <tool-name> --dry-run --arguments '<json>'`
146
146
  5. Execute the tool:
@@ -151,6 +151,22 @@ This is the main escape hatch for:
151
151
  - `npx --package @notis_ai/cli@latest -- notis tools link <toolkit>`
152
152
  - For a revoked or invalid credential-based connection, reconnect with credential JSON on stdin: `npx --package @notis_ai/cli@latest -- notis tools link <toolkit> --reconnect --credentials -`
153
153
 
154
+ ### Discovery latency and caching
155
+
156
+ The discovery bridge may query several connected MCP servers on a cold run and
157
+ can legitimately take longer than the CLI's general 30-second timeout. Always
158
+ use `--timeout-ms 90000` for `tools toolkits`, `tools search`, `tools describe`,
159
+ and schema-only discovery calls. If a discovery call returns `network_timeout`,
160
+ retry that same command once with `--timeout-ms 90000`; do not start a new
161
+ query, invent a tool name, or loop on the default 30-second command.
162
+
163
+ Discovery is idempotent but should be bounded: run the toolkit listing once per
164
+ task, run one natural-language search per distinct capability, and cache the
165
+ returned canonical tool names and schemas for the rest of the current turn.
166
+ After a successful search/schema response, call the returned canonical tool
167
+ directly (with a dry-run before mutations) instead of repeating the same
168
+ discovery request before every connected-service action.
169
+
154
170
  ### Tool access rules
155
171
 
156
172
  - Never guess tool names. Discover them with `npx --package @notis_ai/cli@latest -- notis tools search` first.
@@ -266,15 +282,18 @@ desktop session yourself.
266
282
 
267
283
  ### Deploy fails with "network_error" or "fetch failed"
268
284
 
269
- The backend server at `http://localhost:3001` is not running. Solutions:
285
+ The CLI defaults to the live Notis API (`https://api.notis.ai`, or
286
+ `https://api-beta.notis.ai` when the signed-in user is on beta). Solutions:
287
+
288
+ 1. Run `npx --package @notis_ai/cli@latest -- notis doctor` and confirm `api_base` is a live Notis host
289
+ 2. Use `--direct` for app deploys when you only need Supabase storage upload: `npx --package @notis_ai/cli@latest -- notis apps deploy --direct`
290
+ 3. If auth looks stale, open Notis Desktop (or `Notis Beta`), wait for it to restore the session, then retry
270
291
 
271
- 1. Use `--direct` flag: `npx --package @notis_ai/cli@latest -- notis apps deploy --direct` uploads to Supabase storage directly
272
- 2. Start the backend server: `cd server && python3 main.py`
273
- 3. Check the API base: `npx --package @notis_ai/cli@latest -- notis doctor` shows the configured `api_base`
292
+ Localhost backends are a Notis-developer test lane only. Do not retarget the CLI at loopback from this skill — that path is owned by `/notis-tests` via `./dev.sh` and the worktree runtime lease.
274
293
 
275
294
  ### `npx --package @notis_ai/cli@latest -- notis doctor` shows health/tool_roundtrip errors
276
295
 
277
- The CLI health check pings the backend server. If it's not running, these checks fail. App development commands that are `backend_call: local` (`init`, `build`, `verify`, `link`, `doctor`) work without the server. `dev`, `pull`, `create`, `list`, and normal `deploy` need the backend; `deploy --direct` can bypass it when Supabase credentials are available.
296
+ The CLI health check pings the configured live API. App development commands that are `backend_call: local` (`init`, `build`, `verify`, `link`, `doctor`) work offline. `dev`, `pull`, `create`, `list`, and normal `deploy` need the live API; `deploy --direct` can bypass it when Supabase credentials are available.
278
297
 
279
298
  ### Stale bundle in the portal after deploy
280
299
 
@@ -24,7 +24,7 @@ const BUNDLE_DIR = join(OUTPUT_DIR, 'bundle');
24
24
  const MANIFEST_FILE = join(OUTPUT_DIR, 'manifest.json');
25
25
  const METADATA_DIR = 'metadata';
26
26
  const CLI_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
27
- const MONOREPO_APPS_DIR = resolve(CLI_ROOT, '../..', 'apps');
27
+ const MONOREPO_SCAFFOLDS_DIR = resolve(CLI_ROOT, '../..', 'scaffolds');
28
28
  const DIST_DIR = join(CLI_ROOT, 'dist');
29
29
  const SCAFFOLD_CATALOG_FILE = join(DIST_DIR, 'scaffolds.json');
30
30
  const SCAFFOLD_SOURCE_DIR = join(DIST_DIR, 'scaffolds');
@@ -1075,7 +1075,7 @@ function resolveScaffoldSourceDir(fromSlug) {
1075
1075
  if (existsSync(bundledDir)) {
1076
1076
  return bundledDir;
1077
1077
  }
1078
- const monorepoDir = join(MONOREPO_APPS_DIR, fromSlug);
1078
+ const monorepoDir = join(MONOREPO_SCAFFOLDS_DIR, fromSlug);
1079
1079
  if (existsSync(join(monorepoDir, 'notis.config.ts'))) {
1080
1080
  return monorepoDir;
1081
1081
  }
@@ -1083,15 +1083,15 @@ function resolveScaffoldSourceDir(fromSlug) {
1083
1083
  }
1084
1084
 
1085
1085
  function loadMonorepoScaffoldCatalog() {
1086
- if (!existsSync(MONOREPO_APPS_DIR)) {
1086
+ if (!existsSync(MONOREPO_SCAFFOLDS_DIR)) {
1087
1087
  return [];
1088
1088
  }
1089
1089
  const scaffolds = [];
1090
- for (const entry of readdirSync(MONOREPO_APPS_DIR, { withFileTypes: true })) {
1090
+ for (const entry of readdirSync(MONOREPO_SCAFFOLDS_DIR, { withFileTypes: true })) {
1091
1091
  if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_')) {
1092
1092
  continue;
1093
1093
  }
1094
- const configPath = join(MONOREPO_APPS_DIR, entry.name, 'notis.config.ts');
1094
+ const configPath = join(MONOREPO_SCAFFOLDS_DIR, entry.name, 'notis.config.ts');
1095
1095
  if (!existsSync(configPath)) {
1096
1096
  continue;
1097
1097
  }
@@ -1,4 +1,5 @@
1
1
  // Auto-generated on npm publish.
2
- // Committed value is 'local' so in-repo runs target localhost.
3
- // scripts/set-cli-mode.js rewrites this to 'published' before the npm publish step.
4
- export const MODE = 'local';
2
+ // Committed value is 'published' so the CLI defaults to the live Notis API.
3
+ // Localhost overrides come only from the worktree test lease (`./dev.sh`).
4
+ // scripts/set-cli-mode.js can rewrite this for publish/labeling experiments.
5
+ export const MODE = 'published';
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * CLI mode detection for `notis apps dev`.
3
3
  *
4
- * The repo-local CLI package targets localhost; the published npm CLI package
5
- * targets app.notis.ai. Mode is baked into `cli-mode.generated.js` at publish
6
- * timeno runtime flag is needed (and none is exposed to end users, so a
7
- * crafted env var can't trick a published CLI into opening a localhost portal
8
- * that isn't running).
4
+ * The published npm CLI and the repo-local checkout both default to the live
5
+ * Notis API (`api.notis.ai` / `api-beta.notis.ai`). Localhost is not a CLI
6
+ * defaultthe `/notis-tests` worktree lease (`./dev.sh`) is the only
7
+ * supported loopback override for Notis developers.
9
8
  *
10
- * The `NOTIS_CLI_MODE` env var is honored for internal testing only.
9
+ * Mode is baked into `cli-mode.generated.js` at publish time. The
10
+ * `NOTIS_CLI_MODE` env var is honored for internal labeling only (e.g. the
11
+ * `apps` auto-dev loop inside `./dev.sh`).
11
12
  */
12
13
 
13
14
  import { MODE as BAKED_MODE } from './cli-mode.generated.js';
@@ -20,8 +21,12 @@ export function getCliMode() {
20
21
  return BAKED_MODE === 'published' ? 'published' : 'local';
21
22
  }
22
23
 
23
- export function getDefaultApiBase(mode = getCliMode()) {
24
- return mode === 'local' ? 'http://localhost:3001' : 'https://api.notis.ai';
24
+ export function getDefaultApiBase(mode = getCliMode(), { beta = false } = {}) {
25
+ // Mode no longer switches the default API to localhost. Local loopback is
26
+ // reserved for the worktree test lease; `mode` only affects portal origin
27
+ // labeling for the in-repo apps-dev helper.
28
+ void mode;
29
+ return beta ? 'https://api-beta.notis.ai' : 'https://api.notis.ai';
25
30
  }
26
31
 
27
32
  export function getDefaultPortalOrigin(mode = getCliMode()) {
@@ -668,12 +668,28 @@ async function exchangeCode(metadata, {
668
668
  function persistOAuthTokenResponse(runtime, metadata, tokenResponse) {
669
669
  const now = Math.floor(Date.now() / 1000);
670
670
  const payload = decodeJwtPayload(tokenResponse.access_token);
671
+ const oauthApiBase = (metadata.apiBase || runtime.apiBase || '').replace(/\/+$/, '');
672
+ let beta;
673
+ try {
674
+ const hostname = new URL(oauthApiBase).hostname;
675
+ if (hostname === 'api-beta.notis.ai') beta = true;
676
+ else if (hostname === 'api.notis.ai') beta = false;
677
+ } catch {
678
+ beta = undefined;
679
+ }
671
680
  const config = updateConfig((latest) => {
672
681
  const next = ensureProfile(latest, runtime.profileName);
673
682
  const profile = next.profiles[runtime.profileName];
674
683
  next.profiles[runtime.profileName] = {
675
684
  ...profile,
676
- oauth_api_base: metadata.apiBase || runtime.apiBase,
685
+ // Keep Desktop's api_base intact when an independent OAuth grant refreshes.
686
+ // Only seed api_base from OAuth when the profile has no live API yet.
687
+ api_base:
688
+ profile.api_base && !/^https?:\/\/(localhost|127\.0\.0\.1|::1)(:|\/|$)/i.test(profile.api_base)
689
+ ? profile.api_base
690
+ : (oauthApiBase || profile.api_base),
691
+ beta: typeof profile.beta === 'boolean' ? profile.beta : beta,
692
+ oauth_api_base: oauthApiBase || profile.oauth_api_base,
677
693
  oauth_resource: metadata.resource,
678
694
  oauth_access_token: tokenResponse.access_token,
679
695
  oauth_refresh_token: tokenResponse.refresh_token || profile.oauth_refresh_token,
@@ -17,14 +17,12 @@ export const CONFIG_DIR = join(homedir(), '.notis');
17
17
  export const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
18
18
  export const WORKSPACE_DIR = join(CONFIG_DIR, 'workspace');
19
19
  export const DEFAULT_API_BASE = 'https://api.notis.ai';
20
+ export const BETA_API_BASE = 'https://api-beta.notis.ai';
20
21
  export const DEFAULT_PROFILE = 'default';
21
22
  const WORKTREE_RUNTIME_FILENAME = join('.context', 'notis-runtime.json');
22
23
  const WORKTREE_ROUTING_FILENAME = join('.context', 'notis-routing.json');
23
- const LOCAL_DEFAULT_API_BASES = new Set([
24
- 'http://localhost:3001',
25
- 'http://127.0.0.1:3001',
26
- ]);
27
24
  const LOCAL_API_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
25
+ const LIVE_API_HOSTS = new Set(['api.notis.ai', 'api-beta.notis.ai']);
28
26
  // Cross-process write lock over ~/.notis/config.json. Notis Desktop implements
29
27
  // the same protocol independently in electron/src/cli-auth.ts (updateConfig);
30
28
  // the `${configFile}.write-lock` directory name and these three timings must
@@ -51,6 +49,7 @@ export function normalizeConfig(rawConfig = {}) {
51
49
  profiles[name] = {
52
50
  jwt: typeof profile.jwt === 'string' ? profile.jwt : undefined,
53
51
  api_base: typeof profile.api_base === 'string' ? profile.api_base : undefined,
52
+ beta: typeof profile.beta === 'boolean' ? profile.beta : undefined,
54
53
  auth_mode: profile.auth_mode === 'dev_portal' ? profile.auth_mode : undefined,
55
54
  refresh_token:
56
55
  typeof profile.refresh_token === 'string' ? profile.refresh_token : undefined,
@@ -105,6 +104,7 @@ export function normalizeConfig(rawConfig = {}) {
105
104
  [DEFAULT_PROFILE]: {
106
105
  jwt: typeof raw.jwt === 'string' ? raw.jwt : undefined,
107
106
  api_base: typeof raw.api_base === 'string' ? raw.api_base : undefined,
107
+ beta: typeof raw.beta === 'boolean' ? raw.beta : undefined,
108
108
  auth_mode: raw.auth_mode === 'dev_portal' ? raw.auth_mode : undefined,
109
109
  refresh_token: typeof raw.refresh_token === 'string' ? raw.refresh_token : undefined,
110
110
  access_expires_at:
@@ -425,6 +425,48 @@ function isLocalApiBase(value) {
425
425
  }
426
426
  }
427
427
 
428
+ function isLiveApiBase(value) {
429
+ if (typeof value !== 'string' || !value) {
430
+ return false;
431
+ }
432
+ try {
433
+ const parsed = new URL(value);
434
+ return parsed.protocol === 'https:' && LIVE_API_HOSTS.has(parsed.hostname);
435
+ } catch {
436
+ return false;
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Pick the live Notis API for this profile.
442
+ *
443
+ * Beta users (`users.beta = true`, mirrored onto the CLI profile by Desktop
444
+ * sync / OAuth against api-beta) hit api-beta.notis.ai; everyone else hits
445
+ * api.notis.ai. Localhost is never a default — only the worktree test lease
446
+ * (`./dev.sh` / `/notis-tests`) may retarget the CLI at loopback.
447
+ */
448
+ export function resolveDefaultLiveApiBase(profile = {}) {
449
+ if (profile.beta === true) {
450
+ return BETA_API_BASE;
451
+ }
452
+ if (profile.beta === false) {
453
+ return DEFAULT_API_BASE;
454
+ }
455
+ if (profile.desktop_app_name === 'Notis Beta') {
456
+ return BETA_API_BASE;
457
+ }
458
+ if (isLiveApiBase(profile.api_base)) {
459
+ try {
460
+ if (new URL(profile.api_base).hostname === 'api-beta.notis.ai') {
461
+ return BETA_API_BASE;
462
+ }
463
+ } catch {
464
+ // fall through
465
+ }
466
+ }
467
+ return DEFAULT_API_BASE;
468
+ }
469
+
428
470
  export function getApiBase(config, profileName, override) {
429
471
  if (override) {
430
472
  return override;
@@ -435,20 +477,16 @@ export function getApiBase(config, profileName, override) {
435
477
  }
436
478
  const profile = getProfile(config, profileName);
437
479
  const profileApiBase = profile.api_base;
438
- const conductorPort = Number.parseInt(process.env.CONDUCTOR_PORT || '', 10);
439
480
 
440
- // Conductor assigns dynamic local ports. When a workspace is running under
441
- // Conductor, treat saved localhost profile values as stale and transparently
442
- // retarget the CLI at the active backend port.
443
- if (
444
- Number.isInteger(conductorPort) &&
445
- conductorPort > 0 &&
446
- (!profileApiBase || LOCAL_DEFAULT_API_BASES.has(profileApiBase) || isLocalApiBase(profileApiBase))
447
- ) {
448
- return `http://localhost:${conductorPort + 1}`;
481
+ // Prefer an explicit non-loopback API stored on the profile (Desktop sync /
482
+ // OAuth / custom overrides). Ignore stale localhost values loopback
483
+ // routing is owned exclusively by the worktree runtime lease under
484
+ // `/notis-tests`, not by CONDUCTOR_PORT or leftover local profile state.
485
+ if (typeof profileApiBase === 'string' && profileApiBase && !isLocalApiBase(profileApiBase)) {
486
+ return profileApiBase.replace(/\/+$/, '');
449
487
  }
450
488
 
451
- return profileApiBase || DEFAULT_API_BASE;
489
+ return resolveDefaultLiveApiBase(profile);
452
490
  }
453
491
 
454
492
  export function getJwt(config, profileName) {