@notis_ai/cli 0.2.22 → 0.2.24

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.
package/README.md CHANGED
@@ -85,7 +85,7 @@ Options:
85
85
  Examples:
86
86
  - `npx --package @notis_ai/cli@latest -- notis login`
87
87
  - `npx --package @notis_ai/cli@latest -- notis login --profile work`
88
- - `npx --package @notis_ai/cli@latest -- notis login --profile beta --api-base https://api-beta.notis.ai`
88
+ - `npx --package @notis_ai/cli@latest -- notis login --profile nightly --api-base https://api-nightly.notis.ai`
89
89
  - `npx --package @notis_ai/cli@latest -- notis login --no-browser --print-url`
90
90
  - `npx --package @notis_ai/cli@latest -- notis login --mode browser`
91
91
  - `npx --package @notis_ai/cli@latest -- notis login --mode code`
@@ -7434,6 +7434,18 @@ var init_config_lock = __esm({
7434
7434
  function isReleaseChannel(value) {
7435
7435
  return RELEASE_CHANNELS.includes(value);
7436
7436
  }
7437
+ function normalizeReleaseChannel(value) {
7438
+ if (value === "beta") return "nightly";
7439
+ return isReleaseChannel(value) ? value : null;
7440
+ }
7441
+ function channelFromApiBase(apiBase) {
7442
+ if (typeof apiBase !== "string" || !apiBase) return null;
7443
+ try {
7444
+ return API_HOST_CHANNELS[new URL(apiBase).hostname] || null;
7445
+ } catch {
7446
+ return null;
7447
+ }
7448
+ }
7437
7449
  function packageTagForChannel(channel) {
7438
7450
  return CHANNEL_TAGS[channel] || CHANNEL_TAGS.stable;
7439
7451
  }
@@ -7441,22 +7453,19 @@ function cliCommandForChannel(channel) {
7441
7453
  return `npx --package ${CLI_PACKAGE_NAME}@${packageTagForChannel(channel)} -- notis`;
7442
7454
  }
7443
7455
  function channelFromVersion(version) {
7444
- return String(version || "").includes("-") ? "beta" : "stable";
7456
+ return String(version || "").includes("-") ? "nightly" : "stable";
7445
7457
  }
7446
- function channelFromProfile(profile = {}) {
7447
- if (isReleaseChannel(profile.channel)) {
7448
- return profile.channel;
7449
- }
7450
- if (profile.beta === true) return "beta";
7458
+ function pinnedReleaseChannel(profile = {}) {
7459
+ if (profile.beta === true) return "nightly";
7451
7460
  if (profile.beta === false) return "stable";
7461
+ return normalizeReleaseChannel(profile.channel) || normalizeReleaseChannel(profile.release_channel);
7462
+ }
7463
+ function channelFromProfile(profile = {}) {
7464
+ const pinned = pinnedReleaseChannel(profile);
7465
+ if (pinned) return pinned;
7452
7466
  for (const candidate of [profile.oauth_api_base, profile.api_base]) {
7453
- if (typeof candidate !== "string" || !candidate) continue;
7454
- try {
7455
- const { hostname } = new URL(candidate);
7456
- if (hostname === "api-beta.notis.ai") return "beta";
7457
- if (hostname === "api.notis.ai") return "stable";
7458
- } catch {
7459
- }
7467
+ const channel = channelFromApiBase(candidate);
7468
+ if (channel) return channel;
7460
7469
  }
7461
7470
  return null;
7462
7471
  }
@@ -7487,7 +7496,9 @@ function resolveChannelSwitch({
7487
7496
  const runningChannel = channelFromVersion(runningVersion);
7488
7497
  const targetChannel = channelFromProfile(profile);
7489
7498
  const stay = (reason) => ({ switch: false, reason, runningChannel, targetChannel });
7490
- if (env[CHANNEL_SWITCH_ENV] === "1") return stay("already_switched");
7499
+ if (env[CHANNEL_SWITCH_ENV] && env[CHANNEL_SWITCH_ENV] !== LEGACY_CHANNEL_SWITCHED) {
7500
+ return stay("already_switched");
7501
+ }
7491
7502
  if (env[CHANNEL_DISABLE_ENV] === "0") return stay("disabled");
7492
7503
  if (!targetChannel) return stay("profile_channel_unknown");
7493
7504
  if (targetChannel === runningChannel) return stay("channel_matches");
@@ -7502,13 +7513,19 @@ function resolveChannelSwitch({
7502
7513
  args: ["--yes", "--package", `${CLI_PACKAGE_NAME}@${packageTagForChannel(targetChannel)}`, "--", "notis"]
7503
7514
  };
7504
7515
  }
7505
- var RELEASE_CHANNELS, CHANNEL_TAGS, CLI_PACKAGE_NAME, CHANNEL_SWITCH_ENV, CHANNEL_DISABLE_ENV;
7516
+ var RELEASE_CHANNELS, CHANNEL_TAGS, API_HOST_CHANNELS, CLI_PACKAGE_NAME, CHANNEL_SWITCH_ENV, LEGACY_CHANNEL_SWITCHED, CHANNEL_DISABLE_ENV;
7506
7517
  var init_channel = __esm({
7507
7518
  "src/runtime/channel.js"() {
7508
- RELEASE_CHANNELS = ["stable", "beta"];
7509
- CHANNEL_TAGS = { stable: "latest", beta: "beta" };
7519
+ RELEASE_CHANNELS = ["stable", "nightly"];
7520
+ CHANNEL_TAGS = { stable: "latest", nightly: "nightly" };
7521
+ API_HOST_CHANNELS = {
7522
+ "api.notis.ai": "stable",
7523
+ "api-beta.notis.ai": "nightly",
7524
+ "api-nightly.notis.ai": "nightly"
7525
+ };
7510
7526
  CLI_PACKAGE_NAME = "@notis_ai/cli";
7511
7527
  CHANNEL_SWITCH_ENV = "NOTIS_CLI_CHANNEL_SWITCHED";
7528
+ LEGACY_CHANNEL_SWITCHED = "1";
7512
7529
  CHANNEL_DISABLE_ENV = "NOTIS_CLI_AUTO_CHANNEL";
7513
7530
  }
7514
7531
  });
@@ -7763,11 +7780,10 @@ function normalizeProfile(rawProfile = {}) {
7763
7780
  const raw = rawProfile && typeof rawProfile === "object" ? rawProfile : {};
7764
7781
  return {
7765
7782
  api_base: typeof raw.api_base === "string" ? raw.api_base : void 0,
7766
- beta: typeof raw.beta === "boolean" ? raw.beta : void 0,
7767
- // Which published CLI build this profile runs, as the deployment reported
7768
- // it at login. Unknown values are dropped rather than trusted: this key
7769
- // decides which code executes on the next run.
7770
- channel: isReleaseChannel(raw.channel) ? raw.channel : void 0,
7783
+ // Which published CLI build this profile runs (`stable` | `nightly`), as
7784
+ // the account reported it at login. Unknown values are dropped rather
7785
+ // than trusted: this key decides which code executes on the next run.
7786
+ release_channel: pinnedReleaseChannel(raw) || void 0,
7771
7787
  label: typeof raw.label === "string" ? raw.label : void 0,
7772
7788
  dev_access_token: typeof raw.dev_access_token === "string" ? raw.dev_access_token : void 0,
7773
7789
  dev_access_expires_at: typeof raw.dev_access_expires_at === "number" ? raw.dev_access_expires_at : void 0,
@@ -8048,21 +8064,8 @@ function isLiveApiBase(value) {
8048
8064
  }
8049
8065
  }
8050
8066
  function resolveDefaultLiveApiBase(profile = {}) {
8051
- if (profile.beta === true) {
8052
- return BETA_API_BASE;
8053
- }
8054
- if (profile.beta === false) {
8055
- return DEFAULT_API_BASE;
8056
- }
8057
- if (isLiveApiBase(profile.api_base)) {
8058
- try {
8059
- if (new URL(profile.api_base).hostname === "api-beta.notis.ai") {
8060
- return BETA_API_BASE;
8061
- }
8062
- } catch {
8063
- }
8064
- }
8065
- return DEFAULT_API_BASE;
8067
+ const channel = pinnedReleaseChannel(profile) || (isLiveApiBase(profile.api_base) ? channelFromProfile({ api_base: profile.api_base }) : null);
8068
+ return channel === "nightly" ? NIGHTLY_API_BASE : DEFAULT_API_BASE;
8066
8069
  }
8067
8070
  function getApiBase(config, profileName, override) {
8068
8071
  if (override) {
@@ -8366,7 +8369,7 @@ function credentialIsExpired(runtime, profile = {}, nowSeconds = Math.floor(Date
8366
8369
  function workspacePath(appId) {
8367
8370
  return join2(WORKSPACE_DIR, appId);
8368
8371
  }
8369
- var CONFIG_DIR, CONFIG_FILE, WORKSPACE_DIR, DEFAULT_API_BASE, BETA_API_BASE, DEFAULT_PROFILE, PROFILE_NAME_PATTERN, LEGACY_DESKTOP_PROFILE_KEYS, WORKTREE_RUNTIME_FILENAME, WORKTREE_ROUTING_FILENAME, LOCAL_API_HOSTS, LIVE_API_HOSTS;
8372
+ var CONFIG_DIR, CONFIG_FILE, WORKSPACE_DIR, DEFAULT_API_BASE, NIGHTLY_API_BASE, DEFAULT_PROFILE, PROFILE_NAME_PATTERN, LEGACY_DESKTOP_PROFILE_KEYS, WORKTREE_RUNTIME_FILENAME, WORKTREE_ROUTING_FILENAME, LOCAL_API_HOSTS, LIVE_API_HOSTS;
8370
8373
  var init_profiles = __esm({
8371
8374
  "src/runtime/profiles.js"() {
8372
8375
  init_errors();
@@ -8378,7 +8381,7 @@ var init_profiles = __esm({
8378
8381
  CONFIG_FILE = join2(CONFIG_DIR, "config.json");
8379
8382
  WORKSPACE_DIR = join2(CONFIG_DIR, "workspace");
8380
8383
  DEFAULT_API_BASE = "https://api.notis.ai";
8381
- BETA_API_BASE = "https://api-beta.notis.ai";
8384
+ NIGHTLY_API_BASE = "https://api-nightly.notis.ai";
8382
8385
  DEFAULT_PROFILE = "default";
8383
8386
  PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
8384
8387
  LEGACY_DESKTOP_PROFILE_KEYS = [
@@ -8393,7 +8396,7 @@ var init_profiles = __esm({
8393
8396
  WORKTREE_RUNTIME_FILENAME = join2(".context", "notis-runtime.json");
8394
8397
  WORKTREE_ROUTING_FILENAME = join2(".context", "notis-routing.json");
8395
8398
  LOCAL_API_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
8396
- LIVE_API_HOSTS = /* @__PURE__ */ new Set(["api.notis.ai", "api-beta.notis.ai"]);
8399
+ LIVE_API_HOSTS = /* @__PURE__ */ new Set(["api.notis.ai", "api-nightly.notis.ai", "api-beta.notis.ai"]);
8397
8400
  }
8398
8401
  });
8399
8402
 
@@ -8807,8 +8810,9 @@ async function discoverCliOAuth(apiBase, fetchImpl = fetch) {
8807
8810
  copyPasteRedirectUri: protectedResource.notis_cli_copy_paste_redirect_uri,
8808
8811
  // A deployment that predates channel advertising, or a local one with no
8809
8812
  // published build, leaves this null and the profile keeps resolving its
8810
- // channel from the endpoint it authorized against.
8811
- channel: isReleaseChannel(protectedResource.notis_cli_channel) ? protectedResource.notis_cli_channel : null,
8813
+ // channel from the endpoint it authorized against. Deployments from before
8814
+ // the rename still answer `beta`, which is the nightly channel.
8815
+ channel: normalizeReleaseChannel(protectedResource.notis_cli_channel),
8812
8816
  authorizationEndpoint: authorizationServer.authorization_endpoint,
8813
8817
  tokenEndpoint: authorizationServer.token_endpoint,
8814
8818
  revocationEndpoint: authorizationServer.revocation_endpoint
@@ -9327,14 +9331,6 @@ function persistOAuthTokenResponse(runtime, metadata, tokenResponse) {
9327
9331
  const now = Math.floor(Date.now() / 1e3);
9328
9332
  const payload = decodeJwtPayload(tokenResponse.access_token);
9329
9333
  const oauthApiBase = (metadata.apiBase || runtime.apiBase || "").replace(/\/+$/, "");
9330
- let beta;
9331
- try {
9332
- const hostname = new URL(oauthApiBase).hostname;
9333
- if (hostname === "api-beta.notis.ai") beta = true;
9334
- else if (hostname === "api.notis.ai") beta = false;
9335
- } catch {
9336
- beta = void 0;
9337
- }
9338
9334
  const config = updateConfig((latest) => {
9339
9335
  const next = ensureProfile(latest, runtime.profileName);
9340
9336
  const profile = next.profiles[runtime.profileName];
@@ -9344,11 +9340,12 @@ function persistOAuthTokenResponse(runtime, metadata, tokenResponse) {
9344
9340
  // one API, and the environment the user just authorized against is the
9345
9341
  // only endpoint the resulting token is accepted by.
9346
9342
  api_base: oauthApiBase || profile.api_base,
9347
- beta: beta ?? profile.beta,
9348
9343
  // The deployment that just authorized this profile also names the
9349
9344
  // published build that belongs to it. Pinning it here is what lets the
9350
9345
  // next run correct itself without the user knowing a channel exists.
9351
- channel: isReleaseChannel(metadata.channel) ? metadata.channel : channelFromProfile({ ...profile, beta: beta ?? profile.beta, api_base: oauthApiBase }) ?? profile.channel,
9346
+ // followAccountReleaseChannel then replaces it with the account's own
9347
+ // answer once the new grant can ask for it.
9348
+ release_channel: metadata.channel || channelFromApiBase(oauthApiBase) || profile.release_channel,
9352
9349
  oauth_api_base: oauthApiBase || profile.oauth_api_base,
9353
9350
  oauth_resource: metadata.resource,
9354
9351
  oauth_access_token: tokenResponse.access_token,
@@ -10009,12 +10006,14 @@ function confirmCommand(profileName, channel, apiBase) {
10009
10006
  return [
10010
10007
  cliCommandForChannel(channel),
10011
10008
  `--profile ${quoteShellArgument(profileName || "default")}`,
10012
- `--api-base ${quoteShellArgument(apiBase)}`,
10009
+ // Only an explicit route is repeated: without one, confirming must stay
10010
+ // free to follow the account to its release channel.
10011
+ ...apiBase ? [`--api-base ${quoteShellArgument(apiBase)}`] : [],
10013
10012
  "start"
10014
10013
  ].join(" ");
10015
10014
  }
10016
10015
  function authorizationChannel(metadata, runtime, pending = null) {
10017
- return metadata.channel || pending?.channel || channelFromProfile({ api_base: pending?.api_base || runtime.apiBase });
10016
+ return metadata.channel || normalizeReleaseChannel(pending?.channel) || channelFromProfile({ api_base: pending?.api_base || runtime.apiBase });
10018
10017
  }
10019
10018
  function updateRuntimeFromOAuthProfile(runtime, profile) {
10020
10019
  const oauthApiBase = getOAuthApiBase(profile);
@@ -10092,6 +10091,9 @@ async function redeemAuthorizationCode(runtime, code, fetchImpl) {
10092
10091
  }
10093
10092
  if (pending.api_base) {
10094
10093
  runtime.apiBase = pending.api_base;
10094
+ if (pending.api_base_explicit && !runtime.requestedApiBase) {
10095
+ runtime.requestedApiBase = pending.api_base;
10096
+ }
10095
10097
  }
10096
10098
  metadata = {
10097
10099
  apiBase: pending.api_base,
@@ -10100,7 +10102,7 @@ async function redeemAuthorizationCode(runtime, code, fetchImpl) {
10100
10102
  clientId: pending.client_id,
10101
10103
  tokenEndpoint: pending.token_endpoint,
10102
10104
  revocationEndpoint: pending.revocation_endpoint || `${String(pending.issuer || "").replace(/\/+$/, "")}/oauth/revoke`,
10103
- channel: pending.channel
10105
+ channel: normalizeReleaseChannel(pending.channel)
10104
10106
  };
10105
10107
  if (!metadata.issuer || !metadata.resource || !metadata.clientId || !metadata.tokenEndpoint) {
10106
10108
  throw oauthError(
@@ -10269,7 +10271,7 @@ async function loginWithOAuth(runtime, options = {}, output, fetchImpl = fetch,
10269
10271
  confirm_command: confirmCommand(
10270
10272
  runtime.profileName,
10271
10273
  authorizationChannel(metadata, runtime),
10272
- live.api_base
10274
+ runtime.requestedApiBase ? live.api_base : null
10273
10275
  )
10274
10276
  }
10275
10277
  };
@@ -10362,6 +10364,7 @@ async function loginWithOAuth(runtime, options = {}, output, fetchImpl = fetch,
10362
10364
  savePendingAuthorization(runtime, {
10363
10365
  profile: runtime.profileName,
10364
10366
  api_base: runtime.apiBase,
10367
+ api_base_explicit: Boolean(runtime.requestedApiBase),
10365
10368
  verifier,
10366
10369
  state,
10367
10370
  redirect_uri: redirectUri,
@@ -10431,7 +10434,7 @@ async function loginWithOAuth(runtime, options = {}, output, fetchImpl = fetch,
10431
10434
  confirm_command: confirmCommand(
10432
10435
  runtime.profileName,
10433
10436
  authorizationChannel(metadata, runtime),
10434
- runtime.apiBase
10437
+ runtime.requestedApiBase ? runtime.apiBase : null
10435
10438
  )
10436
10439
  }
10437
10440
  };
@@ -31992,7 +31995,8 @@ var UPSELL_CODES = /* @__PURE__ */ new Set([
31992
31995
  var NESTED_KEYS = ["error", "details", "data", "result", "response", "entitlement_access"];
31993
31996
  var PORTAL_ORIGINS = {
31994
31997
  "api.notis.ai": "https://app.notis.ai",
31995
- "api-beta.notis.ai": "https://beta.notis.ai"
31998
+ "api-nightly.notis.ai": "https://app-nightly.notis.ai",
31999
+ "api-beta.notis.ai": "https://app-nightly.notis.ai"
31996
32000
  };
31997
32001
  function isUpsellBody(value) {
31998
32002
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
@@ -32180,6 +32184,137 @@ init_oauth();
32180
32184
  import { randomUUID as randomUUID3 } from "node:crypto";
32181
32185
  import { createReadStream } from "node:fs";
32182
32186
  import { dirname as dirname5 } from "node:path";
32187
+
32188
+ // src/runtime/release-channel.js
32189
+ init_errors();
32190
+ init_auth_recovery();
32191
+ init_channel();
32192
+ init_profiles();
32193
+ var CHANNEL_LABELS = { stable: "Stable", nightly: "Nightly" };
32194
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
32195
+ var RELEASE_CHANNEL_TIMEOUT_MS = 1e4;
32196
+ function releaseChannelLabel(channel) {
32197
+ return CHANNEL_LABELS[channel] || channel;
32198
+ }
32199
+ function isLoopbackApiBase(apiBase) {
32200
+ try {
32201
+ return LOOPBACK_HOSTS.has(new URL(apiBase).hostname);
32202
+ } catch {
32203
+ return false;
32204
+ }
32205
+ }
32206
+ function trustedApiOrigin(value) {
32207
+ if (typeof value !== "string" || !value) return null;
32208
+ try {
32209
+ const url = new URL(value);
32210
+ const notisHost = url.hostname === "notis.ai" || url.hostname.endsWith(".notis.ai");
32211
+ return url.protocol === "https:" && notisHost && !url.username && !url.password ? url.origin : null;
32212
+ } catch {
32213
+ return null;
32214
+ }
32215
+ }
32216
+ function followsAccountReleaseChannel(runtime = {}, env = process.env) {
32217
+ if (runtime.requestedApiBase || env.NOTIS_API_BASE) return false;
32218
+ if (runtime.worktreeRuntime || runtime.credentialKind !== "oauth" || !runtime.jwt) return false;
32219
+ return Boolean(runtime.apiBase) && !isLoopbackApiBase(runtime.apiBase);
32220
+ }
32221
+ function parseReleaseChannelAnswer(payload) {
32222
+ if (!payload || typeof payload !== "object") return null;
32223
+ const releaseChannel = normalizeReleaseChannel(payload.release_channel);
32224
+ if (!releaseChannel) return null;
32225
+ const deploymentChannel = normalizeReleaseChannel(payload.deployment_channel);
32226
+ const origins = payload.origins && typeof payload.origins === "object" ? payload.origins : {};
32227
+ return {
32228
+ releaseChannel,
32229
+ deploymentChannel,
32230
+ match: !deploymentChannel || payload.match === true || deploymentChannel === releaseChannel,
32231
+ apiOrigin: trustedApiOrigin(origins.api)
32232
+ };
32233
+ }
32234
+ async function fetchAccountReleaseChannel(runtime, fetchImpl = fetch) {
32235
+ try {
32236
+ const response = await fetchImpl(`${String(runtime.apiBase).replace(/\/+$/, "")}/release_channel`, {
32237
+ method: "GET",
32238
+ headers: {
32239
+ Accept: "application/json",
32240
+ Authorization: `Bearer ${runtime.jwt}`,
32241
+ ...runtime.cliVersion ? { "X-Notis-CLI-Version": runtime.cliVersion } : {}
32242
+ },
32243
+ signal: AbortSignal.timeout(RELEASE_CHANNEL_TIMEOUT_MS)
32244
+ });
32245
+ if (!response.ok) return null;
32246
+ return parseReleaseChannelAnswer(await response.json());
32247
+ } catch {
32248
+ return null;
32249
+ }
32250
+ }
32251
+ function persistReleaseChannel(runtime, releaseChannel) {
32252
+ const config = updateConfig((latest) => {
32253
+ const profile = latest.profiles[runtime.profileName];
32254
+ if (profile) profile.release_channel = releaseChannel;
32255
+ return latest;
32256
+ });
32257
+ runtime.channel = releaseChannel;
32258
+ return config.profiles[runtime.profileName];
32259
+ }
32260
+ async function followAccountReleaseChannel(runtime, result, {
32261
+ login,
32262
+ options = {},
32263
+ output,
32264
+ fetchImpl = fetch,
32265
+ env = process.env,
32266
+ followed = false
32267
+ } = {}) {
32268
+ if (!result?.profile || !followsAccountReleaseChannel(runtime, env)) return result;
32269
+ const answer = await fetchAccountReleaseChannel(runtime, fetchImpl);
32270
+ if (!answer?.deploymentChannel) return result;
32271
+ if (answer.match) {
32272
+ return { ...result, profile: persistReleaseChannel(runtime, answer.releaseChannel) || result.profile };
32273
+ }
32274
+ const label = releaseChannelLabel(answer.releaseChannel);
32275
+ if (followed || !answer.apiOrigin || answer.apiOrigin === runtime.apiBase) {
32276
+ output?.notice?.(
32277
+ `Your account uses the ${label} channel, but this sign-in could not move there. Run: ${releaseChannelLoginCommand(runtime, answer)}`
32278
+ );
32279
+ return result;
32280
+ }
32281
+ output?.notice?.(`Your account uses the ${label} channel, signing you in there.`);
32282
+ runtime.apiBase = answer.apiOrigin;
32283
+ const { code: _code, reusePersistedCredential: _reuse, ...nextOptions } = options;
32284
+ const next = await login(runtime, nextOptions, output);
32285
+ if (!next?.profile) return next;
32286
+ return followAccountReleaseChannel(runtime, next, {
32287
+ login,
32288
+ options: nextOptions,
32289
+ output,
32290
+ fetchImpl,
32291
+ env,
32292
+ followed: true
32293
+ });
32294
+ }
32295
+ function releaseChannelLoginCommand(runtime = {}, answer = {}) {
32296
+ return [
32297
+ cliCommandForChannel(answer.releaseChannel),
32298
+ ...runtime.profileName && runtime.profileName !== "default" ? [`--profile ${quoteShellArgument(runtime.profileName)}`] : [],
32299
+ ...answer.apiOrigin ? [`--api-base ${quoteShellArgument(answer.apiOrigin)}`] : [],
32300
+ "login"
32301
+ ].join(" ");
32302
+ }
32303
+ function releaseChannelMismatchError(payload, runtime = {}) {
32304
+ const answer = parseReleaseChannelAnswer(payload) || {};
32305
+ const label = releaseChannelLabel(answer.releaseChannel);
32306
+ const message = typeof payload?.message === "string" && payload.message || (answer.releaseChannel ? `Your account uses the ${label} channel, which this Notis API does not serve.` : "Your account uses another Notis release channel than this API serves.");
32307
+ const command = releaseChannelLoginCommand(runtime, answer);
32308
+ return new CliError({
32309
+ code: "release_channel_mismatch",
32310
+ message: `${message} Sign in again with: ${command}`,
32311
+ exitCode: EXIT_CODES.auth,
32312
+ details: payload || {},
32313
+ hints: [{ command, reason: `Sign this profile in on the ${label || "right"} channel` }]
32314
+ });
32315
+ }
32316
+
32317
+ // src/runtime/transport.js
32183
32318
  init_retry();
32184
32319
  function escapeMultipartHeaderValue(value) {
32185
32320
  return String(value ?? "").replace(/"/g, "%22").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
@@ -32276,6 +32411,9 @@ function normalizeBackendError(status, payload, runtime) {
32276
32411
  details: payload || {}
32277
32412
  });
32278
32413
  }
32414
+ if (status === 403 && backendError === "release_channel_mismatch") {
32415
+ return releaseChannelMismatchError(payload, runtime);
32416
+ }
32279
32417
  if (status === 403) {
32280
32418
  const upsellBody = findUpsell(payload);
32281
32419
  if (upsellBody) {
@@ -32776,6 +32914,18 @@ function validateArguments(schema, args) {
32776
32914
  }
32777
32915
 
32778
32916
  // src/command-specs/tools.js
32917
+ var ROUTER_HELPERS = /* @__PURE__ */ new Set([
32918
+ COMPOSIO_SEARCH_TOOLS,
32919
+ COMPOSIO_GET_TOOL_SCHEMAS,
32920
+ COMPOSIO_MULTI_EXECUTE_TOOL,
32921
+ "COMPOSIO_MANAGE_CONNECTIONS",
32922
+ "COMPOSIO_WAIT_FOR_CONNECTIONS",
32923
+ "COMPOSIO_REMOTE_WORKBENCH",
32924
+ "COMPOSIO_REMOTE_BASH_TOOL"
32925
+ ]);
32926
+ function executionRequest(toolName, arguments_) {
32927
+ return ROUTER_HELPERS.has(toolName) ? { toolName, arguments_ } : { toolName: COMPOSIO_MULTI_EXECUTE_TOOL, arguments_: { tools: [{ tool_slug: toolName, arguments: arguments_ }] } };
32928
+ }
32779
32929
  async function resolveJsonInput(value, label) {
32780
32930
  if (!value) return void 0;
32781
32931
  if (value === "-") {
@@ -33159,10 +33309,7 @@ async function toolsExecHandler(ctx) {
33159
33309
  });
33160
33310
  const result = await runToolCommand({
33161
33311
  runtime: ctx.runtime,
33162
- toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
33163
- arguments_: {
33164
- tools: [{ tool_slug: requestedToolName, arguments: args }]
33165
- },
33312
+ ...executionRequest(requestedToolName, args),
33166
33313
  // The server is authoritative about provider/MCP effects. A tool name that
33167
33314
  // looks like a read is not proof of read-only behavior, so generic
33168
33315
  // executions always carry a key and use mutation-safe transport retries.
@@ -33245,8 +33392,7 @@ ${reason}`
33245
33392
  ctx.output.emitProgress({ phase: "execute", message: `Applying ${requestedToolName}` });
33246
33393
  const applied = await runToolCommand({
33247
33394
  runtime: ctx.runtime,
33248
- toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
33249
- arguments_: { tools: [{ tool_slug: requestedToolName, arguments: decision.arguments }] },
33395
+ ...executionRequest(requestedToolName, decision.arguments),
33250
33396
  mutating: true,
33251
33397
  idempotencyKey: confirmKey,
33252
33398
  fileBindings
@@ -33278,10 +33424,7 @@ async function toolsExecParallelHandler(ctx) {
33278
33424
  calls.map(
33279
33425
  (call) => runToolCommand({
33280
33426
  runtime: ctx.runtime,
33281
- toolName: COMPOSIO_MULTI_EXECUTE_TOOL,
33282
- arguments_: {
33283
- tools: [{ tool_slug: localNotisToolSlug(call.tool_name), arguments: call.arguments || {} }]
33284
- },
33427
+ ...executionRequest(localNotisToolSlug(call.tool_name), call.arguments || {}),
33285
33428
  mutating: true,
33286
33429
  idempotencyKey: nextIdempotencyKey(ctx.globalOptions)
33287
33430
  }).then((r) => ({
@@ -33545,7 +33688,7 @@ function doctorChannelSummary(runtime, moduleDirectory = dirname6(fileURLToPath2
33545
33688
  const decision = resolveChannelSwitch({
33546
33689
  runningVersion: runtime.cliVersion,
33547
33690
  profile: {
33548
- channel: runtime.channel,
33691
+ release_channel: runtime.channel,
33549
33692
  api_base: runtime.apiBase
33550
33693
  },
33551
33694
  moduleDirectory
@@ -34935,7 +35078,11 @@ var agentsCommandSpecs = [
34935
35078
 
34936
35079
  // src/command-specs/auth.js
34937
35080
  async function loginHandler(ctx) {
34938
- const result = await loginWithOAuth(ctx.runtime, ctx.options, ctx.output);
35081
+ const result = await followAccountReleaseChannel(
35082
+ ctx.runtime,
35083
+ await loginWithOAuth(ctx.runtime, ctx.options, ctx.output),
35084
+ { login: loginWithOAuth, options: ctx.options, output: ctx.output }
35085
+ );
34939
35086
  if (result.agentAuthorization) {
34940
35087
  const presentation = loginAgentAuthorizationPresentation(result.agentAuthorization);
34941
35088
  return ctx.output.emitSuccess({
@@ -35029,7 +35176,7 @@ var authCommandSpecs = [
35029
35176
  examples: [
35030
35177
  "notis login",
35031
35178
  "notis login --profile work",
35032
- "notis login --profile beta --api-base https://api-beta.notis.ai",
35179
+ "notis login --profile nightly --api-base https://api-nightly.notis.ai",
35033
35180
  "notis login --no-browser --print-url",
35034
35181
  "notis login --mode browser",
35035
35182
  "notis login --mode code",
@@ -35207,7 +35354,26 @@ async function startHandler(ctx) {
35207
35354
  } catch {
35208
35355
  }
35209
35356
  }
35357
+ const followChannel = (result) => followAccountReleaseChannel(runtime, result, {
35358
+ login: loginWithOAuth,
35359
+ options: { browser: true, mode: options.mode },
35360
+ output
35361
+ });
35362
+ const authorizationResult = (authorization2) => {
35363
+ const presentation = loginAgentAuthorizationPresentation(authorization2.agentAuthorization);
35364
+ return output.emitSuccess({
35365
+ command: "start",
35366
+ data: {
35367
+ authenticated: false,
35368
+ profile: runtime.profileName,
35369
+ ...authorization2.agentAuthorization
35370
+ },
35371
+ ...presentation
35372
+ });
35373
+ };
35210
35374
  if (isAuthenticated(runtime)) {
35375
+ const followed = await followChannel({ profile: getProfile(loadConfig(), runtime.profileName) });
35376
+ if (followed?.agentAuthorization) return authorizationResult(followed);
35211
35377
  return authenticatedResult(ctx);
35212
35378
  }
35213
35379
  if (options.briefOnly) {
@@ -35218,22 +35384,13 @@ async function startHandler(ctx) {
35218
35384
  hints: getAuthRecovery(runtime, { mode: "missing" }).hints
35219
35385
  });
35220
35386
  }
35221
- const authorization = await loginWithOAuth(
35387
+ const authorization = await followChannel(await loginWithOAuth(
35222
35388
  runtime,
35223
35389
  { browser: true, mode: options.mode, reusePersistedCredential: true },
35224
35390
  output
35225
- );
35391
+ ));
35226
35392
  if (authorization?.agentAuthorization) {
35227
- const presentation = loginAgentAuthorizationPresentation(authorization.agentAuthorization);
35228
- return output.emitSuccess({
35229
- command: "start",
35230
- data: {
35231
- authenticated: false,
35232
- profile: runtime.profileName,
35233
- ...authorization.agentAuthorization
35234
- },
35235
- ...presentation
35236
- });
35393
+ return authorizationResult(authorization);
35237
35394
  }
35238
35395
  if (!isAuthenticated(runtime)) {
35239
35396
  throw new CliError({
@@ -37466,6 +37623,7 @@ import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as rea
37466
37623
  import { homedir as homedir8 } from "node:os";
37467
37624
  import { dirname as dirname13, join as join12 } from "node:path";
37468
37625
  import { fileURLToPath as fileURLToPath6 } from "node:url";
37626
+ var LIVE_API_BASES = ["https://api.notis.ai", "https://api-nightly.notis.ai", "https://api-beta.notis.ai"];
37469
37627
  var SKILL_SYNC_SERVICE_LABEL = "ai.notis.skills-sync";
37470
37628
  var bundlePath = fileURLToPath6(new URL("../../dist/skill-sync-worker.mjs", import.meta.url));
37471
37629
  function atomicWrite2(target, value, mode = 384) {
@@ -37483,7 +37641,7 @@ async function maybeInstallSkillSyncService(runtime, {
37483
37641
  fetchSettings,
37484
37642
  refresh
37485
37643
  } = {}) {
37486
- if (process.platform !== "darwin" || runtime.credentialKind !== "oauth" || config.current_profile !== runtime.profileName || !["https://api.notis.ai", "https://api-beta.notis.ai"].includes(runtime.apiBase)) return;
37644
+ if (process.platform !== "darwin" || runtime.credentialKind !== "oauth" || config.current_profile !== runtime.profileName || !LIVE_API_BASES.includes(runtime.apiBase)) return;
37487
37645
  const plist = join12(homedir8(), "Library", "LaunchAgents", `${SKILL_SYNC_SERVICE_LABEL}.plist`);
37488
37646
  if (existsSync9(plist)) {
37489
37647
  const saved = readFileSync13(plist, "utf8");
@@ -37498,7 +37656,7 @@ async function maybeInstallSkillSyncService(runtime, {
37498
37656
  }
37499
37657
  async function installSkillSyncService(runtime, options = {}) {
37500
37658
  if ((options.platform || process.platform) !== "darwin") return { status: "unsupported_platform" };
37501
- if (runtime.credentialKind !== "oauth" || !runtime.oauthUserId || !["https://api.notis.ai", "https://api-beta.notis.ai"].includes(runtime.apiBase)) {
37659
+ if (runtime.credentialKind !== "oauth" || !runtime.oauthUserId || !LIVE_API_BASES.includes(runtime.apiBase)) {
37502
37660
  return { status: "skipped_non_personal_profile" };
37503
37661
  }
37504
37662
  return withSkillSyncLock(
@@ -37515,7 +37673,7 @@ async function installSkillSyncServiceLocked(runtime, {
37515
37673
  uid = process.getuid?.()
37516
37674
  } = {}) {
37517
37675
  if (platform !== "darwin") return { status: "unsupported_platform" };
37518
- if (runtime.credentialKind !== "oauth" || runtime.envCredentialOverride || !isValidProfileName(runtime.profileName) || !["https://api.notis.ai", "https://api-beta.notis.ai"].includes(runtime.apiBase)) {
37676
+ if (runtime.credentialKind !== "oauth" || runtime.envCredentialOverride || !isValidProfileName(runtime.profileName) || !LIVE_API_BASES.includes(runtime.apiBase)) {
37519
37677
  return { status: "skipped_non_personal_profile" };
37520
37678
  }
37521
37679
  const root = join12(home, ".notis", "skills", "service");
@@ -1 +1 @@
1
- {"schema":1,"stamp":1792843001,"commit":"974cae565cf52fde4d5a93fb0502c52b83e2abf9"}
1
+ {"schema":1,"stamp":1793114190,"commit":"a7010008e66c7091ebc496a82627bd42a1c5ead6"}
@@ -30,7 +30,7 @@ Use the registry-resolved published npm package everywhere:
30
30
 
31
31
  Always use this NPX command form so the agent runs the current published CLI, with one exception: on the Notis Cloud Computer (home folder `/vercel/sandbox`) the CLI is preinstalled and kept current, so run `notis ...` there with the same arguments, and use the NPX form only if `notis` is not found. Parallel NPX runs reinstall the package and can break each other. In hosted shells, the CLI is pre-authenticated through `NOTIS_JWT`. On a local machine the CLI holds its own OAuth grant: `notis login` authorizes one in the browser, and signing in to the Notis desktop app authorizes one automatically for that account. Either way the grant belongs to the CLI, which refreshes it without the desktop app running.
32
32
 
33
- `@latest` is correct for every account, including beta ones. Each deployment reports which published build belongs to it, `notis login` pins that on the profile, and a later run that finds itself on the wrong build hands the invocation to the right one before doing anything. Never substitute a channel by hand: pinning `@beta` on a production profile is how a machine ends up running a build its API does not expect. `notis doctor` reports the active channel, and `NOTIS_CLI_AUTO_CHANNEL=0` turns the hand-off off for a run.
33
+ `@latest` is correct for every account, including nightly ones. Each account reports the release channel it belongs to, `notis login` pins that on the profile, and a later run that finds itself on the wrong build hands the invocation to the right one before doing anything. Never substitute a channel by hand: pinning `@nightly` on a stable profile is how a machine ends up running a build its API does not expect. `notis doctor` reports the active channel, and `NOTIS_CLI_AUTO_CHANNEL=0` turns the hand-off off for a run.
34
34
 
35
35
  The CLI bundles this `notis-cli` base skill and refreshes its canonical copy under `~/.notis/skills/base/` on every launch. It is independent of account-skill sync, feature flags, target selection, and cloud deletion.
36
36
 
@@ -38,7 +38,7 @@ The CLI bundles this `notis-cli` base skill and refreshes its canonical copy und
38
38
 
39
39
  A profile is one account paired with one API endpoint. Profiles live side by side; switching between them never signs any of them out.
40
40
 
41
- Notis Desktop signs the CLI in for every account saved in the app, one profile each. `default` keeps its name; each added account gets a profile named after its email with `@` turned into `-` (for example `flo-mindtheflo.com`), and Beta Desktop adds `-beta`. Switching accounts in Desktop never changes the active profile. Only the active profile's skills are linked into Claude Code, Codex and Cursor; every other account's skills sync into their own folder. Signing an account out of Desktop signs it out of the CLI on this computer and removes the profile and skills Desktop made for it. If that was the active profile, the CLI switches to the account Desktop opens next; with no other signed-in Desktop account it stays signed out. Profiles made with `notis login` are never removed or picked automatically.
41
+ Notis Desktop signs the CLI in for every account saved in the app, one profile each. `default` keeps its name; each added account gets a profile named after its email with `@` turned into `-` (for example `flo-mindtheflo.com`), and a nightly account adds `-nightly`. Switching accounts in Desktop never changes the active profile. Only the active profile's skills are linked into Claude Code, Codex and Cursor; every other account's skills sync into their own folder. Signing an account out of Desktop signs it out of the CLI on this computer and removes the profile and skills Desktop made for it. If that was the active profile, the CLI switches to the account Desktop opens next; with no other signed-in Desktop account it stays signed out. Profiles made with `notis login` are never removed or picked automatically.
42
42
 
43
43
  - `npx --package @notis_ai/cli@latest -- notis profile list`: every profile on this machine, with its endpoint, user, and whether it is signed in. The active one is marked.
44
44
  - `npx --package @notis_ai/cli@latest -- notis profile use <name>`: change which account subsequent commands run as, and link that account's skills into the agent folders straight away.