neon 4.2.0 → 4.3.0

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
@@ -759,6 +759,38 @@ On a TTY the command asks for config location (global is the default), then agen
759
759
 
760
760
  The default mints an account-wide API key (or reuses the Bearer already configured for Neon at `https://mcp.neon.tech/mcp`) and writes it into each selected agent's config. That key reaches everything the account can, in every organization. Revoke it with `neon api-keys revoke <id>`. `--oauth` writes the URL with no `Authorization` header; the agent signs in on first use. `--project` writes into the project config (`.cursor/mcp.json` and similar). `--read-only` adds `?readonly=true`. `--project-id` adds `?projectId=`. `--category` adds `?category=` (repeatable or comma-separated: `projects`, `branches`, `schema`, `querying`, `neon_auth`, `data_api`, `observability`, `docs`). Those query params restrict which MCP tools the server exposes; they do not change what the minted key can do.
761
761
 
762
+ ## Install Neon agent skills (`skills`)
763
+
764
+ `neon skills` installs Neon agent skills by running `npx skills add`. It does not call the Neon API. This command needs Node.js 22.20 or newer. The rest of the CLI supports Node.js 20.19 or newer.
765
+
766
+ ```bash
767
+ # Interactive: this directory, then agents, then skills, then confirm.
768
+ $ neon skills
769
+
770
+ # Skip prompts. This directory, every detected agent, the default skills.
771
+ $ neon skills -y
772
+
773
+ $ neon skills -s neon -s neon-ai-gateway --agent cursor
774
+
775
+ $ neon skills --agent cursor --agent claude-code
776
+
777
+ # User-level skills.
778
+ $ neon skills --global
779
+
780
+ # Update installed skills in this directory.
781
+ $ neon skills update
782
+ $ neon skills update -y
783
+ $ neon skills update --global -y
784
+ ```
785
+
786
+ On a TTY the command asks which agents and which skills, then shows a summary to confirm. Detected agents start selected from project-folder markers such as `.cursor`. Default skills start selected. `neon-postgres-agent-platforms` is offered and starts unselected.
787
+
788
+ `-y` skips those questions and installs the default skills. `--skill` / `-s` names specific skills and skips the skill picker. `--agent` and `--global` still apply. Without a TTY, pass `-y` or `--skill`. `--agent` alone is not enough.
789
+
790
+ `--skill` names: `claimable-postgres`, `neon`, `neon-ai-gateway`, `neon-functions`, `neon-object-storage`, `neon-postgres`, `neon-postgres-branches`, `neon-postgres-egress-optimizer`, `neon-postgres-agent-platforms`.
791
+
792
+ `--agent` names 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.
793
+
762
794
  ## Snapshots (`snapshots`)
763
795
 
764
796
  `neon snapshots` (alias `neon snapshot`) manages **snapshots** — point-in-time backups of a branch that you can list, rename, expire, restore into a branch, or schedule automatically. Snapshots are a Beta Neon feature and were previously only available in the Console and REST API; this command group brings them to the CLI.
@@ -1139,6 +1171,7 @@ Id Name Project Created At Last Used At Last
1139
1171
  | bootstrap | | Scaffold a project from a template |
1140
1172
  | init | | Set up a project for a coding agent |
1141
1173
  | mcp | | Install the Neon MCP server |
1174
+ | skills | `update` | Install Neon agent skills |
1142
1175
  | bucket | `create`, `list`, `delete`, `object list`, `object get`, `object put`, `object delete` (incl. `--recursive`) | Manage buckets and their objects |
1143
1176
  | [completion](https://neon.com/docs/reference/cli-completion) | | Generate a completion script |
1144
1177
 
@@ -6,7 +6,7 @@ import { log } from "../log.js";
6
6
  import { getApiClient } from "../api.js";
7
7
  import { o as isOwnedCredentialPath } from "../_chunks/paths-g3DRqJiD.js";
8
8
  import { setAuthContext } from "../auth_context.js";
9
- import { isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isProfileCommand } from "../context.js";
9
+ import { isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isProfileCommand, isSkillsCommand } from "../context.js";
10
10
  import { extendTokenSet } from "../utils/auth.js";
11
11
  import { auth, refreshToken } from "../auth.js";
12
12
  import { i as selectCredential, n as displacedProfileWarning, t as credentialInputs } from "../_chunks/auth_selection-Bx2NAgkk.js";
@@ -217,6 +217,7 @@ const ensureAuth = async (props) => {
217
217
  if (isProfileCommand(props)) return;
218
218
  if (isMcpOauth(props)) return;
219
219
  if (props._[0] === "open") return;
220
+ if (isSkillsCommand(props)) return;
220
221
  const isLocalDev = props._[0] === "dev";
221
222
  const isBootstrap = props._[0] === "bootstrap";
222
223
  const isInit = props._[0] === "init";
@@ -24,8 +24,8 @@ const BRANCH_FIELDS = [
24
24
  "name",
25
25
  "id",
26
26
  "current_state",
27
- "created_at",
28
- "expires_at"
27
+ "expires_at",
28
+ "created_at"
29
29
  ];
30
30
  const BRANCH_FIELDS_RESET = [
31
31
  "name",
@@ -29,6 +29,7 @@ import { t as profile_exports } from "./profile.js";
29
29
  import { t as psql_exports } from "./psql.js";
30
30
  import { t as roles_exports } from "./roles.js";
31
31
  import { t as set_context_exports } from "./set_context.js";
32
+ import { t as skills_exports } from "./skills.js";
32
33
  import { t as snapshots_exports } from "./snapshots.js";
33
34
  import { t as status_exports } from "./status.js";
34
35
  import { t as user_exports } from "./user.js";
@@ -60,6 +61,7 @@ var commands_default = [
60
61
  open_exports,
61
62
  init_exports,
62
63
  mcp_exports,
64
+ skills_exports,
63
65
  data_api_exports,
64
66
  functions_exports,
65
67
  dev_exports,
@@ -12,6 +12,7 @@ import prompts from "prompts";
12
12
  //#region src/commands/projects.ts
13
13
  var projects_exports = /* @__PURE__ */ __exportAll({
14
14
  PROJECT_FIELDS: () => PROJECT_FIELDS,
15
+ RECOVERABLE_PROJECT_FIELDS: () => RECOVERABLE_PROJECT_FIELDS,
15
16
  REGIONS: () => REGIONS,
16
17
  aliases: () => aliases,
17
18
  builder: () => builder,
@@ -25,6 +26,14 @@ const PROJECT_FIELDS = [
25
26
  "region_id",
26
27
  "created_at"
27
28
  ];
29
+ const RECOVERABLE_PROJECT_FIELDS = [
30
+ "id",
31
+ "name",
32
+ "recoverable_until",
33
+ "deleted_at",
34
+ "region_id",
35
+ "created_at"
36
+ ];
28
37
  const REGIONS = [
29
38
  "aws-us-west-2",
30
39
  "aws-ap-southeast-1",
@@ -185,11 +194,7 @@ const list = async (props) => {
185
194
  const sharedProjects = props.orgId || props.recoverableOnly ? [] : await getList(props.apiClient.listSharedProjects);
186
195
  const out = writer(props);
187
196
  out.write(ownedProjects, {
188
- fields: props.recoverableOnly ? [
189
- ...PROJECT_FIELDS,
190
- "deleted_at",
191
- "recoverable_until"
192
- ] : PROJECT_FIELDS,
197
+ fields: props.recoverableOnly ? RECOVERABLE_PROJECT_FIELDS : PROJECT_FIELDS,
193
198
  title: "Projects",
194
199
  emptyMessage: props.recoverableOnly ? "You don't have any recoverable projects." : "You don't have any projects yet. See how to create a new project:\n> neon projects create --help"
195
200
  });
@@ -351,4 +356,4 @@ const onPromptState = (state) => {
351
356
  }
352
357
  };
353
358
  //#endregion
354
- export { PROJECT_FIELDS, REGIONS, aliases, builder, command, describe, handler, projects_exports as t };
359
+ export { PROJECT_FIELDS, RECOVERABLE_PROJECT_FIELDS, REGIONS, aliases, builder, command, describe, handler, projects_exports as t };
@@ -0,0 +1,223 @@
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
+ import { log } from "../log.js";
3
+ import { writer } from "../writer.js";
4
+ import { noPassthrough } from "../utils/flags.js";
5
+ import { getAgentDisplayName } from "../mcp/agents.js";
6
+ import "../init/agents.js";
7
+ import { canPickAgentsInteractively } from "../utils/agent_picker.js";
8
+ import { mappedSkillsAgentNames } from "../skills/targets.js";
9
+ import { confirmSkillsInstall, confirmSkillsUpdate } from "../skills/wizard.js";
10
+ import { assertSkillsCanRun, resolveSkillsPlan } from "../skills/plan.js";
11
+ import { assertSkillsNode, neonSkillsRetryCommand, neonSkillsUpdateRetryCommand, runSkillsCli, skillsAddArgs, skillsMetadata, skillsUpdateArgs, skillsUpdateDetail, skillsUpdateHadNothing } from "../skills/run.js";
12
+ //#region src/commands/skills.ts
13
+ var skills_exports = /* @__PURE__ */ __exportAll({
14
+ builder: () => builder,
15
+ command: () => command,
16
+ describe: () => describe,
17
+ handler: () => handler
18
+ });
19
+ const scopeLabel = (scope) => scope === "project" ? "this directory" : "user-level";
20
+ const coerceAgents = (value) => {
21
+ if (value === void 0) return [];
22
+ const list = Array.isArray(value) ? value : [value];
23
+ if (list.length === 0) throw new Error("--agent needs a value. Pass one, or omit the flag entirely.");
24
+ return list.map((item) => {
25
+ if (typeof item !== "string" || item.trim() === "") throw new Error("--agent needs a value. Pass one, or omit the flag entirely.");
26
+ return item;
27
+ });
28
+ };
29
+ const coerceSkills = (value) => {
30
+ if (value === void 0) return [];
31
+ const list = Array.isArray(value) ? value : [value];
32
+ if (list.length === 0) throw new Error("--skill needs a value. Pass one, or omit the flag entirely.");
33
+ return list.map((item) => {
34
+ if (typeof item !== "string" || item.trim() === "") throw new Error("--skill needs a value. Pass one, or omit the flag entirely.");
35
+ return item;
36
+ });
37
+ };
38
+ const command = "skills";
39
+ const describe = "Install Neon agent skills into coding agents";
40
+ const builder = (argv) => argv.usage("$0 skills [command] [options]").command("update", "Update installed skills to the latest versions", (yargs) => yargs.usage("$0 skills update [options]").options({ global: {
41
+ type: "boolean",
42
+ default: false,
43
+ describe: "Update user-level skills (skills CLI -g). Default is this directory"
44
+ } }).example("$0 skills update", "Interactive: confirm, then update skills in this directory").example("$0 skills update -y", "Update skills in this directory without prompting").example("$0 skills update --global -y", "Update user-level skills").strict().hide("agent").hide("skill").check((args) => {
45
+ noPassthrough("skills update")(args);
46
+ const agents = args.agent;
47
+ if (Array.isArray(agents) && agents.length > 0) throw new Error("neon skills update does not take --agent. It refreshes every installed skill in this directory (or --global).");
48
+ const skills = args.skill;
49
+ if (Array.isArray(skills) && skills.length > 0) throw new Error("neon skills update does not take --skill. It refreshes every installed skill in this directory (or --global).");
50
+ return true;
51
+ }), (args) => updateHandler(args)).options({
52
+ yes: {
53
+ alias: "y",
54
+ type: "boolean",
55
+ default: false,
56
+ describe: "Skip prompts"
57
+ },
58
+ global: {
59
+ type: "boolean",
60
+ default: false,
61
+ describe: "Install user-level skills (skills CLI -g). Default is this directory"
62
+ },
63
+ agent: {
64
+ alias: "a",
65
+ type: "array",
66
+ string: true,
67
+ describe: "Coding agent to install into (repeatable). Skips the agent picker",
68
+ coerce: coerceAgents
69
+ },
70
+ skill: {
71
+ alias: "s",
72
+ type: "array",
73
+ string: true,
74
+ describe: "Skill to install (repeatable). Skips the skill picker",
75
+ coerce: coerceSkills
76
+ }
77
+ }).example("$0 skills", "Interactive: this directory, agents, skills, then confirm").example("$0 skills -y", "This directory, detected agents, the default skills").example("$0 skills -s neon -s neon-ai-gateway --agent cursor", "Install named skills into specific agents").example("$0 skills --global", "Install user-level skills").strict().check(noPassthrough("skills"));
78
+ const handler = async (props) => {
79
+ assertSkillsNode();
80
+ const cwd = process.cwd();
81
+ const yes = props.yes === true;
82
+ const interactive = canPickAgentsInteractively() && !yes;
83
+ const plan = await resolveSkillsPlan({
84
+ global: props.global === true,
85
+ agents: props.agent ?? [],
86
+ skills: props.skill ?? [],
87
+ yes,
88
+ cwd,
89
+ interactive
90
+ });
91
+ const mapped = mappedSkillsAgentNames(plan.agents);
92
+ for (const agent of plan.skipped) log.warning("Skipping %s: no skills mapping.", getAgentDisplayName(agent));
93
+ if (interactive) {
94
+ if (!await confirmSkillsInstall({
95
+ scope: plan.scope,
96
+ agents: plan.agents,
97
+ invocations: plan.invocations
98
+ })) {
99
+ log.info("Aborted. Nothing was written.");
100
+ return;
101
+ }
102
+ }
103
+ const metadata = skillsMetadata("skills");
104
+ const rows = [];
105
+ const failed = [];
106
+ const scope = scopeLabel(plan.scope);
107
+ for (const invocation of plan.invocations) {
108
+ const skills = invocation.skills.join(", ");
109
+ const args = skillsAddArgs({
110
+ source: invocation.source,
111
+ skills: invocation.skills,
112
+ agents: mapped,
113
+ global: plan.scope === "global",
114
+ metadata
115
+ });
116
+ try {
117
+ await runSkillsCli({
118
+ args,
119
+ cwd
120
+ });
121
+ rows.push({
122
+ scope,
123
+ skills,
124
+ agents: mapped.join(", "),
125
+ status: "installed"
126
+ });
127
+ } catch (error) {
128
+ const message = error instanceof Error ? error.message : String(error);
129
+ rows.push({
130
+ scope,
131
+ skills,
132
+ agents: mapped.join(", "),
133
+ status: "failed",
134
+ error: "skills CLI failed"
135
+ });
136
+ failed.push({
137
+ label: skills,
138
+ message,
139
+ skills: invocation.skills
140
+ });
141
+ }
142
+ }
143
+ const out = writer(props);
144
+ out.write(rows, {
145
+ fields: [
146
+ "scope",
147
+ "skills",
148
+ "agents",
149
+ "status",
150
+ "error"
151
+ ],
152
+ title: "Skills"
153
+ });
154
+ out.end();
155
+ if (failed.length === 0) {
156
+ log.info(plan.scope === "project" ? "Wrote skills in this directory." : "Wrote user-level skills.");
157
+ return;
158
+ }
159
+ const first = failed[0];
160
+ if (first === void 0) throw new Error("Failed to install Neon agent skills.");
161
+ const retry = neonSkillsRetryCommand({
162
+ skills: failed.flatMap((row) => row.skills),
163
+ agents: plan.agents,
164
+ global: plan.scope === "global"
165
+ });
166
+ if (first.message.includes("needs npx (Node.js)") || first.message.includes("needs Node.js")) throw new Error(first.message);
167
+ if (failed.length === rows.length) throw new Error(`${first.message}\nRetry with: ${retry}`);
168
+ throw new Error(`Failed to install Neon agent skills for: ${failed.map((row) => row.label).join(", ")}.\n${first.message}\nRetry with: ${retry}`);
169
+ };
170
+ const updateHandler = async (props) => {
171
+ assertSkillsNode();
172
+ const cwd = process.cwd();
173
+ const yes = props.yes === true;
174
+ const interactive = canPickAgentsInteractively() && !yes;
175
+ assertSkillsCanRun({
176
+ yes,
177
+ interactive,
178
+ action: "update"
179
+ });
180
+ const scope = props.global === true ? "global" : "project";
181
+ if (interactive) {
182
+ if (!await confirmSkillsUpdate({ scope })) {
183
+ log.info("Aborted. Nothing was written.");
184
+ return;
185
+ }
186
+ }
187
+ const rows = [];
188
+ const args = skillsUpdateArgs({ global: scope === "global" });
189
+ let failure;
190
+ try {
191
+ const result = await runSkillsCli({
192
+ args,
193
+ cwd
194
+ });
195
+ const output = `${result.stdout}\n${result.stderr}`;
196
+ rows.push({
197
+ scope: scopeLabel(scope),
198
+ status: skillsUpdateHadNothing(output) ? "none" : "updated",
199
+ detail: skillsUpdateDetail(output)
200
+ });
201
+ } catch (error) {
202
+ failure = error instanceof Error ? error.message : String(error);
203
+ rows.push({
204
+ scope: scopeLabel(scope),
205
+ status: "failed",
206
+ error: "skills CLI failed"
207
+ });
208
+ }
209
+ const out = writer(props);
210
+ out.write(rows, {
211
+ fields: [
212
+ "scope",
213
+ "status",
214
+ "detail",
215
+ "error"
216
+ ],
217
+ title: "Skills"
218
+ });
219
+ out.end();
220
+ if (failure !== void 0) throw new Error(failure.includes("needs npx (Node.js)") || failure.includes("needs Node.js") ? failure : `${failure}\nRetry with: ${neonSkillsUpdateRetryCommand(scope === "global")}`);
221
+ };
222
+ //#endregion
223
+ export { builder, command, describe, handler, skills_exports as t };
@@ -18,8 +18,8 @@ const SNAPSHOT_FIELDS = [
18
18
  "id",
19
19
  "name",
20
20
  "source_branch_id",
21
- "created_at",
22
- "expires_at"
21
+ "expires_at",
22
+ "created_at"
23
23
  ];
24
24
  const SCHEDULE_FIELDS = [
25
25
  "frequency",
package/dist/context.js CHANGED
@@ -51,6 +51,7 @@ const isProfileCommand = (args) => args._[0] === "profile" || args._[0] === "pro
51
51
  */
52
52
  const isApiKeysCommand = (args) => args._[0] === "api-keys" || args._[0] === "api-key";
53
53
  const isMcpCommand = (args) => args._[0] === "mcp";
54
+ const isSkillsCommand = (args) => args._[0] === "skills";
54
55
  /** Raw argv is required because auth middleware runs before MCP flags are parsed. */
55
56
  const isMcpOauth = (args) => isMcpCommand(args) && argvEnablesMcpOauth(process.argv);
56
57
  const OAUTH_FALSE = /* @__PURE__ */ new Set([
@@ -130,6 +131,7 @@ const enrichFromContext = (args) => {
130
131
  if (isApiKeysCommand(args)) return;
131
132
  if (isProfileCommand(args)) return;
132
133
  if (isMcpCommand(args)) return;
134
+ if (isSkillsCommand(args)) return;
133
135
  const context = readContextFile(args.contextFile);
134
136
  if (!args.orgId) args.orgId = context.orgId;
135
137
  if (!args.projectId) args.projectId = context.projectId;
@@ -235,4 +237,4 @@ const globToRegExp = (pattern) => {
235
237
  return new RegExp(`^${source}$`);
236
238
  };
237
239
  //#endregion
238
- export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isProfileCommand, readContextFile, setContext, updateContextFile, walkContextFile };
240
+ export { applyContext, contextBranch, currentContextFile, enrichFromContext, ensureGitignored, isApiKeysCommand, isConfigInit, isCurrentBranchProbe, isMcpCommand, isMcpOauth, isProfileCommand, isSkillsCommand, readContextFile, setContext, updateContextFile, walkContextFile };
@@ -32,6 +32,12 @@ function displayWidth(s) {
32
32
  }
33
33
  return width;
34
34
  }
35
+ function presentFields(data, fields) {
36
+ return fields.filter((field) => data.some((item) => {
37
+ const value = fieldValue(item, field);
38
+ return value !== void 0 && value !== "";
39
+ }));
40
+ }
35
41
  function formatHumanChunk(chunk) {
36
42
  const arrayData = Array.isArray(chunk.data) ? chunk.data : [chunk.data];
37
43
  const isList = Array.isArray(chunk.data);
@@ -42,48 +48,28 @@ function formatHumanChunk(chunk) {
42
48
  out += formatTitle(chunk.title, chunk.colorTitle) + "\n";
43
49
  }
44
50
  if (!arrayData.length) return out;
45
- const fields = chunk.fields.filter((field) => arrayData.some((item) => {
46
- const value = fieldValue(item, field);
47
- return value !== void 0 && value !== "";
48
- }));
51
+ if (isList) {
52
+ const fields = presentFields(arrayData, chunk.fields);
53
+ if (fields.length) {
54
+ const headers = fields.map(titleCaseField);
55
+ const rows = arrayData.map((item) => fields.map((field) => cellText(item, field, chunk.renderColumns)));
56
+ out += formatColumns(headers, rows, naturalWidths(headers, rows)) + "\n";
57
+ }
58
+ return out;
59
+ }
60
+ const fields = presentFields(arrayData, chunk.fields);
49
61
  if (!fields.length) return out;
50
- const headers = fields.map(titleCaseField);
51
- const rows = arrayData.map((item) => fields.map((field) => cellText(item, field, chunk.renderColumns)));
52
- const body = isList ? formatList(headers, rows, chunk.width) : formatStacked(headers, rows, chunk.width);
62
+ const body = formatStacked(fields.map(titleCaseField), arrayData.map((item) => fields.map((field) => cellText(item, field, chunk.renderColumns))), chunk.width);
53
63
  if (body) out += body + "\n";
54
64
  return out;
55
65
  }
56
66
  function formatTitle(title, colorTitle) {
57
67
  return colorTitle ? chalk.bold(title) : title;
58
68
  }
59
- function formatList(headers, rows, width) {
60
- if (headers.length === 1) return formatOneColumn(headers[0] ?? "", rows.map((row) => row[0] ?? ""));
61
- if (width === void 0) return formatColumns(headers, rows, naturalWidths(headers, rows));
62
- for (let n = headers.length; n >= 2; n--) {
63
- const subsetHeaders = headers.slice(0, n);
64
- const subsetRows = rows.map((row) => row.slice(0, n));
65
- const natural = naturalWidths(subsetHeaders, subsetRows);
66
- if (rowWidth(natural) <= width) return formatColumns(subsetHeaders, subsetRows, natural);
67
- const fitted = tryFit(subsetHeaders, subsetRows, width);
68
- if (fitted === void 0) continue;
69
- if (!natural.slice(0, n - 1).every((col, i) => fitted[i] === col)) continue;
70
- return formatColumns(subsetHeaders.map((cell, i) => truncateTo(cell, fitted[i] ?? 0)), subsetRows.map((row) => row.map((cell, i) => truncateTo(cell, fitted[i] ?? 0))), fitted);
71
- }
72
- return formatStacked(headers, rows, width);
73
- }
74
- function rowWidth(widths) {
75
- if (widths.length === 0) return 0;
76
- return widths.reduce((sum, col) => sum + col, 0) + (widths.length - 1) * GUTTER;
77
- }
78
- function formatOneColumn(header, values) {
79
- const lines = [chalk.green(header)];
80
- for (const value of values) lines.push(value);
81
- return lines.join("\n");
82
- }
83
69
  function formatColumns(headers, rows, widths) {
84
70
  const joinRow = (cells) => cells.map((cell, i) => {
85
71
  const width = widths[i] ?? 0;
86
- return i === cells.length - 1 ? truncateTo(cell, width) : padEndWidth(cell, width);
72
+ return i === cells.length - 1 ? cell : padEndWidth(cell, width);
87
73
  }).join(" ".repeat(GUTTER)).trimEnd();
88
74
  const lines = [chalk.green(joinRow(headers))];
89
75
  for (const row of rows) lines.push(joinRow(row));
@@ -101,25 +87,6 @@ function formatStacked(headers, rows, width) {
101
87
  function stackedLine(header, value, labelWidth) {
102
88
  return `${padEndWidth(header, labelWidth)}${" ".repeat(GUTTER)}${value}`;
103
89
  }
104
- function tryFit(headers, rows, width) {
105
- const n = headers.length;
106
- if (n === 0) return [];
107
- const gutters = (n - 1) * GUTTER;
108
- if (width <= gutters) return;
109
- const budget = width - gutters;
110
- if (budget < n * MIN_COL) return;
111
- const widths = naturalWidths(headers, rows);
112
- let overflow = widths.reduce((sum, col) => sum + col, 0) - budget;
113
- if (overflow <= 0) return widths;
114
- for (let i = n - 1; i >= 0 && overflow > 0; i--) {
115
- const current = widths[i] ?? 0;
116
- const take = Math.min(Math.max(current - MIN_COL, 0), overflow);
117
- widths[i] = current - take;
118
- overflow -= take;
119
- }
120
- if (overflow > 0) return;
121
- return widths;
122
- }
123
90
  function naturalWidths(headers, rows) {
124
91
  return headers.map((header, i) => Math.max(displayWidth(header), ...rows.map((row) => displayWidth(row[i] ?? ""))));
125
92
  }
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ const NO_SUBCOMMANDS_VERBS = [
30
30
  "open",
31
31
  "init",
32
32
  "mcp",
33
+ "skills",
33
34
  "dev",
34
35
  "deploy",
35
36
  "diff",
@@ -1,4 +1,4 @@
1
- import { agentSupportsHttpMcp, agentSupportsProjectMcp, detectInstalledAgents, getAgentDisplayName, listMcpAgentIds, resolveAddMcpAgentId, tryResolveAddMcpAgentId } from "../mcp/agents.js";
1
+ import { agentSupportsHttpMcp, agentSupportsProjectMcp, detectInstalledAgents, getAgentDisplayName, listMcpAgentIds, resolveAddMcpAgentId, tryResolveAddMcpAgentId, uniqueAgentIds } from "../mcp/agents.js";
2
2
  import { agents } from "add-mcp";
3
3
  //#region src/init/agents.ts
4
4
  const SKILLS_AGENT_BY_TYPE = {
@@ -48,4 +48,4 @@ function skillsPickerOptions() {
48
48
  }));
49
49
  }
50
50
  //#endregion
51
- export { agentPickerHint, agentSupportsHttpMcp, agentSupportsProjectMcp, detectInstalledAgents, getAgentDisplayName, getSkillsAgentName, listMcpAgentIds, mcpPickerOptions, resolveAddMcpAgentId, skillsPickerOptions, supportsSkills, tryResolveAddMcpAgentId };
51
+ export { agentPickerHint, agentSupportsHttpMcp, agentSupportsProjectMcp, detectInstalledAgents, getAgentDisplayName, getSkillsAgentName, listMcpAgentIds, mcpPickerOptions, resolveAddMcpAgentId, skillsPickerOptions, supportsSkills, tryResolveAddMcpAgentId, uniqueAgentIds };
@@ -0,0 +1,101 @@
1
+ //#region src/skills/catalog.ts
2
+ const AGENT_SKILLS_SOURCE = "neondatabase/agent-skills";
3
+ const PLATFORMS_SKILLS_SOURCE = "neondatabase/neon-for-agent-platforms";
4
+ const NEON_SKILL_CATALOG = [
5
+ {
6
+ skill: "claimable-postgres",
7
+ source: AGENT_SKILLS_SOURCE,
8
+ defaultSelected: true
9
+ },
10
+ {
11
+ skill: "neon",
12
+ source: AGENT_SKILLS_SOURCE,
13
+ defaultSelected: true
14
+ },
15
+ {
16
+ skill: "neon-ai-gateway",
17
+ source: AGENT_SKILLS_SOURCE,
18
+ defaultSelected: true
19
+ },
20
+ {
21
+ skill: "neon-functions",
22
+ source: AGENT_SKILLS_SOURCE,
23
+ defaultSelected: true
24
+ },
25
+ {
26
+ skill: "neon-object-storage",
27
+ source: AGENT_SKILLS_SOURCE,
28
+ defaultSelected: true
29
+ },
30
+ {
31
+ skill: "neon-postgres",
32
+ source: AGENT_SKILLS_SOURCE,
33
+ defaultSelected: true
34
+ },
35
+ {
36
+ skill: "neon-postgres-branches",
37
+ source: AGENT_SKILLS_SOURCE,
38
+ defaultSelected: true
39
+ },
40
+ {
41
+ skill: "neon-postgres-egress-optimizer",
42
+ source: AGENT_SKILLS_SOURCE,
43
+ defaultSelected: true
44
+ },
45
+ {
46
+ skill: "neon-postgres-agent-platforms",
47
+ source: PLATFORMS_SKILLS_SOURCE,
48
+ defaultSelected: false
49
+ }
50
+ ];
51
+ const catalogBySkill = () => {
52
+ const map = /* @__PURE__ */ new Map();
53
+ for (const entry of NEON_SKILL_CATALOG) {
54
+ if (map.has(entry.skill)) throw new Error(`Duplicate catalog skill: ${entry.skill}`);
55
+ map.set(entry.skill, entry);
56
+ }
57
+ return map;
58
+ };
59
+ const CATALOG_BY_SKILL = catalogBySkill();
60
+ const listSkillIds = () => NEON_SKILL_CATALOG.map((entry) => entry.skill);
61
+ const defaultSkillEntries = () => NEON_SKILL_CATALOG.filter((entry) => entry.defaultSelected);
62
+ function resolveSkillId(raw) {
63
+ if (raw === "*") throw new Error("neon skills does not accept --skill *. Pass --skill <name> for each skill, or omit --skill to install the default skills with -y.");
64
+ const entry = CATALOG_BY_SKILL.get(raw);
65
+ if (entry === void 0) throw new Error(`Unknown skill: "${raw}". Supported skills: ${listSkillIds().join(", ")}`);
66
+ return entry;
67
+ }
68
+ function uniqueSkillEntries(entries) {
69
+ const seen = /* @__PURE__ */ new Set();
70
+ const out = [];
71
+ for (const entry of entries) {
72
+ if (seen.has(entry.skill)) continue;
73
+ seen.add(entry.skill);
74
+ out.push(entry);
75
+ }
76
+ return out;
77
+ }
78
+ const invocationsForSelection = (selected) => {
79
+ const unique = uniqueSkillEntries(selected);
80
+ if (unique.length === 0) throw new Error("No skills selected. Pass -y to install the default skills, or --skill <name>.");
81
+ const bySource = /* @__PURE__ */ new Map();
82
+ const sourceOrder = [];
83
+ for (const entry of unique) {
84
+ const list = bySource.get(entry.source);
85
+ if (list === void 0) {
86
+ bySource.set(entry.source, [entry.skill]);
87
+ sourceOrder.push(entry.source);
88
+ } else list.push(entry.skill);
89
+ }
90
+ return sourceOrder.map((source) => {
91
+ const skills = bySource.get(source);
92
+ if (skills === void 0 || skills.length === 0) throw new Error("skills add needs at least one --skill.");
93
+ return {
94
+ source,
95
+ skills
96
+ };
97
+ });
98
+ };
99
+ const yesInstallInvocations = () => invocationsForSelection(defaultSkillEntries());
100
+ //#endregion
101
+ export { AGENT_SKILLS_SOURCE, NEON_SKILL_CATALOG, PLATFORMS_SKILLS_SOURCE, defaultSkillEntries, invocationsForSelection, listSkillIds, resolveSkillId, uniqueSkillEntries, yesInstallInvocations };
@@ -0,0 +1,57 @@
1
+ import { tryResolveAddMcpAgentId } from "../mcp/agents.js";
2
+ import "../init/agents.js";
3
+ import { agentChoicesFrom, pickAgentsInteractively, resolveAgentSelection } from "../utils/agent_picker.js";
4
+ import { invocationsForSelection, resolveSkillId, uniqueSkillEntries, yesInstallInvocations } from "./catalog.js";
5
+ import { detectSkillsAgents, skillsInstallableAgents } from "./targets.js";
6
+ import { pickSkillsInteractively } from "./wizard.js";
7
+ //#region src/skills/plan.ts
8
+ const assertSkillsCanRun = (options) => {
9
+ if (options.action === "update") {
10
+ if (options.yes || options.interactive) return;
11
+ throw new Error("No interactive terminal. Pass -y to update installed skills.");
12
+ }
13
+ if (options.yes || options.interactive || options.hasSkills === true) return;
14
+ throw new Error("No interactive terminal. Pass -y to install the default skills, or --skill <name>.");
15
+ };
16
+ async function resolveSkillsPlan(options) {
17
+ assertSkillsCanRun({
18
+ yes: options.yes,
19
+ interactive: options.interactive,
20
+ action: "install",
21
+ hasSkills: options.skills.length > 0
22
+ });
23
+ const specified = uniqueSkillEntries(options.skills.map(resolveSkillId));
24
+ const prompt = options.interactive && !options.yes;
25
+ const scope = options.global ? "global" : "project";
26
+ const available = skillsInstallableAgents();
27
+ const detected = await detectSkillsAgents({
28
+ scope,
29
+ cwd: options.cwd
30
+ });
31
+ const selected = await resolveAgentSelection({
32
+ specified: options.agents,
33
+ choices: agentChoicesFrom(available, detected),
34
+ detected,
35
+ message: "Which coding agents should get Neon agent skills? (space to toggle, enter to confirm)",
36
+ nonInteractiveMessage: scope === "project" ? `No coding agents detected in this project. Pass --agent <name>. Supported agents: ${available.join(", ")}` : `No coding agents detected. Pass --agent <name>. Supported agents: ${available.join(", ")}`,
37
+ resolveSpecified: (raw) => {
38
+ if (raw === "*") throw new Error("neon skills does not accept --agent *. Pass --agent <name> for each coding agent, or omit --agent to use detected agents.");
39
+ const id = tryResolveAddMcpAgentId(raw);
40
+ if (!id) throw new Error(`Unknown agent: "${raw}". Supported agents: ${available.join(", ")}`);
41
+ return id;
42
+ },
43
+ pick: prompt ? options.pickAgents ?? pickAgentsInteractively : void 0,
44
+ interactive: prompt
45
+ });
46
+ const agents = selected.filter((id) => available.includes(id));
47
+ const skipped = selected.filter((id) => !available.includes(id));
48
+ if (agents.length === 0) throw new Error(`None of the selected agents can install skills. Supported agents: ${available.join(", ")}`);
49
+ return {
50
+ scope,
51
+ agents,
52
+ skipped,
53
+ invocations: specified.length > 0 ? invocationsForSelection(specified) : options.yes ? yesInstallInvocations() : invocationsForSelection(await (options.pickSkills ?? pickSkillsInteractively)())
54
+ };
55
+ }
56
+ //#endregion
57
+ export { assertSkillsCanRun, resolveSkillsPlan };
@@ -0,0 +1,119 @@
1
+ import pkg_default from "../pkg.js";
2
+ import { execa } from "execa";
3
+ //#region src/skills/run.ts
4
+ const TELEMETRY_BLOCKERS = ["DISABLE_TELEMETRY", "DO_NOT_TRACK"];
5
+ const skillsMetadata = (command) => JSON.stringify({
6
+ origin: "neon-cli",
7
+ command,
8
+ version: pkg_default.version
9
+ });
10
+ const skillsChildEnv = (base = process.env) => {
11
+ const env = { ...base };
12
+ for (const key of TELEMETRY_BLOCKERS) delete env[key];
13
+ for (const key of Object.keys(env)) if (key.toUpperCase() === "NEON_API_KEY") delete env[key];
14
+ return env;
15
+ };
16
+ const SKILLS_MIN_NODE = "22.20.0";
17
+ const nodeParts = (version) => {
18
+ const [major, minor, patch] = version.replace(/^v/, "").split(".").map((part) => Number.parseInt(part, 10));
19
+ return [
20
+ Number.isInteger(major) ? major : 0,
21
+ Number.isInteger(minor) ? minor : 0,
22
+ Number.isInteger(patch) ? patch : 0
23
+ ];
24
+ };
25
+ const nodeMeetsMinimum = (current, minimum) => {
26
+ const left = nodeParts(current);
27
+ const right = nodeParts(minimum);
28
+ for (let i = 0; i < 3; i += 1) {
29
+ const a = left[i];
30
+ const b = right[i];
31
+ if (a === void 0 || b === void 0) return false;
32
+ if (a !== b) return a > b;
33
+ }
34
+ return true;
35
+ };
36
+ const assertSkillsNode = (version = process.version) => {
37
+ if (nodeMeetsMinimum(version, "22.20.0")) return;
38
+ throw new Error(`neon skills needs Node.js ${SKILLS_MIN_NODE} or newer to run the skills CLI. This process is Node.js ${version.replace(/^v/, "")}. Upgrade Node.js, then retry.`);
39
+ };
40
+ const skillsAddArgs = (options) => {
41
+ if (options.skills.length === 0) throw new Error("skills add needs at least one --skill.");
42
+ const args = [
43
+ "-y",
44
+ "skills",
45
+ "add",
46
+ options.source
47
+ ];
48
+ for (const skill of options.skills) args.push("--skill", skill);
49
+ for (const agent of options.agents) args.push("--agent", agent);
50
+ if (options.global) args.push("-g");
51
+ args.push("-y", "--metadata", options.metadata);
52
+ return args;
53
+ };
54
+ const neonSkillsRetryCommand = (options) => {
55
+ const args = ["skills"];
56
+ for (const skill of options.skills) args.push("-s", skill);
57
+ for (const agent of options.agents) args.push("--agent", agent);
58
+ if (options.global) args.push("--global");
59
+ args.push("-y");
60
+ return `neon ${args.map(quoteNpxArg).join(" ")}`;
61
+ };
62
+ const neonSkillsUpdateRetryCommand = (global) => global ? "neon skills update --global -y" : "neon skills update -y";
63
+ const quoteNpxArg = (part) => /[\s"'\\*]/.test(part) ? `'${part.replace(/'/g, `'\\''`)}'` : part;
64
+ const npxCommand = (args) => `npx ${args.map(quoteNpxArg).join(" ")}`;
65
+ const ANSI = /\u001b\[[0-9;]*[A-Za-z]/g;
66
+ const UPDATE_BANNER = /^Checking for skill updates/i;
67
+ const UPDATE_NOTHING = /No (?:project|global) skills (?:to update|to check|tracked in lock file|can be updated in place)|All global skills are up to date/i;
68
+ const UPDATE_RESULT = /No (?:project|global) skills (?:to update|to check|tracked in lock file|can be updated in place)\.?|All global skills are up to date|(?:✓\s*)?Updated \d+ skills?(?:\(s\))?|(?:✗\s*)?Failed to update \d+ skills?(?:\(s\))?/i;
69
+ const stripAnsi = (text) => text.replace(ANSI, "").replace(/\r/g, "");
70
+ const skillsUpdateHadNothing = (output) => UPDATE_NOTHING.test(stripAnsi(output));
71
+ const skillsUpdateDetail = (output) => {
72
+ const text = stripAnsi(output);
73
+ let last;
74
+ for (const line of text.split("\n")) {
75
+ const trimmed = line.trim();
76
+ if (UPDATE_RESULT.test(trimmed)) last = trimmed;
77
+ }
78
+ if (last !== void 0) return last;
79
+ for (const line of text.split("\n").reverse()) {
80
+ const trimmed = line.trim();
81
+ if (trimmed.length === 0 || UPDATE_BANNER.test(trimmed)) continue;
82
+ return trimmed;
83
+ }
84
+ };
85
+ const skillsUpdateArgs = (options) => [
86
+ "-y",
87
+ "skills",
88
+ "update",
89
+ options.global ? "-g" : "-p",
90
+ "-y"
91
+ ];
92
+ const runSkillsCli = async (options) => {
93
+ assertSkillsNode();
94
+ try {
95
+ const result = await execa("npx", options.args, {
96
+ cwd: options.cwd,
97
+ env: skillsChildEnv(),
98
+ extendEnv: false,
99
+ stdio: "pipe",
100
+ timeout: 12e4
101
+ });
102
+ return {
103
+ stdout: result.stdout,
104
+ stderr: result.stderr
105
+ };
106
+ } catch (error) {
107
+ if (isCommandMissing(error)) throw new Error("neon skills needs npx (Node.js) to run the skills CLI. Install Node.js, then retry.");
108
+ if (isExecaFailure(error)) {
109
+ const childOut = [error.stderr, error.stdout].filter((part) => typeof part === "string" && part.length > 0).join("\n");
110
+ const detail = childOut.length > 0 ? childOut : error.shortMessage;
111
+ throw new Error(detail.length > 0 ? `skills CLI failed:\n${detail}` : "skills CLI failed.");
112
+ }
113
+ throw error;
114
+ }
115
+ };
116
+ const isCommandMissing = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
117
+ const isExecaFailure = (error) => typeof error === "object" && error !== null && "shortMessage" in error && typeof error.shortMessage === "string";
118
+ //#endregion
119
+ export { SKILLS_MIN_NODE, assertSkillsNode, neonSkillsRetryCommand, neonSkillsUpdateRetryCommand, nodeMeetsMinimum, npxCommand, quoteNpxArg, runSkillsCli, skillsAddArgs, skillsChildEnv, skillsMetadata, skillsUpdateArgs, skillsUpdateDetail, skillsUpdateHadNothing };
@@ -0,0 +1,23 @@
1
+ import { detectInstalledAgents, listMcpAgentIds, uniqueAgentIds } from "../mcp/agents.js";
2
+ import { getSkillsAgentName, supportsSkills } from "../init/agents.js";
3
+ import { detectProjectAgents } from "add-mcp";
4
+ //#region src/skills/targets.ts
5
+ const skillsInstallableAgents = () => listMcpAgentIds().filter((id) => supportsSkills(id));
6
+ const detectSkillsAgents = async (options) => {
7
+ const detected = options.scope === "project" ? detectProjectAgents(options.cwd) : await detectInstalledAgents();
8
+ return uniqueAgentIds(detected).filter((id) => supportsSkills(id));
9
+ };
10
+ const mappedSkillsAgentNames = (agents) => {
11
+ const names = [];
12
+ const seen = /* @__PURE__ */ new Set();
13
+ for (const agent of agents) {
14
+ const name = getSkillsAgentName(agent);
15
+ if (name === void 0 || seen.has(name)) continue;
16
+ seen.add(name);
17
+ names.push(name);
18
+ }
19
+ if (names.length === 0) throw new Error(`None of the selected agents can install skills. Supported agents: ${skillsInstallableAgents().join(", ")}`);
20
+ return names;
21
+ };
22
+ //#endregion
23
+ export { detectSkillsAgents, mappedSkillsAgentNames, skillsInstallableAgents };
@@ -0,0 +1,73 @@
1
+ import { getAgentDisplayName } from "../mcp/agents.js";
2
+ import "../init/agents.js";
3
+ import { canPickAgentsInteractively } from "../utils/agent_picker.js";
4
+ import { NEON_SKILL_CATALOG } from "./catalog.js";
5
+ import prompts from "prompts";
6
+ //#region src/skills/wizard.ts
7
+ const restoreCursorOnAbort = (state) => {
8
+ if (state.aborted) {
9
+ process.stdout.write("\x1B[?25h");
10
+ process.stdout.write("\n");
11
+ process.exit(1);
12
+ }
13
+ };
14
+ const pickSkillsInteractively = async () => {
15
+ if (!canPickAgentsInteractively()) throw new Error("No interactive terminal. Pass -y to install the default skills, or --skill <name>.");
16
+ const { skills } = await prompts({
17
+ onState: restoreCursorOnAbort,
18
+ type: "multiselect",
19
+ name: "skills",
20
+ message: "Which Neon agent skills should be installed? (space to toggle, enter to confirm)",
21
+ instructions: false,
22
+ min: 1,
23
+ choices: NEON_SKILL_CATALOG.map((entry) => ({
24
+ value: entry,
25
+ title: entry.skill,
26
+ selected: entry.defaultSelected
27
+ }))
28
+ });
29
+ if (!Array.isArray(skills) || skills.length === 0) throw new Error("No skills selected. Pass -y to install the default skills, or --skill <name>.");
30
+ return skills.filter(isSkillEntry);
31
+ };
32
+ const isSkillEntry = (value) => {
33
+ if (typeof value !== "object" || value === null) return false;
34
+ if (!("source" in value) || !("skill" in value)) return false;
35
+ return typeof value.source === "string" && typeof value.skill === "string" && value.source.length > 0 && value.skill.length > 0;
36
+ };
37
+ const skillsInstallSummary = (options) => {
38
+ const skills = options.invocations.flatMap((invocation) => invocation.skills).join(", ");
39
+ const rows = [
40
+ ["Config", options.scope === "project" ? "this directory" : "user-level"],
41
+ ["Agents", options.agents.map(getAgentDisplayName).join(", ")],
42
+ ["Skills", skills]
43
+ ];
44
+ const labelWidth = Math.max(...rows.map(([label]) => label.length));
45
+ return rows.map(([label, value]) => `${label.padEnd(labelWidth)} ${value}`).join("\n");
46
+ };
47
+ const confirmSkillsInstall = async (options) => {
48
+ if (!canPickAgentsInteractively()) return true;
49
+ process.stdout.write(`\n${skillsInstallSummary(options)}\n\n`);
50
+ const { ok } = await prompts({
51
+ onState: restoreCursorOnAbort,
52
+ type: "confirm",
53
+ name: "ok",
54
+ message: "Write Neon agent skills into these agents?",
55
+ initial: true
56
+ });
57
+ return ok === true;
58
+ };
59
+ const confirmSkillsUpdate = async (options) => {
60
+ if (!canPickAgentsInteractively()) return true;
61
+ const config = options.scope === "project" ? "this directory" : "user-level";
62
+ process.stdout.write(`\nConfig ${config}\n\n`);
63
+ const { ok } = await prompts({
64
+ onState: restoreCursorOnAbort,
65
+ type: "confirm",
66
+ name: "ok",
67
+ message: "Update installed skills to the latest versions?",
68
+ initial: true
69
+ });
70
+ return ok === true;
71
+ };
72
+ //#endregion
73
+ export { confirmSkillsInstall, confirmSkillsUpdate, pickSkillsInteractively, skillsInstallSummary };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",