neon 8.3.1 → 8.3.2

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
@@ -839,7 +839,9 @@ The target directory must be empty unless you pass `--force` (a lone `.git` is i
839
839
 
840
840
  Recommended installs the Neon plugin for every detected agent (global config, project folders, and the host CLI). Plugin-capable agents get the plugin; the rest get skills and MCP. If none are detected, it installs the default Neon skills for Cursor and Codex in this directory (`./.agents/skills`) and does not configure MCP. It then links a project when the CLI is authenticated, and writes a default `neon.ts` (Postgres only) using the package manager already in the directory.
841
841
 
842
- Unauthenticated `-y` skips linking and prints the next step: sign up at https://neon.com/signup, then `neon login`, `neon link`, or `neon claim create` (no account, expires in 72 hours unless claimed). `-y` never opens a browser. `-y` with `--project-id` or other account flags links when the CLI is already signed in; otherwise it errors and names `neon login` or `--claimable`.
842
+ Unauthenticated `-y` skips linking and prints the next step: sign up at https://neon.com/signup, then `neon login`, `neon link`, or `neon claim create` (no account, expires in 72 hours unless claimed). `-y` never opens a browser. `-y` with `--project-id` or other account flags links when the CLI is already signed in. Without credentials, `--org-id`, `--project-id`, and `--branch` together write `.neon` offline (as `neon link --no-checks` does), and init prints `neon login` as the next step; Neon checks the IDs on the first command that reaches it. With only some of those flags, init still installs agent tooling and writes `neon.ts`, then prints `neon login` and the `neon link` command with those flags.
843
+
844
+ When init picks MCP auth (no `--mcp-auth`) and the CLI credential is an organization or project-scoped API key, which cannot mint the MCP server's key, init configures MCP with OAuth instead and continues. With `--mcp-auth api-key`, init skips MCP, finishes linking and `neon.ts`, then reports `Neon setup failed.` with the `neon mcp` commands to retry and exits 1.
843
845
 
844
846
  Custom asks how to add Neon to coding agents (plugin, skills and MCP separately, or skip), then how to get a project (sign in and link, or a claimable project when you are not signed in), then which services `neon.ts` should declare. Skills and MCP use separate agent pickers, and either can be skipped. Postgres is listed first as always included. The package manager is inferred from the directory; Custom asks only when none is detected. `--skill` selects skills (not the plugin) and skips that picker. MCP flags (`--mcp-config-location`, `--mcp-auth`, `--mcp-project-scoped`) select skills and MCP. `--mcp-config-location` is where the config is written: `global` maps to `neon mcp`, and `project` maps to `neon mcp --project`. With `-y`, the location defaults to `global`; interactive Custom asks. If some selected agents cannot write that location, init warns and skips MCP for those agents while continuing with the supported agents. It fails when none of the selected agents support the location. `--mcp-project-scoped` limits MCP tools to the linked Neon project by passing its ID to `neon mcp --project-id`. Omitted is false, including in Custom; there is no prompt. `--no-agent-setup` skips agent setup.
845
847
 
package/dist/init/copy.js CHANGED
@@ -51,7 +51,7 @@ const NO_AGENTS_FALLBACK_STATUS = "No coding agents detected. Installing the def
51
51
  const NO_AGENT_SETUP_CONFLICT = "--no-agent-setup cannot be combined with --agent, --skill, or MCP flags.";
52
52
  const INIT_NEEDS_YES = "No interactive terminal. Re-run with -y to set up this directory with Recommended defaults: `neon init -y`. To pick each step instead, run `neon init` in a terminal, or see `neon init --help` for the flag that sets each step.";
53
53
  const NON_TTY_LINK_NEEDS_AUTH = "No interactive terminal. Sign in with `neon login`, then re-run, or pass --claimable.";
54
- const YES_LINK_NEEDS_AUTH = "-y cannot sign in. Sign in with `neon login`, then re-run, or pass --claimable.";
54
+ const MCP_OAUTH_FALLBACK = "This CLI credential cannot mint an API key for the Neon MCP server. Configuring it with OAuth instead.";
55
55
  const MCP_SCOPED_NEEDS_PROJECT = "--mcp-project-scoped requires a linked project. Link a project first, or omit --mcp-project-scoped.";
56
56
  const namedAgentsUnavailable = (ids) => `--agent ${ids.join(", ")} cannot install this tooling. Pass agents that support it, or omit --agent.`;
57
57
  const mcpConfigLocationUnavailable = (ids, location) => location === "project" ? `--agent ${ids.join(", ")} cannot install project-level MCP config. Use --mcp-config-location global, or choose an agent that supports project config.` : `--agent ${ids.join(", ")} cannot install global MCP config. Choose an agent that supports global config.`;
@@ -78,7 +78,8 @@ const PROGRESS = {
78
78
  install: (pm) => `Installing Neon dependencies with ${pm}...`,
79
79
  env: "Pulling Neon environment variables..."
80
80
  };
81
- const unattendedUnauthedNext = () => [
81
+ /** `deferred` carries the account and credential flags init was given. */
82
+ const unattendedUnauthedNext = (deferred) => deferred === void 0 ? [
82
83
  "Link a project with a Neon account and an authenticated CLI.",
83
84
  "Sign up: https://neon.com/signup",
84
85
  `${getCliName()} login`,
@@ -86,6 +87,16 @@ const unattendedUnauthedNext = () => [
86
87
  "",
87
88
  "Or create a claimable project without an account. It expires in 72 hours unless claimed.",
88
89
  `${getCliName()} claim create`
90
+ ] : [
91
+ "Link the project once the CLI is authenticated.",
92
+ deferred.login,
93
+ deferred.link
94
+ ];
95
+ const PROJECT_PINNED_OFFLINE = "linked, not verified";
96
+ const offlinePinNext = (input) => [
97
+ "The link was written without contacting Neon. Sign in to use it:",
98
+ input.login,
99
+ ...input.envPull !== void 0 ? ["Then pull the branch's environment variables:", input.envPull] : []
89
100
  ];
90
101
  const skippedLinkNext = () => ["To connect this directory to a Neon project:", `${getCliName()} link`];
91
102
  const extraServicesNext = () => [
@@ -122,6 +133,17 @@ const installFailedNext = (command) => ({
122
133
  body: "Could not install the Neon dependencies.",
123
134
  next: ["Install the dependencies, then run neon init again:", command]
124
135
  });
136
+ const MCP_API_KEY_NEEDS_AUTH = "API-key MCP setup needs a Neon credential to mint the key. Sign in with `neon login` or pass a personal API key.";
137
+ /** `mcp` is the `neon mcp` invocation without an auth flag. */
138
+ const mcpKeyFailedNext = (reason, mcp) => [
139
+ reason === "cannot-mint" ? "The Neon MCP server was not configured: this CLI credential cannot mint its API key." : "The Neon MCP server was not configured: minting its API key needs a signed-in CLI.",
140
+ "Configure it with a personal API key:",
141
+ `${mcp} --api-key <personal-api-key>`,
142
+ "",
143
+ "Or with OAuth:",
144
+ `${mcp} --oauth`
145
+ ];
146
+ const mcpAfterLinkNext = (mcp) => ["Then configure the Neon MCP server for that project:", mcp];
125
147
  const envPullFailedNext = () => ({
126
148
  heading: HEADING_FAILED,
127
149
  body: "The project is linked, but its environment variables could not be pulled.",
@@ -137,4 +159,4 @@ const headingForOutcome = (outcome) => {
137
159
  }
138
160
  };
139
161
  //#endregion
140
- export { AGENT_DETECTED_DESCRIPTION, AGENT_MCP_PICKER_MESSAGE, AGENT_MCP_SKIP_MESSAGE, AGENT_OTHER_DESCRIPTION, AGENT_PICKER_MESSAGE, AGENT_PLUGIN_SKIP_MESSAGE, AGENT_SETUP_MESSAGE, AGENT_SETUP_PLUGIN_DESCRIPTION, AGENT_SETUP_PLUGIN_TITLE, AGENT_SETUP_SKILLS_DESCRIPTION, AGENT_SETUP_SKILLS_TITLE, AGENT_SETUP_SKIP_DESCRIPTION, AGENT_SETUP_SKIP_TITLE, AGENT_SKILLS_PICKER_MESSAGE, AGENT_SKILLS_SKIP_MESSAGE, CANCELLED_BODY, CLAIMABLE_ACCOUNT_FLAGS, CLAIMABLE_ALREADY_LINKED, CLAIMABLE_MCP_API_KEY, CLAIMABLE_NO_LINK, CONFIG_CONFIRM_HINT, CONFIG_CONFIRM_MESSAGE, HEADING_CANCELLED, HEADING_COMPLETE, HEADING_FAILED, HEADING_PENDING, INIT_NEEDS_YES, INIT_SUBTITLE, MCP_AUTH_API_KEY_DESCRIPTION, MCP_AUTH_API_KEY_TITLE, MCP_AUTH_MESSAGE, MCP_AUTH_OAUTH_DESCRIPTION, MCP_AUTH_OAUTH_TITLE, MCP_SCOPED_NEEDS_PROJECT, MCP_SCOPE_GLOBAL_DESCRIPTION, MCP_SCOPE_GLOBAL_TITLE, MCP_SCOPE_MESSAGE, MCP_SCOPE_PROJECT_DESCRIPTION, MCP_SCOPE_PROJECT_TITLE, MODE_CUSTOM_DESCRIPTION, MODE_CUSTOM_TITLE, MODE_MESSAGE, MODE_RECOMMENDED_DESCRIPTION, MODE_RECOMMENDED_TITLE, NON_TTY_AGENT_SETUP, NON_TTY_LINK_NEEDS_AUTH, NO_AGENTS_FALLBACK_STATUS, NO_AGENT_SETUP_CONFLICT, PACKAGE_MANAGER_MESSAGE, PROGRESS, PROJECT_SETUP_CLAIMABLE_DESCRIPTION, PROJECT_SETUP_CLAIMABLE_TITLE, PROJECT_SETUP_LINK_DESCRIPTION, PROJECT_SETUP_LINK_TITLE, PROJECT_SETUP_MESSAGE, SERVICES_HINT, SERVICES_MESSAGE, SKILLS_PICKER_MESSAGE, YES_LINK_NEEDS_AUTH, agentMissingCommandDescription, claimableNext, claimableThenServicesNext, envPullFailedNext, existingConfigNext, extraServicesNext, headingForOutcome, installFailedNext, mcpConfigLocationSkipped, mcpConfigLocationUnavailable, namedAgentsUnavailable, skippedLinkNext, unattendedUnauthedNext };
162
+ export { AGENT_DETECTED_DESCRIPTION, AGENT_MCP_PICKER_MESSAGE, AGENT_MCP_SKIP_MESSAGE, AGENT_OTHER_DESCRIPTION, AGENT_PICKER_MESSAGE, AGENT_PLUGIN_SKIP_MESSAGE, AGENT_SETUP_MESSAGE, AGENT_SETUP_PLUGIN_DESCRIPTION, AGENT_SETUP_PLUGIN_TITLE, AGENT_SETUP_SKILLS_DESCRIPTION, AGENT_SETUP_SKILLS_TITLE, AGENT_SETUP_SKIP_DESCRIPTION, AGENT_SETUP_SKIP_TITLE, AGENT_SKILLS_PICKER_MESSAGE, AGENT_SKILLS_SKIP_MESSAGE, CANCELLED_BODY, CLAIMABLE_ACCOUNT_FLAGS, CLAIMABLE_ALREADY_LINKED, CLAIMABLE_MCP_API_KEY, CLAIMABLE_NO_LINK, CONFIG_CONFIRM_HINT, CONFIG_CONFIRM_MESSAGE, HEADING_CANCELLED, HEADING_COMPLETE, HEADING_FAILED, HEADING_PENDING, INIT_NEEDS_YES, INIT_SUBTITLE, MCP_API_KEY_NEEDS_AUTH, MCP_AUTH_API_KEY_DESCRIPTION, MCP_AUTH_API_KEY_TITLE, MCP_AUTH_MESSAGE, MCP_AUTH_OAUTH_DESCRIPTION, MCP_AUTH_OAUTH_TITLE, MCP_OAUTH_FALLBACK, MCP_SCOPED_NEEDS_PROJECT, MCP_SCOPE_GLOBAL_DESCRIPTION, MCP_SCOPE_GLOBAL_TITLE, MCP_SCOPE_MESSAGE, MCP_SCOPE_PROJECT_DESCRIPTION, MCP_SCOPE_PROJECT_TITLE, MODE_CUSTOM_DESCRIPTION, MODE_CUSTOM_TITLE, MODE_MESSAGE, MODE_RECOMMENDED_DESCRIPTION, MODE_RECOMMENDED_TITLE, NON_TTY_AGENT_SETUP, NON_TTY_LINK_NEEDS_AUTH, NO_AGENTS_FALLBACK_STATUS, NO_AGENT_SETUP_CONFLICT, PACKAGE_MANAGER_MESSAGE, PROGRESS, PROJECT_PINNED_OFFLINE, PROJECT_SETUP_CLAIMABLE_DESCRIPTION, PROJECT_SETUP_CLAIMABLE_TITLE, PROJECT_SETUP_LINK_DESCRIPTION, PROJECT_SETUP_LINK_TITLE, PROJECT_SETUP_MESSAGE, SERVICES_HINT, SERVICES_MESSAGE, SKILLS_PICKER_MESSAGE, agentMissingCommandDescription, claimableNext, claimableThenServicesNext, envPullFailedNext, existingConfigNext, extraServicesNext, headingForOutcome, installFailedNext, mcpAfterLinkNext, mcpConfigLocationSkipped, mcpConfigLocationUnavailable, mcpKeyFailedNext, namedAgentsUnavailable, offlinePinNext, skippedLinkNext, unattendedUnauthedNext };
package/dist/init/run.js CHANGED
@@ -15,10 +15,12 @@ import { missingPluginsCommands, pluginsInstallableAgents } from "../plugins/tar
15
15
  import { skillsInstallableAgents } from "../skills/targets.js";
16
16
  import { FALLBACK_SKILLS_AGENTS, assertNamedAgentTooling, funnelAgentSetup, planInitToolingSteps, recommendedTooling, resolveNamedAgents, splitInitTooling } from "./plan.js";
17
17
  import { inferPackageManager, resolvePackageManager } from "../utils/package_manager.js";
18
- import { CANCELLED_BODY, CLAIMABLE_ACCOUNT_FLAGS, CLAIMABLE_ALREADY_LINKED, CLAIMABLE_MCP_API_KEY, CLAIMABLE_NO_LINK, MCP_SCOPED_NEEDS_PROJECT, NON_TTY_LINK_NEEDS_AUTH, NO_AGENTS_FALLBACK_STATUS, PROGRESS, YES_LINK_NEEDS_AUTH, claimableNext, claimableThenServicesNext, envPullFailedNext, existingConfigNext, extraServicesNext, installFailedNext, mcpConfigLocationSkipped, mcpConfigLocationUnavailable, namedAgentsUnavailable, skippedLinkNext, unattendedUnauthedNext } from "./copy.js";
18
+ import { CANCELLED_BODY, CLAIMABLE_ACCOUNT_FLAGS, CLAIMABLE_ALREADY_LINKED, CLAIMABLE_MCP_API_KEY, CLAIMABLE_NO_LINK, MCP_API_KEY_NEEDS_AUTH, MCP_SCOPED_NEEDS_PROJECT, NON_TTY_LINK_NEEDS_AUTH, NO_AGENTS_FALLBACK_STATUS, PROGRESS, PROJECT_PINNED_OFFLINE, claimableNext, claimableThenServicesNext, envPullFailedNext, existingConfigNext, extraServicesNext, installFailedNext, mcpAfterLinkNext, mcpConfigLocationSkipped, mcpConfigLocationUnavailable, mcpKeyFailedNext, namedAgentsUnavailable, offlinePinNext, skippedLinkNext, unattendedUnauthedNext } from "./copy.js";
19
19
  import { agentsRowValue, configRowValue, formatInitDone, headingForKind, printInitBanner, printInitDone, printInitProgress, projectRowValue, shouldPrintInitBanner } from "./chrome.js";
20
20
  import { ConfigInstallFailed, initCmd } from "../commands/config.js";
21
+ import { quoteFlagValue, runLink } from "../commands/link.js";
21
22
  import { runAuthenticatedLink } from "./link.js";
23
+ import { CannotMintApiKeyError } from "../mcp/mint.js";
22
24
  import { pullInitEnv } from "./auth.js";
23
25
  import { InitCancelled, raceSigint, restoreCursor } from "./cancelled.js";
24
26
  import { pickAgentSetupInteractively, pickInitAgentsInteractively, pickInitConfigInteractively, pickInitMcpAuthInteractively, pickInitMcpConfigLocationInteractively, pickInitModeInteractively, pickInitPackageManagerInteractively, pickInitProjectSetupInteractively, pickInitServicesInteractively, pickInitSkillsInteractively } from "./wizard.js";
@@ -34,6 +36,24 @@ const isLinked = (contextFile) => {
34
36
  const projectId = readContextFile(contextFile).projectId;
35
37
  return typeof projectId === "string" && projectId.length > 0;
36
38
  };
39
+ const commandLine = (command, pairs) => [`${getCliName()} ${command}`, ...pairs.flatMap(([flag, value]) => value === void 0 ? [] : [`${flag} ${quoteFlagValue(value)}`])].join(" ");
40
+ const deferredLinkCommands = (props, inputs) => {
41
+ const session = [["--profile", props.profile], ["--config-dir", props.configDir === defaultDir ? void 0 : props.configDir]];
42
+ const context = [["--context-file", props.contextFile === ".neon" ? void 0 : props.contextFile]];
43
+ return {
44
+ login: commandLine("login", session),
45
+ link: commandLine("link", [
46
+ ["--org-id", inputs.orgId],
47
+ ["--project-id", inputs.projectId],
48
+ ["--project-name", inputs.projectName],
49
+ ["--region-id", inputs.regionId],
50
+ ["--branch", inputs.branch],
51
+ ...context,
52
+ ...session
53
+ ]),
54
+ envPull: commandLine("env pull", [...context, ...session])
55
+ };
56
+ };
37
57
  const expandTelemetryServices = (services) => {
38
58
  if (services.includes("data-api") && !services.includes("auth")) return CONFIG_INIT_SERVICES.filter((service) => service === "auth" || services.includes(service));
39
59
  return [...services];
@@ -231,6 +251,7 @@ const runInit = async (props) => {
231
251
  const pluginReady = (agents) => agents.filter((id) => !unavailablePluginAgents.includes(id));
232
252
  let tooling = { setup: "skip" };
233
253
  let mcpAuth = props.mcpAuth;
254
+ let mcpAuthChosen = props.mcpAuth !== void 0;
234
255
  let mcpConfigLocation = props.mcpConfigLocation ?? "global";
235
256
  let selectedSkills = props.skill;
236
257
  let delayMcp = false;
@@ -293,7 +314,9 @@ const runInit = async (props) => {
293
314
  mcpConfigLocation = props.mcpConfigLocation ?? (yes ? "global" : props.pickMcpConfigLocation !== void 0 ? await props.pickMcpConfigLocation() : detection.interactive ? await pickInitMcpConfigLocationInteractively() : "global");
294
315
  if (mcpSelection.length > 0) validateMcpConfigLocationSupport(mcpSelection, mcpConfigLocation);
295
316
  const pickAuth = props.pickMcpAuth ?? (detection.interactive ? pickInitMcpAuthInteractively : void 0);
296
- mcpAuth = props.mcpAuth ?? (yes ? void 0 : await pickAuth?.({ authenticated: detection.authenticated })) ?? (detection.authenticated && props.claimable !== true ? "api-key" : "oauth");
317
+ const chosenAuth = props.mcpAuth ?? (yes ? void 0 : await pickAuth?.({ authenticated: detection.authenticated }));
318
+ mcpAuthChosen = chosenAuth !== void 0;
319
+ mcpAuth = chosenAuth ?? (detection.authenticated && props.claimable !== true ? "api-key" : "oauth");
297
320
  }
298
321
  tooling = skillsMcpTooling(skillsSelection, mcpSelection, mcpConfigLocation, namedFallback);
299
322
  }
@@ -305,16 +328,36 @@ const runInit = async (props) => {
305
328
  if (mcpAuth === void 0 && usesMcp && (!detection.authenticated || props.claimable === true)) mcpAuth = "oauth";
306
329
  delayMcp = usesMcp && (mcpAuth === "api-key" || props.mcpProjectScoped === true);
307
330
  const mcpOauth = mcpAuth === "oauth";
308
- const earlyTooling = delayMcp ? tooling.setup === "skills-mcp" ? {
309
- setup: "skills-mcp",
310
- skillsAgents: tooling.skillsAgents,
311
- mcpAgents: []
312
- } : tooling.setup === "mixed" ? {
313
- setup: "mixed",
314
- pluginAgents: tooling.pluginAgents,
315
- skillsAgents: tooling.skillsAgents,
316
- mcpAgents: []
317
- } : tooling : tooling;
331
+ const earlyTooling = delayMcp ? withoutMcp(tooling) : tooling;
332
+ const mcpState = {};
333
+ const reportedTooling = () => mcpState.error === void 0 && mcpState.deferredPin === void 0 ? tooling : withoutMcp(tooling);
334
+ const mcpCommand = (projectIdArg) => [
335
+ `${getCliName()} mcp -y`,
336
+ ...mcpConfigLocation === "project" ? ["--project"] : [],
337
+ ...projectIdArg !== void 0 ? [`--project-id ${projectIdArg}`] : [],
338
+ ...("mcpAgents" in tooling ? tooling.mcpAgents : []).map((agent) => `--agent ${agent}`)
339
+ ].join(" ");
340
+ const mcpFailedNext = () => mcpState.error === void 0 ? [] : mcpKeyFailedNext(mcpState.reason ?? "cannot-mint", mcpCommand(mcpState.pinId !== void 0 ? quoteFlagValue(mcpState.pinId) : void 0));
341
+ const mcpDeferredNext = () => mcpState.deferredPin === void 0 ? [] : mcpAfterLinkNext(`${mcpCommand(mcpState.deferredPin)}${mcpOauth ? " --oauth" : ""}`);
342
+ const runTooling = async (steps, plan) => {
343
+ let ran = plan;
344
+ try {
345
+ await runToolingSteps(steps, {
346
+ cwd,
347
+ output: props.output,
348
+ auth,
349
+ operations: props.operations,
350
+ narrate: "human",
351
+ mcpOauthFallback: !mcpAuthChosen
352
+ });
353
+ } catch (error) {
354
+ if (!(error instanceof CannotMintApiKeyError)) throw error;
355
+ mcpState.error = error;
356
+ mcpState.reason = "cannot-mint";
357
+ ran = withoutMcp(plan);
358
+ }
359
+ funnel.agentsInstalled = uniqueAgents([...funnel.agentsInstalled, ...agentsFromTooling(ran)]);
360
+ };
318
361
  const toolingSteps = planInitToolingSteps({
319
362
  tooling: earlyTooling,
320
363
  yes: true,
@@ -324,20 +367,12 @@ const runInit = async (props) => {
324
367
  mcpProject: mcpConfigLocation === "project",
325
368
  ...selectedSkills !== void 0 ? { skills: selectedSkills } : {}
326
369
  });
327
- if (toolingSteps.length > 0) {
328
- await runToolingSteps(toolingSteps, {
329
- cwd,
330
- output: props.output,
331
- auth,
332
- operations: props.operations,
333
- narrate: "human"
334
- });
335
- funnel.agentsInstalled = agentsFromTooling(earlyTooling);
336
- }
370
+ if (toolingSteps.length > 0) await runTooling(toolingSteps, earlyTooling);
337
371
  let projectSetup = "skip";
338
372
  let linkedNow = false;
339
373
  let claimExpiresAt;
340
374
  const noLink = props.link === false;
375
+ const linkByDefault = detection.authenticated || hasExplicitLinkInputs && !yes;
341
376
  if (noLink) {
342
377
  funnel.link = alreadyLinked ? "already_linked" : "skipped";
343
378
  projectSetup = "skip";
@@ -346,19 +381,39 @@ const runInit = async (props) => {
346
381
  projectSetup = "already";
347
382
  if (claimableExisting) claimExpiresAt = readLinkedClaimableCredentials(configDir, readContextFile(contextFile))?.expiresAt;
348
383
  } else if (recommended) {
349
- if (hasExplicitLinkInputs || detection.authenticated) projectSetup = "link";
384
+ if (linkByDefault) projectSetup = "link";
350
385
  else if (detection.interactive && !yes) projectSetup = "link";
351
386
  else {
352
387
  projectSetup = "skip";
353
388
  funnel.link = "skipped";
354
389
  }
355
390
  } else if (props.claimable === true) projectSetup = "claimable";
356
- else if (hasExplicitLinkInputs || detection.authenticated) projectSetup = "link";
391
+ else if (linkByDefault) projectSetup = "link";
357
392
  else if (yes) {
358
393
  projectSetup = "skip";
359
394
  funnel.link = "skipped";
360
395
  } else if (props.pickProjectSetup !== void 0 || detection.interactive) projectSetup = await (props.pickProjectSetup ?? pickInitProjectSetupInteractively)();
361
396
  else throw new Error(NON_TTY_LINK_NEEDS_AUTH);
397
+ const linkProps = {
398
+ apiClient: props.apiClient,
399
+ apiKey: props.apiKey,
400
+ apiHost: props.apiHost,
401
+ output: props.output,
402
+ contextFile,
403
+ yes,
404
+ clear: false,
405
+ checks: true,
406
+ envPull: true,
407
+ config: false,
408
+ cwd,
409
+ ...linkInputs,
410
+ ...props.configDir ? { configDir: props.configDir } : {},
411
+ ...props.profile ? { profile: props.profile } : {},
412
+ ...props.oauthHost ? { oauthHost: props.oauthHost } : {},
413
+ ...props.clientId ? { clientId: props.clientId } : {},
414
+ ...props.forceAuth !== void 0 ? { forceAuth: props.forceAuth } : {},
415
+ ...props.allowUnsafeTls !== void 0 ? { allowUnsafeTls: props.allowUnsafeTls } : {}
416
+ };
362
417
  if (usesMcp && mcpAuth === "api-key" && (projectSetup === "claimable" || props.claimable === true)) throw new Error(CLAIMABLE_MCP_API_KEY);
363
418
  if (projectSetup === "claimable") {
364
419
  printInitProgress(PROGRESS.claim);
@@ -374,32 +429,28 @@ const runInit = async (props) => {
374
429
  funnel.link = "claimable";
375
430
  claimExpiresAt = readLinkedClaimableCredentials(configDir, readContextFile(contextFile))?.expiresAt;
376
431
  } else if (projectSetup === "link") {
377
- if (yes && !detection.authenticated) throw new Error(YES_LINK_NEEDS_AUTH);
378
432
  printInitProgress(detection.authenticated ? PROGRESS.link : PROGRESS.auth);
379
- await (props.linkProject ?? runAuthenticatedLink)({
380
- apiClient: props.apiClient,
381
- apiKey: props.apiKey,
382
- apiHost: props.apiHost,
383
- output: props.output,
384
- contextFile,
385
- yes,
386
- clear: false,
387
- checks: true,
388
- envPull: true,
389
- config: false,
390
- cwd,
391
- ...linkInputs,
392
- ...props.configDir ? { configDir: props.configDir } : {},
393
- ...props.profile ? { profile: props.profile } : {},
394
- ...props.oauthHost ? { oauthHost: props.oauthHost } : {},
395
- ...props.clientId ? { clientId: props.clientId } : {},
396
- ...props.forceAuth !== void 0 ? { forceAuth: props.forceAuth } : {},
397
- ...props.allowUnsafeTls !== void 0 ? { allowUnsafeTls: props.allowUnsafeTls } : {}
433
+ await (props.linkProject ?? runAuthenticatedLink)(linkProps);
434
+ linkedNow = true;
435
+ funnel.link = "linked";
436
+ }
437
+ const pinnedOffline = funnel.link === "skipped" && !noLink && linkInputs.orgId !== void 0 && linkInputs.projectId !== void 0 && linkInputs.branch !== void 0 && linkInputs.projectName === void 0 && linkInputs.regionId === void 0;
438
+ if (pinnedOffline) {
439
+ await runLink({
440
+ ...linkProps,
441
+ checks: false,
442
+ envPull: false
398
443
  });
399
444
  linkedNow = true;
400
445
  funnel.link = "linked";
401
446
  }
402
- if (delayMcp && tooling.setup !== "skip") {
447
+ if (delayMcp && tooling.setup !== "skip" && props.mcpProjectScoped === true && funnel.link === "skipped" && !noLink) mcpState.deferredPin = linkInputs.projectId !== void 0 ? quoteFlagValue(linkInputs.projectId) : "<project-id>";
448
+ else if (delayMcp && tooling.setup !== "skip" && mcpAuth === "api-key" && yes && !detection.authenticated) {
449
+ mcpState.error = new Error(MCP_API_KEY_NEEDS_AUTH);
450
+ mcpState.reason = "signed-out";
451
+ const linkedId = readContextFile(contextFile).projectId;
452
+ if (props.mcpProjectScoped === true && typeof linkedId === "string") mcpState.pinId = linkedId;
453
+ } else if (delayMcp && tooling.setup !== "skip") {
403
454
  const linkedId = readContextFile(contextFile).projectId;
404
455
  let pinId;
405
456
  if (props.mcpProjectScoped === true) {
@@ -424,16 +475,8 @@ const runInit = async (props) => {
424
475
  mcpProject: mcpConfigLocation === "project",
425
476
  ...pinId !== void 0 ? { mcpProjectId: pinId } : {}
426
477
  });
427
- if (mcpSteps.length > 0) {
428
- await runToolingSteps(mcpSteps, {
429
- cwd,
430
- output: props.output,
431
- auth,
432
- operations: props.operations,
433
- narrate: "human"
434
- });
435
- funnel.agentsInstalled = uniqueAgents([...funnel.agentsInstalled, ...agentsFromTooling(mcpOnly)]);
436
- }
478
+ mcpState.pinId = pinId;
479
+ if (mcpSteps.length > 0) await runTooling(mcpSteps, mcpOnly);
437
480
  }
438
481
  const canAskConfig = props.pickConfig !== void 0 || detection.interactive;
439
482
  const configResolution = resolveInitConfigChoice({
@@ -490,7 +533,7 @@ const runInit = async (props) => {
490
533
  heading: failed.heading,
491
534
  body: failed.body,
492
535
  rows: [],
493
- next: failed.next
536
+ next: joinNext(failed.next, mcpFailedNext())
494
537
  }));
495
538
  printed = true;
496
539
  outcome = "error";
@@ -503,8 +546,8 @@ const runInit = async (props) => {
503
546
  const context = readContextFile(contextFile);
504
547
  const branch = contextBranch(context);
505
548
  const linked = isLinked(contextFile);
506
- const alreadyPulled = linkedNow && funnel.link === "linked";
507
- if (shouldPullEnvAfterInitConfig({
549
+ const alreadyPulled = linkedNow && funnel.link === "linked" && !pinnedOffline;
550
+ if (funnel.link !== "skipped" && !pinnedOffline && shouldPullEnvAfterInitConfig({
508
551
  wroteNewFile,
509
552
  extraServices,
510
553
  alreadyPulled,
@@ -530,7 +573,7 @@ const runInit = async (props) => {
530
573
  {
531
574
  label: "Agents",
532
575
  value: agentsRowValue({
533
- tooling,
576
+ tooling: reportedTooling(),
534
577
  installed: funnel.agentsInstalled
535
578
  })
536
579
  },
@@ -543,20 +586,29 @@ const runInit = async (props) => {
543
586
  value: configRowValue(funnel.config, existingFilename)
544
587
  }
545
588
  ],
546
- next: failed.next
589
+ next: joinNext(failed.next, mcpFailedNext())
547
590
  }));
548
591
  printed = true;
549
592
  outcome = "error";
550
593
  throw error;
551
594
  }
552
- const pendingUnauthed = funnel.link === "skipped" && !noLink && !alreadyLinked;
595
+ const pendingUnauthed = funnel.link === "skipped" && !noLink;
553
596
  const pendingServices = extraServices && linked;
554
597
  const pendingClaimable = funnel.link === "claimable" && claimExpiresAt !== void 0;
555
598
  const pendingExisting = funnel.config === "existing" && linked && !wroteNewFile;
556
- const pending = pendingUnauthed || pendingServices || pendingClaimable && extraServices || pendingExisting;
599
+ const pending = pinnedOffline || pendingUnauthed || pendingServices || pendingClaimable && extraServices || pendingExisting;
557
600
  const next = [];
558
- if (pendingUnauthed) next.push(...unattendedUnauthedNext());
559
- else if (noLink && !alreadyLinked) next.push(...skippedLinkNext());
601
+ if (pinnedOffline) {
602
+ const commands = deferredLinkCommands(props, linkInputs);
603
+ next.push(...offlinePinNext({
604
+ login: commands.login,
605
+ ...extraServices || existingConfig ? {} : { envPull: commands.envPull }
606
+ }));
607
+ } else if (pendingUnauthed) {
608
+ next.push(...unattendedUnauthedNext(hasExplicitLinkInputs ? deferredLinkCommands(props, linkInputs) : void 0));
609
+ const deferredMcp = mcpDeferredNext();
610
+ if (deferredMcp.length > 0) next.push("", ...deferredMcp);
611
+ } else if (noLink && !alreadyLinked) next.push(...skippedLinkNext());
560
612
  if (pendingClaimable && extraServices && claimExpiresAt !== void 0) {
561
613
  if (next.length > 0) next.push("");
562
614
  next.push(...claimableThenServicesNext(claimExpiresAt));
@@ -573,28 +625,30 @@ const runInit = async (props) => {
573
625
  outcome = pending && !pendingUnauthed && noLink ? "success" : pending ? "pending" : "success";
574
626
  if (pendingUnauthed) outcome = "pending";
575
627
  if (noLink && !pendingUnauthed && !pendingServices && !pendingExisting) outcome = "success";
628
+ if (mcpState.error !== void 0) outcome = "error";
576
629
  printInitDone(formatInitDone({
577
630
  heading: headingForKind(outcome),
578
631
  rows: [
579
632
  {
580
633
  label: "Agents",
581
634
  value: agentsRowValue({
582
- tooling,
635
+ tooling: reportedTooling(),
583
636
  installed: funnel.agentsInstalled
584
637
  })
585
638
  },
586
639
  {
587
640
  label: "Project",
588
- value: projectRowValue(funnel.link ?? (linked ? "already_linked" : null))
641
+ value: pinnedOffline ? PROJECT_PINNED_OFFLINE : projectRowValue(funnel.link ?? (linked ? "already_linked" : null))
589
642
  },
590
643
  {
591
644
  label: "Config",
592
645
  value: configRowValue(funnel.config, existingFilename)
593
646
  }
594
647
  ],
595
- next
648
+ next: joinNext(mcpFailedNext(), next)
596
649
  }));
597
650
  printed = true;
651
+ if (mcpState.error !== void 0) throw mcpState.error;
598
652
  takeCommandSuccessExtras();
599
653
  recordCommandSuccessExtras({
600
654
  init_kind: detection.emptyDirectory ? "empty-skip" : "existing",
@@ -628,6 +682,22 @@ const uniqueAgents = (ids) => {
628
682
  }
629
683
  return out;
630
684
  };
685
+ const joinNext = (first, second) => first.length === 0 || second.length === 0 ? [...first, ...second] : [
686
+ ...first,
687
+ "",
688
+ ...second
689
+ ];
690
+ const withoutMcp = (tooling) => {
691
+ if (tooling.setup === "mixed") return {
692
+ ...tooling,
693
+ mcpAgents: []
694
+ };
695
+ if (tooling.setup === "skills-mcp") return tooling.skillsAgents.length === 0 ? { setup: "skip" } : {
696
+ ...tooling,
697
+ mcpAgents: []
698
+ };
699
+ return tooling;
700
+ };
631
701
  const agentsFromTooling = (tooling) => {
632
702
  switch (tooling.setup) {
633
703
  case "skip": return [];
@@ -1,7 +1,8 @@
1
1
  import { log } from "../log.js";
2
2
  import { getCliName } from "../utils/cli_name.js";
3
3
  import { assertNamedAgentTooling, chooseYesAgentTooling, collectYesAgents, initYesSupportedAgents, noDetectedAgentsMessage, planAgentSteps, planToolingSteps, planYesAgentSteps, resolveInitAgentSetup } from "./plan.js";
4
- import { PROGRESS } from "./copy.js";
4
+ import { MCP_OAUTH_FALLBACK, PROGRESS } from "./copy.js";
5
+ import { CannotMintApiKeyError } from "../mcp/mint.js";
5
6
  import { detectAgent } from "./detect_host.js";
6
7
  import { AgentSelectionSkipped, canPickAgentsInteractively } from "../utils/agent_picker.js";
7
8
  import { mcpInstallError, reportMcpInstall } from "../commands/mcp.js";
@@ -99,10 +100,19 @@ const runToolingSteps = async (steps, options) => {
99
100
  break;
100
101
  }
101
102
  case "mcp": {
102
- const outcome = await raceSigint(ops.installMcp({
103
+ const install = (oauth) => raceSigint(ops.installMcp({
103
104
  ...step.options,
104
- ...sharedOptions
105
+ ...sharedOptions,
106
+ oauth
105
107
  }));
108
+ let outcome;
109
+ try {
110
+ outcome = await install(step.options.oauth);
111
+ } catch (error) {
112
+ if (!(error instanceof CannotMintApiKeyError) || options.mcpOauthFallback !== true || step.options.oauth === true) throw error;
113
+ log.warning(MCP_OAUTH_FALLBACK);
114
+ outcome = await install(true);
115
+ }
106
116
  reportMcpInstall(reportProps, outcome);
107
117
  const error = mcpInstallError(outcome);
108
118
  if (error) throw error;
package/dist/mcp/mint.js CHANGED
@@ -4,7 +4,12 @@ import { getCliName } from "../utils/cli_name.js";
4
4
  import { mintedKeyName } from "../profile_keys.js";
5
5
  import { orgIdForProject } from "../commands/api_keys.js";
6
6
  //#region src/mcp/mint.ts
7
- const cannotMintMessage = "This CLI credential cannot mint API keys. Organization and project-scoped keys cannot create other keys. Sign in with `neon login` or pass a personal API key.";
7
+ var CannotMintApiKeyError = class extends Error {
8
+ constructor() {
9
+ super("This CLI credential cannot mint API keys. Organization and project-scoped keys cannot create other keys. Sign in with `neon login` or pass a personal API key.");
10
+ this.name = "CannotMintApiKeyError";
11
+ }
12
+ };
8
13
  function mintedKeyRevokeCommand(key) {
9
14
  return key.orgId ? `${getCliName()} api-keys revoke ${key.id} --org-id ${key.orgId}` : `${getCliName()} api-keys revoke ${key.id}`;
10
15
  }
@@ -44,7 +49,7 @@ const createAndAssert = async (client, create, name, scope) => {
44
49
  const { data } = await create();
45
50
  return assertUsable(client, data, name, scope);
46
51
  } catch (err) {
47
- if (isNeonApiError(err) && (err.status === 403 || err.status === 404)) throw new Error(cannotMintMessage);
52
+ if (isNeonApiError(err) && (err.status === 403 || err.status === 404)) throw new CannotMintApiKeyError();
48
53
  throw err;
49
54
  }
50
55
  };
@@ -67,4 +72,4 @@ async function withdrawMintedKey(apiClient, key) {
67
72
  return withdraw(apiClient, key.id, key.orgId);
68
73
  }
69
74
  //#endregion
70
- export { mintMcpApiKey, mintedKeyRevokeCommand, withdrawMintedKey };
75
+ export { CannotMintApiKeyError, mintMcpApiKey, mintedKeyRevokeCommand, withdrawMintedKey };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "8.3.1",
3
+ "version": "8.3.2",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -101,8 +101,8 @@
101
101
  "typescript": "^5.9.0",
102
102
  "vitest": "^3.0.9",
103
103
  "@neon-internals/cli-core": "0.0.0",
104
- "@neon-internals/env-core": "0.0.29",
105
104
  "@neon/e2e-harness": "0.0.0",
105
+ "@neon-internals/env-core": "0.0.29",
106
106
  "@neon/functions": "0.11.0"
107
107
  },
108
108
  "publishConfig": {