@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 +1 -1
- package/dist/agent-hooks/notis-agent-hook.mjs +244 -86
- package/dist/base-skills/notis-cli/.notis-base-skill.json +1 -1
- package/dist/base-skills/notis-cli/SKILL.md +2 -2
- package/dist/skill-sync-worker.mjs +39 -46
- package/package.json +3 -3
- package/src/cli.js +4 -3
- package/src/command-specs/auth.js +7 -2
- package/src/command-specs/meta.js +1 -1
- package/src/command-specs/onboarding.js +26 -12
- package/src/command-specs/tools.js +20 -10
- package/src/runtime/channel.js +70 -30
- package/src/runtime/oauth.js +23 -23
- package/src/runtime/profiles.js +14 -28
- package/src/runtime/release-channel.js +184 -0
- package/src/runtime/skill-sync-service.js +6 -3
- package/src/runtime/transport.js +7 -0
- package/src/runtime/upsell.js +2 -1
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
|
|
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("-") ? "
|
|
7456
|
+
return String(version || "").includes("-") ? "nightly" : "stable";
|
|
7445
7457
|
}
|
|
7446
|
-
function
|
|
7447
|
-
if (
|
|
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
|
-
|
|
7454
|
-
|
|
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]
|
|
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", "
|
|
7509
|
-
CHANNEL_TAGS = { stable: "latest",
|
|
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
|
-
|
|
7767
|
-
//
|
|
7768
|
-
//
|
|
7769
|
-
|
|
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
|
-
|
|
8052
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 || !
|
|
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 || !
|
|
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) || !
|
|
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":
|
|
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
|
|
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
|
|
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.
|