neon 4.2.1 → 4.3.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 +37 -4
- package/dist/commands/api_keys.js +3 -3
- package/dist/commands/auth.js +2 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/mcp.js +14 -8
- package/dist/commands/skills.js +223 -0
- package/dist/context.js +3 -1
- package/dist/index.js +1 -0
- package/dist/init/agents.js +2 -2
- package/dist/mcp/mint.js +37 -13
- package/dist/mcp/plan.js +1 -1
- package/dist/mcp/wizard.js +3 -3
- package/dist/skills/catalog.js +101 -0
- package/dist/skills/plan.js +57 -0
- package/dist/skills/run.js +119 -0
- package/dist/skills/targets.js +23 -0
- package/dist/skills/wizard.js +73 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -736,7 +736,7 @@ $ neon mcp -y
|
|
|
736
736
|
# OAuth: no API key minted. The agent prompts for Neon sign-in on first use.
|
|
737
737
|
$ neon mcp --oauth
|
|
738
738
|
|
|
739
|
-
# Project-level config. A minted key is still account-wide.
|
|
739
|
+
# Project-level config. A minted key is still account-wide unless a project is pinned.
|
|
740
740
|
$ neon mcp --project
|
|
741
741
|
|
|
742
742
|
$ neon mcp --agent cursor --agent claude-code
|
|
@@ -744,7 +744,7 @@ $ neon mcp --agent cursor --agent claude-code
|
|
|
744
744
|
# Hide write tools. Does not change the minted key.
|
|
745
745
|
$ neon mcp --read-only
|
|
746
746
|
|
|
747
|
-
# Pin MCP tools to one project.
|
|
747
|
+
# Pin MCP tools to one project. A newly minted API key is limited to that project.
|
|
748
748
|
$ neon mcp --project-id <project-id>
|
|
749
749
|
|
|
750
750
|
# Limit which tool categories are visible.
|
|
@@ -753,11 +753,43 @@ $ neon mcp --category querying --category schema
|
|
|
753
753
|
|
|
754
754
|
On a TTY the command asks for config location (global is the default), then agents, then API key vs OAuth, then a summary to confirm before it writes. Detected agents start selected: globally installed agents or project-folder markers such as `.cursor` when the install is project.
|
|
755
755
|
|
|
756
|
-
`-y` skips those questions. `--project`, `--oauth` and `--agent` skip the question they answer and still apply with `-y`. `--read-only` and `--category` are flags only and are never prompted. A linked project-folder install asks whether to pin MCP tools to that `.neon` project (`?projectId=`). An unlinked project folder does not ask. Global installs never add that param unless you pass `--project-id`. Without a TTY, pass `-y` to mint into every detected agent, `--agent` to name the agents or `--oauth` to write the URL only.
|
|
756
|
+
`-y` skips those questions. `--project`, `--oauth` and `--agent` skip the question they answer and still apply with `-y`. `--read-only` and `--category` are flags only and are never prompted. A linked project-folder install asks whether to pin MCP tools to that `.neon` project (`?projectId=`). If you pin and selected API-key auth, the minted key is limited to that project too. An unlinked project folder does not ask. Global installs never add that param unless you pass `--project-id`. `-y` does not infer a project from `.neon`. Without a TTY, pass `-y` to mint into every detected agent, `--agent` to name the agents or `--oauth` to write the URL only.
|
|
757
757
|
|
|
758
758
|
`--agent` names: `antigravity`, `cline`, `cline-cli`, `claude-code`, `codex`, `cursor`, `gemini-cli`, `goose`, `github-copilot-cli`, `grok-build`, `mcporter`, `opencode`, `vscode`, `windsurf`, `zed`. Project installs drop `antigravity`, `cline`, `cline-cli`, `goose` and `windsurf`. `claude-desktop` is a known name that is then skipped.
|
|
759
759
|
|
|
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
|
|
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=` and, when a key is minted, limits that key to the named project. Accepting the linked-project pin does the same. Revoke a project-scoped key with `neon api-keys revoke <id> --org-id <org>`. A reused Bearer keeps the scope it already has. `--category` adds `?category=` (repeatable or comma-separated: `projects`, `branches`, `schema`, `querying`, `neon_auth`, `data_api`, `observability`, `docs`). `--read-only` and `--category` restrict which MCP tools the server exposes; they do not change what the minted key can do.
|
|
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.
|
|
761
793
|
|
|
762
794
|
## Snapshots (`snapshots`)
|
|
763
795
|
|
|
@@ -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
|
|
|
@@ -265,16 +265,16 @@ const revoke = async (props) => {
|
|
|
265
265
|
* scoped key — the endpoint that accepts `project_id` is org-only — so that case fails here
|
|
266
266
|
* with the reason, rather than as a 404 from a URL the user never typed.
|
|
267
267
|
*/
|
|
268
|
-
const orgIdForProject = async (client, projectId) => {
|
|
268
|
+
const orgIdForProject = async (client, projectId, usage = "api-keys") => {
|
|
269
269
|
let orgId;
|
|
270
270
|
try {
|
|
271
271
|
const { data: { project } } = await client.getProject(projectId);
|
|
272
272
|
orgId = project.org_id;
|
|
273
273
|
} catch (err) {
|
|
274
|
-
if (isNeonApiError(err) && err.status === 404) throw new Error(projectId.startsWith("org-") ? `Project ${projectId} not found. That looks like an organization id. Pass it as --org-id instead.` : `Project ${projectId} not found. Check the id with \`neon projects list\`.`);
|
|
274
|
+
if (isNeonApiError(err) && err.status === 404) throw new Error(projectId.startsWith("org-") ? usage === "mcp" ? `Project ${projectId} not found. That looks like an organization id. neon mcp takes a project id on --project-id.` : `Project ${projectId} not found. That looks like an organization id. Pass it as --org-id instead.` : `Project ${projectId} not found. Check the id with \`neon projects list\`.`);
|
|
275
275
|
throw err;
|
|
276
276
|
}
|
|
277
|
-
if (!orgId) throw new Error(`Project ${projectId} does not belong to an organization, so it cannot have a project-scoped API key. Omit --project-id to create an account key.`);
|
|
277
|
+
if (!orgId) throw new Error(usage === "mcp" ? `Project ${projectId} does not belong to an organization, so it cannot have a project-scoped API key. Pass --oauth to pin tools without minting, or omit --project-id to mint an account-wide key.` : `Project ${projectId} does not belong to an organization, so it cannot have a project-scoped API key. Omit --project-id to create an account key.`);
|
|
278
278
|
return orgId;
|
|
279
279
|
};
|
|
280
280
|
//#endregion
|
package/dist/commands/auth.js
CHANGED
|
@@ -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";
|
package/dist/commands/index.js
CHANGED
|
@@ -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,
|
package/dist/commands/mcp.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getCliName } from "../utils/cli_name.js";
|
|
|
5
5
|
import { writer } from "../writer.js";
|
|
6
6
|
import { noPassthrough, single } from "../utils/flags.js";
|
|
7
7
|
import { existingNeonApiKey, installNeonMcpServer, neonMcpUrl, parseMcpCategories, trackedProjectMcpConfig } from "../mcp/install.js";
|
|
8
|
-
import { mintMcpApiKey, withdrawMintedKey } from "../mcp/mint.js";
|
|
8
|
+
import { mintMcpApiKey, mintedKeyRevokeCommand, withdrawMintedKey } from "../mcp/mint.js";
|
|
9
9
|
import { canPickAgentsInteractively } from "../utils/agent_picker.js";
|
|
10
10
|
import { resolveInstallTargets } from "../mcp/targets.js";
|
|
11
11
|
import { confirmMcpInstall } from "../mcp/wizard.js";
|
|
@@ -28,7 +28,7 @@ const builder = (argv) => argv.usage("$0 mcp [options]").options({
|
|
|
28
28
|
project: {
|
|
29
29
|
type: "boolean",
|
|
30
30
|
default: false,
|
|
31
|
-
describe: "Write project-level MCP config. Skips the config-location question.
|
|
31
|
+
describe: "Write project-level MCP config. Skips the config-location question. A linked project-folder install may still pin a project and scope a newly minted key"
|
|
32
32
|
},
|
|
33
33
|
yes: {
|
|
34
34
|
alias: "y",
|
|
@@ -59,7 +59,7 @@ const builder = (argv) => argv.usage("$0 mcp [options]").options({
|
|
|
59
59
|
},
|
|
60
60
|
"project-id": {
|
|
61
61
|
type: "string",
|
|
62
|
-
describe: "Pin MCP tools to one Neon project (?projectId=).
|
|
62
|
+
describe: "Pin MCP tools to one Neon project (?projectId=). A newly minted API key is limited to that project. A linked project-folder install asks the same when you pick API-key auth",
|
|
63
63
|
coerce: single("project-id")
|
|
64
64
|
},
|
|
65
65
|
category: {
|
|
@@ -131,7 +131,8 @@ const handler = async (props) => {
|
|
|
131
131
|
skipped,
|
|
132
132
|
auth: plan.auth,
|
|
133
133
|
reuse: existing !== void 0,
|
|
134
|
-
url
|
|
134
|
+
url,
|
|
135
|
+
mintProjectId: plan.urlProjectId
|
|
135
136
|
})) {
|
|
136
137
|
log.info("Aborted. Nothing was written.");
|
|
137
138
|
return;
|
|
@@ -146,13 +147,18 @@ const handler = async (props) => {
|
|
|
146
147
|
apiKey: existing
|
|
147
148
|
};
|
|
148
149
|
log.info("Reusing the API key already configured for the Neon MCP server.");
|
|
150
|
+
if (plan.urlProjectId) log.warning("That key keeps its existing scope. Remove the Neon MCP entry to mint a project-scoped key, or pass --oauth to pin tools without a key.");
|
|
149
151
|
} else {
|
|
150
|
-
minted = await mintMcpApiKey({
|
|
152
|
+
minted = await mintMcpApiKey({
|
|
153
|
+
apiClient: props.apiClient,
|
|
154
|
+
projectId: plan.urlProjectId
|
|
155
|
+
});
|
|
151
156
|
auth = {
|
|
152
157
|
kind: "api-key",
|
|
153
158
|
apiKey: minted.key
|
|
154
159
|
};
|
|
155
|
-
log.
|
|
160
|
+
if (minted.projectId) log.info("Limited to %s: it cannot create projects, mint API keys, or read any other project. It can still change and delete everything inside that project.", minted.projectId);
|
|
161
|
+
else log.warning("This key reaches everything your account can, in every organization.");
|
|
156
162
|
}
|
|
157
163
|
const rows = [];
|
|
158
164
|
let successes = 0;
|
|
@@ -189,7 +195,7 @@ const handler = async (props) => {
|
|
|
189
195
|
if (successes === 0) {
|
|
190
196
|
if (minted && props.apiClient) {
|
|
191
197
|
const withdrawn = await withdrawMintedKey(props.apiClient, minted);
|
|
192
|
-
throw new Error(withdrawn ? "Failed to write Neon MCP config to any agent. The minted API key has been revoked." : `Failed to write Neon MCP config to any agent. The minted API key could NOT be revoked. Remove it with \`${
|
|
198
|
+
throw new Error(withdrawn ? "Failed to write Neon MCP config to any agent. The minted API key has been revoked." : `Failed to write Neon MCP config to any agent. The minted API key could NOT be revoked. Remove it with \`${mintedKeyRevokeCommand(minted)}\`.`);
|
|
193
199
|
}
|
|
194
200
|
throw new Error("Failed to write Neon MCP config to any agent.");
|
|
195
201
|
}
|
|
@@ -204,7 +210,7 @@ const handler = async (props) => {
|
|
|
204
210
|
title: "MCP"
|
|
205
211
|
});
|
|
206
212
|
out.end();
|
|
207
|
-
if (minted) log.info("Minted API key %s (id %d,
|
|
213
|
+
if (minted) log.info("Minted API key %s (id %d, %s). Revoke with: %s", minted.name, minted.id, minted.projectId ? "project" : "account", mintedKeyRevokeCommand(minted));
|
|
208
214
|
if (plan.auth === "oauth") log.info("The agent will prompt for Neon sign-in on first use.");
|
|
209
215
|
if (failedAgents.length > 0) throw new Error(`Failed to write Neon MCP config for: ${failedAgents.join(", ")}.`);
|
|
210
216
|
};
|
|
@@ -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 };
|
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 };
|
package/dist/index.js
CHANGED
package/dist/init/agents.js
CHANGED
|
@@ -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 };
|
package/dist/mcp/mint.js
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
import { log } from "../log.js";
|
|
2
2
|
import { isNeonApiError } from "../api.js";
|
|
3
3
|
import { getCliName } from "../utils/cli_name.js";
|
|
4
|
+
import { orgIdForProject } from "../commands/api_keys.js";
|
|
4
5
|
import { mintedKeyName } from "../profile_keys.js";
|
|
5
6
|
//#region src/mcp/mint.ts
|
|
6
7
|
const cannotMintMessage = "This CLI credential cannot mint API keys. Organization and project-scoped keys cannot create other keys. Sign in with `neon auth` or pass a personal API key.";
|
|
7
|
-
|
|
8
|
+
function mintedKeyRevokeCommand(key) {
|
|
9
|
+
return key.orgId ? `${getCliName()} api-keys revoke ${key.id} --org-id ${key.orgId}` : `${getCliName()} api-keys revoke ${key.id}`;
|
|
10
|
+
}
|
|
11
|
+
const withdraw = async (client, keyId, orgId) => {
|
|
8
12
|
if (!Number.isSafeInteger(keyId) || keyId === void 0 || keyId <= 0) return false;
|
|
9
13
|
try {
|
|
10
|
-
const { data } = await client.revokeApiKey(keyId);
|
|
14
|
+
const { data } = orgId ? await client.revokeOrgApiKey(orgId, keyId) : await client.revokeApiKey(keyId);
|
|
11
15
|
return data.revoked === true && data.id === keyId;
|
|
12
16
|
} catch (err) {
|
|
13
17
|
log.error("Failed to revoke API key %d: %s", keyId, err instanceof Error ? err.message : String(err));
|
|
14
18
|
return false;
|
|
15
19
|
}
|
|
16
20
|
};
|
|
17
|
-
const assertUsable = async (client, data, fallbackName) => {
|
|
18
|
-
const
|
|
21
|
+
const assertUsable = async (client, data, fallbackName, scope) => {
|
|
22
|
+
const wanted = scope.projectId;
|
|
23
|
+
const problem = typeof data.key !== "string" || data.key.trim() === "" ? "Neon returned no key." : data.project_id !== wanted ? wanted === void 0 ? `Neon returned a key scoped to ${data.project_id} rather than the whole account.` : `Neon returned a key scoped to ${data.project_id ?? "nothing"} rather than ${wanted}.` : !Number.isSafeInteger(data.id) ? "Neon returned no key id." : null;
|
|
19
24
|
if (problem) {
|
|
20
|
-
const withdrawn = await withdraw(client, data.id);
|
|
21
|
-
throw new Error(`${problem} ${withdrawn ? "The key has been revoked; nothing was issued." : `The key could NOT be revoked and may still be live${data.id === void 0 ? "" : `. Remove it with \`${
|
|
25
|
+
const withdrawn = await withdraw(client, data.id, scope.orgId);
|
|
26
|
+
throw new Error(`${problem} ${withdrawn ? "The key has been revoked; nothing was issued." : `The key could NOT be revoked and may still be live${data.id === void 0 ? "" : `. Remove it with \`${mintedKeyRevokeCommand({
|
|
27
|
+
id: data.id,
|
|
28
|
+
orgId: scope.orgId
|
|
29
|
+
})}\``}.`}`);
|
|
22
30
|
}
|
|
23
31
|
const id = data.id;
|
|
24
32
|
const key = data.key;
|
|
@@ -26,21 +34,37 @@ const assertUsable = async (client, data, fallbackName) => {
|
|
|
26
34
|
return {
|
|
27
35
|
id,
|
|
28
36
|
name: data.name ?? fallbackName,
|
|
29
|
-
key
|
|
37
|
+
key,
|
|
38
|
+
orgId: scope.orgId,
|
|
39
|
+
projectId: scope.projectId
|
|
30
40
|
};
|
|
31
41
|
};
|
|
32
|
-
async
|
|
33
|
-
const name = mintedKeyName("mcp");
|
|
42
|
+
const createAndAssert = async (client, create, name, scope) => {
|
|
34
43
|
try {
|
|
35
|
-
const { data } = await
|
|
36
|
-
return assertUsable(
|
|
44
|
+
const { data } = await create();
|
|
45
|
+
return assertUsable(client, data, name, scope);
|
|
37
46
|
} catch (err) {
|
|
38
47
|
if (isNeonApiError(err) && (err.status === 403 || err.status === 404)) throw new Error(cannotMintMessage);
|
|
39
48
|
throw err;
|
|
40
49
|
}
|
|
50
|
+
};
|
|
51
|
+
async function mintMcpApiKey(options) {
|
|
52
|
+
const name = mintedKeyName("mcp");
|
|
53
|
+
const projectId = options.projectId;
|
|
54
|
+
if (projectId !== void 0) {
|
|
55
|
+
const orgId = await orgIdForProject(options.apiClient, projectId, "mcp");
|
|
56
|
+
return createAndAssert(options.apiClient, () => options.apiClient.createOrgApiKey(orgId, {
|
|
57
|
+
key_name: name,
|
|
58
|
+
project_id: projectId
|
|
59
|
+
}), name, {
|
|
60
|
+
orgId,
|
|
61
|
+
projectId
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return createAndAssert(options.apiClient, () => options.apiClient.createApiKey({ key_name: name }), name, {});
|
|
41
65
|
}
|
|
42
66
|
async function withdrawMintedKey(apiClient, key) {
|
|
43
|
-
return withdraw(apiClient, key.id);
|
|
67
|
+
return withdraw(apiClient, key.id, key.orgId);
|
|
44
68
|
}
|
|
45
69
|
//#endregion
|
|
46
|
-
export { mintMcpApiKey, withdrawMintedKey };
|
|
70
|
+
export { mintMcpApiKey, mintedKeyRevokeCommand, withdrawMintedKey };
|
package/dist/mcp/plan.js
CHANGED
|
@@ -28,7 +28,7 @@ async function resolveMcpPlan(options) {
|
|
|
28
28
|
const auth = options.oauth ? "oauth" : prompt ? await (options.pickAuth ?? pickMcpAuth)() : "api-key";
|
|
29
29
|
let urlProjectId = options.projectId;
|
|
30
30
|
if (urlProjectId === void 0 && prompt && scope === "project" && options.linkedProjectId) {
|
|
31
|
-
if (await (options.pickProjectPin ?? pickMcpProjectPin)(options.linkedProjectId)) urlProjectId = options.linkedProjectId;
|
|
31
|
+
if (await (options.pickProjectPin ?? pickMcpProjectPin)(options.linkedProjectId, auth === "api-key")) urlProjectId = options.linkedProjectId;
|
|
32
32
|
}
|
|
33
33
|
return {
|
|
34
34
|
scope,
|
package/dist/mcp/wizard.js
CHANGED
|
@@ -51,20 +51,20 @@ const pickMcpAuth = async () => {
|
|
|
51
51
|
if (auth !== "api-key" && auth !== "oauth") throw new Error("Aborted.");
|
|
52
52
|
return auth;
|
|
53
53
|
};
|
|
54
|
-
const pickMcpProjectPin = async (linkedProjectId) => {
|
|
54
|
+
const pickMcpProjectPin = async (linkedProjectId, willMintKey = false) => {
|
|
55
55
|
if (!canPickAgentsInteractively()) return false;
|
|
56
56
|
const { pin } = await prompts({
|
|
57
57
|
onState: restoreCursorOnAbort,
|
|
58
58
|
type: "confirm",
|
|
59
59
|
name: "pin",
|
|
60
|
-
message: `Pin MCP tools to the linked project ${linkedProjectId}?`,
|
|
60
|
+
message: willMintKey ? `Pin MCP tools to the linked project ${linkedProjectId}? A newly minted API key would be limited to that project.` : `Pin MCP tools to the linked project ${linkedProjectId}?`,
|
|
61
61
|
initial: true
|
|
62
62
|
});
|
|
63
63
|
return pin === true;
|
|
64
64
|
};
|
|
65
65
|
const mcpInstallSummary = (options) => {
|
|
66
66
|
const agents = options.install.map(getAgentDisplayName).join(", ");
|
|
67
|
-
const auth = options.auth === "oauth" ? "OAuth, agent signs in on first use" : options.reuse ? "reuse the API key already in agent config" : "mint an account-wide API key that reaches every organization";
|
|
67
|
+
const auth = options.auth === "oauth" ? "OAuth, agent signs in on first use" : options.reuse ? "reuse the API key already in agent config" : options.mintProjectId ? `mint an API key limited to ${options.mintProjectId}` : "mint an account-wide API key that reaches every organization";
|
|
68
68
|
const rows = [
|
|
69
69
|
["Config", options.scope === "project" ? "this directory" : "user-level"],
|
|
70
70
|
["Agents", agents],
|
|
@@ -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.
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"yaml": "^2.9.0",
|
|
64
64
|
"yargs": "17.7.2",
|
|
65
65
|
"yoctocolors": "^2.1.2",
|
|
66
|
-
"@neon/sdk": "2.2.0",
|
|
67
66
|
"@neon/config": "1.0.2",
|
|
67
|
+
"@neon/sdk": "2.2.0",
|
|
68
68
|
"@neon/config-runtime": "1.0.2"
|
|
69
69
|
},
|
|
70
70
|
"optionalDependencies": {
|