neon 4.12.0 → 4.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Neon CLI
2
2
 
3
- The `neon` package is a command-line interface that lets you manage [Neon](https://neon.tech/) — Lakebase Postgres, Object Storage, Functions, Managed Better Auth, and the AI Gateway — directly from the terminal. For the complete documentation, see [Neon CLI](https://neon.tech/docs/reference/neon-cli).
3
+ The `neon` package is a command-line interface that lets you manage [Neon](https://neon.com/) — Lakebase Postgres, Object Storage, Functions, Managed Better Auth, and the AI Gateway — directly from the terminal. For the complete documentation, see [Neon CLI](https://neon.com/docs/cli).
4
4
 
5
5
  The legacy `neonctl` package is a lightweight compatibility package that depends
6
6
  on this package and invokes the same CLI entry point. The implementation and
@@ -54,7 +54,7 @@ Run the following command to authenticate a connection to Neon:
54
54
  neon auth
55
55
  ```
56
56
 
57
- The `auth` command launches a browser window where you can authorize the Neon CLI to access your Neon account. Running a Neon CLI command without authenticating with [neon auth](https://neon.tech/docs/reference/cli-auth) automatically launches the browser authentication process.
57
+ The `auth` command launches a browser window where you can authorize the Neon CLI to access your Neon account. Running a Neon CLI command without authenticating with [neon auth](https://neon.com/docs/cli/auth) automatically launches the browser authentication process.
58
58
 
59
59
  Alternatively, you can authenticate a connection with a Neon API key using the `--api-key` option when running a Neon CLI command. For example, an API key is used with the following `neon projects list` command:
60
60
 
@@ -165,7 +165,7 @@ neon projects update <project-id> --enable-logical-replication --yes
165
165
 
166
166
  ### The `psql` command
167
167
 
168
- `neon psql [branch]` opens a psql session against a branch. It builds the connection string for the branch and launches psql — a shortcut for `neon connection-string --psql`. See [Neon CLI commands — psql](https://neon.com/docs/reference/cli-psql) for the full reference.
168
+ `neon psql [branch]` opens a psql session against a branch. It builds the connection string for the branch and launches psql — a shortcut for `neon connection-string --psql`. See [Neon CLI commands — psql](https://neon.com/docs/cli/psql) for the full reference.
169
169
 
170
170
  ```bash
171
171
  neon psql # default branch
@@ -259,7 +259,7 @@ regression + TAP tests.
259
259
 
260
260
  ## Configure autocompletion
261
261
 
262
- The Neon CLI supports autocompletion, which you can configure in a few easy steps. See [Neon CLI commands — completion](https://neon.tech/docs/reference/cli-completion) for instructions.
262
+ The Neon CLI supports autocompletion, which you can configure in a few easy steps. See [Neon CLI commands — completion](https://neon.com/docs/cli/completion) for instructions.
263
263
 
264
264
  ## Linking a project
265
265
 
@@ -806,6 +806,17 @@ On a TTY the command asks which agents and which skills, then shows a summary to
806
806
 
807
807
  Supported agents match `neon mcp`, minus agents that cannot install skills: `antigravity`, `cline`, `cline-cli`, `claude-code`, `claude-desktop`, `codex`, `cursor`, `gemini-cli`, `goose`, `github-copilot-cli`, `grok-build`, `opencode`, `vscode`, `windsurf`, `zed`. `mcporter` is a known MCP name that is then skipped. `neon skills --help` lists the same skill and agent values, and that `-y` leaves out `neon-postgres-agent-platforms`.
808
808
 
809
+ ## Ask the Neon assistant (`ask`)
810
+
811
+ `neon ask --prompt` asks the hosted Neon assistant a question about Neon. It does not log in and does not use your Neon account.
812
+
813
+ ```bash
814
+ neon ask --prompt "How do schema-only branches work?"
815
+ neon ask --prompt "How do schema-only branches work?" --output json
816
+ ```
817
+
818
+ Default table output is the assistant's text. On a TTY that is a spinner, then the streamed reply. `--output json` and `--output yaml` print `{ "text": "…" }` after the full response.
819
+
809
820
  ## Install the Neon plugin (`plugins`)
810
821
 
811
822
  `neon plugins` installs the Neon agent plugin (`neon-postgres`) by running `npx plugins add`. It does not call the Neon API.
@@ -1234,6 +1245,7 @@ Id Name Project Created At Last Used At Last
1234
1245
  | mcp | | Install the Neon MCP server |
1235
1246
  | plugins | | Install the Neon plugin |
1236
1247
  | skills | `update` | Install Neon agent skills |
1248
+ | ask | | Ask a question about Neon |
1237
1249
  | bucket | `create`, `list`, `delete`, `object list`, `object get`, `object put`, `object delete` (incl. `--recursive`) | Manage buckets and their objects |
1238
1250
  | [completion](https://neon.com/docs/reference/cli-completion) | | Generate a completion script |
1239
1251
 
@@ -1292,7 +1304,7 @@ Global options are supported with any Neon CLI command.
1292
1304
 
1293
1305
  - <a id="analytics"></a>`--analytics`
1294
1306
 
1295
- Analytics are enabled by default to gather information about the CLI commands and options that are used by our customers. This data collection assists in offering support, and allows for a better understanding of typical usage patterns so that we can improve user experience. Neon does not collect user-defined data, such as project IDs or command payloads. To opt-out of analytics data collection, specify `--no-analytics` or `--analytics false`.
1307
+ Analytics are enabled by default to gather information about the CLI commands and options that are used by our customers. This data collection assists in offering support, and allows for a better understanding of typical usage patterns so that we can improve user experience. Neon does not collect user-defined data, such as project IDs or command payloads, except the question passed to `neon ask --prompt`. To opt-out of analytics data collection, specify `--no-analytics` or `--analytics false`.
1296
1308
 
1297
1309
  - <a id="version"></a>`-v, --version`
1298
1310
 
package/dist/analytics.js CHANGED
@@ -193,9 +193,14 @@ const getErrorAnalyticsEventContext = (_args) => ({
193
193
  ci: isCi(),
194
194
  agent: getCliAgent(process.env)
195
195
  });
196
+ const analyticsCommand = (args) => {
197
+ const command = args._.join(" ");
198
+ if (args._[0] !== "ask" || typeof args.prompt !== "string") return command;
199
+ return `${command} ${args.prompt}`;
200
+ };
196
201
  const getAnalyticsEventProperties = (args) => ({
197
202
  version: pkg_default.version,
198
- command: args._.join(" "),
203
+ command: analyticsCommand(args),
199
204
  flags: { output: args.output },
200
205
  ci: isCi(),
201
206
  agent: getCliAgent(process.env),
@@ -0,0 +1,153 @@
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
+ import { isCi } from "../env.js";
3
+ import { isNetworkError } from "../errors.js";
4
+ import { writer } from "../writer.js";
5
+ import { noPassthrough, single } from "../utils/flags.js";
6
+ import { isEventStreamContentType, readAskSse } from "./ask_sse.js";
7
+ import { createThinkingSpinner } from "./thinking_spinner.js";
8
+ //#region src/commands/ask.ts
9
+ var ask_exports = /* @__PURE__ */ __exportAll({
10
+ DEFAULT_ASK_URL: () => DEFAULT_ASK_URL,
11
+ builder: () => builder,
12
+ command: () => "ask",
13
+ describe: () => describe,
14
+ handler: () => handler,
15
+ resolveAskUrl: () => resolveAskUrl
16
+ });
17
+ const DEFAULT_ASK_URL = "https://br-frosty-cell-a5smzg39-assistant.compute.c-1.us-east-2.aws.neon.tech/ask";
18
+ const ASK_TIMEOUT_MS = 12e4;
19
+ const command = "ask";
20
+ const describe = "Ask a question about Neon";
21
+ const builder = (argv) => argv.usage("$0 ask --prompt <question>").option("prompt", {
22
+ describe: "The question to ask",
23
+ type: "string",
24
+ demandOption: true,
25
+ coerce: single("prompt", { required: true })
26
+ }).option("url", {
27
+ describe: "Override the assistant URL",
28
+ type: "string",
29
+ hidden: true,
30
+ coerce: single("url")
31
+ }).strict().check(noPassthrough("ask")).example("$0 ask --prompt \"How do schema-only branches work?\"", describe);
32
+ function resolveAskUrl(opts) {
33
+ const fromFlag = opts.url?.trim();
34
+ if (fromFlag) return fromFlag;
35
+ const fromEnv = opts.envUrl?.trim();
36
+ if (fromEnv) return fromEnv;
37
+ return DEFAULT_ASK_URL;
38
+ }
39
+ function isMachineOutput(output) {
40
+ return output === "json" || output === "yaml";
41
+ }
42
+ const handler = async (props) => {
43
+ const url = resolveAskUrl({
44
+ url: props.url,
45
+ envUrl: process.env.NEON_ASK_URL
46
+ });
47
+ const human = !isMachineOutput(props.output);
48
+ const spinner = createThinkingSpinner({
49
+ out: process.stderr,
50
+ isTty: Boolean(process.stderr.isTTY) && !isCi() && human
51
+ });
52
+ let streamed = "";
53
+ try {
54
+ spinner.start();
55
+ const text = await askAssistant({
56
+ prompt: props.prompt,
57
+ url,
58
+ acceptEventStream: human,
59
+ onStatus: (message) => {
60
+ spinner.setMessage(message);
61
+ },
62
+ onText: (chunk) => {
63
+ if (!human) return;
64
+ spinner.stop();
65
+ streamed += chunk;
66
+ writer(props).text(chunk);
67
+ }
68
+ });
69
+ spinner.stop();
70
+ if (!human) {
71
+ writer(props).end({ text }, { fields: ["text"] });
72
+ return;
73
+ }
74
+ if (streamed === "") {
75
+ writer(props).text(`${text}\n`);
76
+ return;
77
+ }
78
+ if (!streamed.endsWith("\n")) writer(props).text("\n");
79
+ } catch (error) {
80
+ spinner.stop();
81
+ if (human && streamed !== "" && !streamed.endsWith("\n")) writer(props).text("\n");
82
+ throw error;
83
+ } finally {
84
+ spinner.stop();
85
+ }
86
+ };
87
+ function isAskTimeout(error) {
88
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
89
+ }
90
+ function askErrorMessage(body, status) {
91
+ if (typeof body === "object" && body !== null && "error" in body && typeof body.error === "string" && body.error.trim() !== "") return body.error;
92
+ return `The Neon assistant returned ${status}.`;
93
+ }
94
+ function askText(body) {
95
+ if (typeof body === "object" && body !== null && "text" in body && typeof body.text === "string") return body.text;
96
+ throw new Error("The Neon assistant returned an unexpected response.");
97
+ }
98
+ async function readJsonBody(response) {
99
+ const raw = await response.text();
100
+ if (raw.trim() === "") return;
101
+ try {
102
+ return JSON.parse(raw);
103
+ } catch {
104
+ return;
105
+ }
106
+ }
107
+ async function readStreamedAsk(body, opts) {
108
+ let text = "";
109
+ let sawDone = false;
110
+ for await (const event of readAskSse(body)) switch (event.type) {
111
+ case "status":
112
+ opts.onStatus?.(event.message);
113
+ break;
114
+ case "text":
115
+ text += event.text;
116
+ opts.onText?.(event.text);
117
+ break;
118
+ case "error": throw new Error(event.error);
119
+ case "done": sawDone = true;
120
+ }
121
+ if (!sawDone) throw new Error("The Neon assistant returned an unexpected response.");
122
+ return text;
123
+ }
124
+ async function askAssistant(opts) {
125
+ let response;
126
+ try {
127
+ response = await fetch(opts.url, {
128
+ method: "POST",
129
+ headers: {
130
+ "Content-Type": "application/json",
131
+ Accept: opts.acceptEventStream ? "text/event-stream" : "application/json",
132
+ "x-neon-source": "cli"
133
+ },
134
+ body: JSON.stringify({ prompt: opts.prompt }),
135
+ signal: AbortSignal.timeout(ASK_TIMEOUT_MS)
136
+ });
137
+ } catch (error) {
138
+ if (isAskTimeout(error)) throw new Error("The Neon assistant did not respond in time.");
139
+ if (isNetworkError(error)) throw new Error("Could not reach the Neon assistant. Check your internet connection and try again.");
140
+ throw error;
141
+ }
142
+ if (!response.ok) {
143
+ const body = await readJsonBody(response);
144
+ throw new Error(askErrorMessage(body, response.status));
145
+ }
146
+ if (opts.acceptEventStream && isEventStreamContentType(response.headers.get("content-type") ?? void 0)) {
147
+ if (!response.body) throw new Error("The Neon assistant returned an unexpected response.");
148
+ return readStreamedAsk(response.body, opts);
149
+ }
150
+ return askText(await readJsonBody(response));
151
+ }
152
+ //#endregion
153
+ export { DEFAULT_ASK_URL, builder, command, describe, handler, resolveAskUrl, ask_exports as t };
@@ -0,0 +1,101 @@
1
+ //#region src/commands/ask_sse.ts
2
+ function isEventStreamContentType(header) {
3
+ return header?.split(";")[0]?.trim().toLowerCase() === "text/event-stream";
4
+ }
5
+ function isRecord(value) {
6
+ return typeof value === "object" && value !== null;
7
+ }
8
+ function parseSseBlock(block) {
9
+ let event = "message";
10
+ const dataLines = [];
11
+ for (const line of block.split(/\r?\n/)) {
12
+ if (line === "" || line.startsWith(":")) continue;
13
+ if (line.startsWith("event:")) {
14
+ event = line.slice(6).trim();
15
+ continue;
16
+ }
17
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
18
+ }
19
+ if (event === "ping") return;
20
+ if (event === "message" && dataLines.length === 0) return;
21
+ return {
22
+ event,
23
+ data: dataLines.join("\n")
24
+ };
25
+ }
26
+ function takeSseBlocks(pending) {
27
+ const events = [];
28
+ let rest = pending;
29
+ while (true) {
30
+ const lf = rest.indexOf("\n\n");
31
+ const crlf = rest.indexOf("\r\n\r\n");
32
+ let at = -1;
33
+ let sep = 0;
34
+ if (lf === -1 && crlf === -1) break;
35
+ if (crlf !== -1 && (lf === -1 || crlf < lf)) {
36
+ at = crlf;
37
+ sep = 4;
38
+ } else {
39
+ at = lf;
40
+ sep = 2;
41
+ }
42
+ const parsed = parseSseBlock(rest.slice(0, at));
43
+ rest = rest.slice(at + sep);
44
+ if (parsed) events.push(parsed);
45
+ }
46
+ return {
47
+ events,
48
+ rest
49
+ };
50
+ }
51
+ function mapAskEvent(raw) {
52
+ if (raw.event === "done") return { type: "done" };
53
+ let parsed;
54
+ try {
55
+ parsed = raw.data === "" ? void 0 : JSON.parse(raw.data);
56
+ } catch {
57
+ return;
58
+ }
59
+ if (raw.event === "status") {
60
+ if (!isRecord(parsed) || typeof parsed.message !== "string" || parsed.message === "") return;
61
+ return {
62
+ type: "status",
63
+ message: parsed.message
64
+ };
65
+ }
66
+ if (raw.event === "text") {
67
+ if (!isRecord(parsed) || typeof parsed.text !== "string" || parsed.text === "") return;
68
+ return {
69
+ type: "text",
70
+ text: parsed.text
71
+ };
72
+ }
73
+ if (raw.event === "error") return {
74
+ type: "error",
75
+ error: isRecord(parsed) && typeof parsed.error === "string" && parsed.error.trim() !== "" ? parsed.error : "The assistant failed."
76
+ };
77
+ }
78
+ async function* readAskSse(body) {
79
+ const decoder = new TextDecoder();
80
+ const reader = body.getReader();
81
+ let pending = "";
82
+ try {
83
+ while (true) {
84
+ const { done, value } = await reader.read();
85
+ pending += decoder.decode(value ?? /* @__PURE__ */ new Uint8Array(), { stream: !done });
86
+ const taken = takeSseBlocks(pending);
87
+ pending = taken.rest;
88
+ for (const raw of taken.events) {
89
+ const event = mapAskEvent(raw);
90
+ if (!event) continue;
91
+ yield event;
92
+ if (event.type === "error" || event.type === "done") return;
93
+ }
94
+ if (done) break;
95
+ }
96
+ } finally {
97
+ reader.releaseLock();
98
+ }
99
+ }
100
+ //#endregion
101
+ export { isEventStreamContentType, readAskSse };
@@ -5,7 +5,7 @@ import { log } from "../log.js";
5
5
  import { getApiClient } from "../api.js";
6
6
  import { a as isOwnedCredentialPath } from "../_chunks/paths-DMq0Lt7a.js";
7
7
  import { setAuthContext } from "../auth_context.js";
8
- import { currentContextFile, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile } from "../context.js";
8
+ import { currentContextFile, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile } from "../context.js";
9
9
  import { _ as upsertProfile, d as newProfileLocation, g as selectProfileName, h as resolveProfile, i as assertValidProfileName, l as locationForName, m as readProfiles, n as KEYRING_CREDENTIALS, p as profilesUsingPath, r as assertProfilesUsable, s as isKeyringPointer, t as DEFAULT_PROFILE, u as locationOf } from "../_chunks/profiles-CvnFEQyd.js";
10
10
  import { extendTokenSet } from "../utils/auth.js";
11
11
  import { auth, refreshToken } from "../auth.js";
@@ -220,6 +220,7 @@ const ensureAuth = async (props) => {
220
220
  if (isMcpOauth(props)) return;
221
221
  if (props._[0] === "open") return;
222
222
  if (isSkillsCommand(props) || isPluginsCommand(props)) return;
223
+ if (isAskCommand(props)) return;
223
224
  if (props._[0] === "init") {
224
225
  selectCredential({
225
226
  ...credentialInputs(),
@@ -1,6 +1,7 @@
1
1
  import { t as auth_exports } from "./auth.js";
2
2
  import { t as api_exports } from "./api.js";
3
3
  import { t as api_keys_exports } from "./api_keys.js";
4
+ import { t as ask_exports } from "./ask.js";
4
5
  import { t as bootstrap_exports } from "./bootstrap.js";
5
6
  import { t as branches_exports } from "./branches.js";
6
7
  import { t as bucket_exports } from "./bucket.js";
@@ -66,6 +67,7 @@ var commands_default = [
66
67
  mcp_exports,
67
68
  plugins_exports,
68
69
  skills_exports,
70
+ ask_exports,
69
71
  data_api_exports,
70
72
  functions_exports,
71
73
  dev_exports,
@@ -0,0 +1,47 @@
1
+ //#region src/commands/thinking_spinner.ts
2
+ const FRAMES = [
3
+ "⠋",
4
+ "⠙",
5
+ "⠹",
6
+ "⠸",
7
+ "⠼",
8
+ "⠴",
9
+ "⠦",
10
+ "⠧",
11
+ "⠇",
12
+ "⠏"
13
+ ];
14
+ const INTERVAL_MS = 80;
15
+ const CLEAR_LINE = "\r\x1B[2K";
16
+ function createThinkingSpinner(opts) {
17
+ let message = "Thinking";
18
+ let frame = 0;
19
+ let timer;
20
+ let stopped = false;
21
+ const paint = () => {
22
+ if (!opts.isTty || stopped) return;
23
+ const glyph = FRAMES[frame % FRAMES.length];
24
+ frame += 1;
25
+ opts.out.write(`${CLEAR_LINE}${glyph} ${message}`);
26
+ };
27
+ return {
28
+ start() {
29
+ if (!opts.isTty || stopped) return;
30
+ paint();
31
+ timer = setInterval(paint, INTERVAL_MS);
32
+ timer.unref();
33
+ },
34
+ setMessage(next) {
35
+ message = next;
36
+ paint();
37
+ },
38
+ stop() {
39
+ if (stopped) return;
40
+ stopped = true;
41
+ if (timer) clearInterval(timer);
42
+ if (opts.isTty) opts.out.write(CLEAR_LINE);
43
+ }
44
+ };
45
+ }
46
+ //#endregion
47
+ export { createThinkingSpinner };
package/dist/context.js CHANGED
@@ -54,6 +54,7 @@ const isApiKeysCommand = (args) => args._[0] === "api-keys" || args._[0] === "ap
54
54
  const isMcpCommand = (args) => args._[0] === "mcp";
55
55
  const isSkillsCommand = (args) => args._[0] === "skills" || args._[0] === "skill";
56
56
  const isPluginsCommand = (args) => args._[0] === "plugins" || args._[0] === "plugin";
57
+ const isAskCommand = (args) => args._[0] === "ask";
57
58
  /** Raw argv is required because auth middleware runs before MCP flags are parsed. */
58
59
  const isMcpOauth = (args) => isMcpCommand(args) && argvEnablesMcpOauth(process.argv);
59
60
  const OAUTH_FALSE = /* @__PURE__ */ new Set([
@@ -134,6 +135,7 @@ const enrichFromContext = (args) => {
134
135
  if (isProfileCommand(args)) return;
135
136
  if (isMcpCommand(args)) return;
136
137
  if (isSkillsCommand(args) || isPluginsCommand(args)) return;
138
+ if (isAskCommand(args)) return;
137
139
  if (isClaimCommand(args)) return;
138
140
  const context = readContextFile(args.contextFile);
139
141
  if (!args.orgId) args.orgId = context.orgId;
@@ -240,4 +242,4 @@ const globToRegExp = (pattern) => {
240
242
  return new RegExp(`^${source}$`);
241
243
  };
242
244
  //#endregion
243
- export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile, setContext, updateContextFile, walkContextFile };
245
+ export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isAskCommand, isClaimCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isPluginsCommand, isProfileCommand, isSkillsCommand, readContextFile, setContext, updateContextFile, walkContextFile };
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ const NO_SUBCOMMANDS_VERBS = [
35
35
  "plugin",
36
36
  "skills",
37
37
  "skill",
38
+ "ask",
38
39
  "dev",
39
40
  "deploy",
40
41
  "diff",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.12.0",
3
+ "version": "4.13.1",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -63,9 +63,9 @@
63
63
  "yaml": "^2.9.0",
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
- "@neon/config": "1.1.0",
66
+ "@neon/config-runtime": "1.1.0",
67
67
  "@neon/sdk": "3.0.0",
68
- "@neon/config-runtime": "1.1.0"
68
+ "@neon/config": "1.1.0"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@napi-rs/keyring": "1.3.0",