Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devin-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "jfrog",
"displayName": "JFrog Platform",
"version": "0.3.1",
"version": "0.3.2",
"description": "Official JFrog plugin for Devin. Ships the JFrog skills bundle (platform ops, init, MCP management, AI Catalog, package safety, reference architecture, package-manager setup) and registers the JFrog Platform MCP server (remote HTTP + OAuth).",
"author": {
"name": "JFrog Ltd.",
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/sync-skills-vendor.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"repo": "jfrog/jfrog-skills",
"pin": "v0.28.0",
"pin": "v0.30.0",
"paths": [
"skills"
]
Expand Down
20 changes: 12 additions & 8 deletions skills/jfrog-ai-catalog/references/installing-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,18 @@ every choice (`--repo`, target) up front.
**Resolve `<harness>` from the environment check script — never from your model
name.** If `<UA>` is not already known from this session, run
`bash <skill_path>/../jfrog/scripts/check-environment.sh <model-slug>` now and capture
its stdout as `<UA>`. Parse the `tool=<h>` field from `<UA>` and pass it straight
through as `--harness <h>`.

If `tool` is `unknown` or empty, do **not** guess — ask the user for the
desired install path and use `--path <dir>` instead.

If the CLI rejects the harness with `unknown agent`, fall back to asking the
user for `--path <dir>`, the same as the unknown/empty case above.
its stdout as `<UA>`. Parse the `tool=<h>` field from `<UA>` and map it to a
`jf` harness name:

| `tool=` value in `<UA>` | `--harness` for `jf agent plugins` |
|-------------------------|-------------------------------------|
| `claude` | `claude` |
| `cursor` | `cursor` |
| `copilot` | `vscode` |
| `unknown`, empty, or any other | Ask the user |

If `tool` is `unknown`, empty, or not in the table — do **not** guess. Ask
the user for the desired install path and use `--path <dir>` instead.

Choose exactly one install target (these are mutually exclusive):

Expand Down
21 changes: 15 additions & 6 deletions skills/jfrog-init/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ metadata:

# /jfrog-init — verify and guide JFrog plugin readiness

**First output must be a tool call, not text.** No "I'll start..." preamble.

Walks a fixed, ordered checklist and stops at the first red result, guiding
the user through the matching fix before re-checking. Every detector in
`scripts/` is idempotent, read-only, JSON-emitting, and implemented in
Expand Down Expand Up @@ -50,8 +52,8 @@ substitutes it automatically, identically, in both this text and the
`allowed-tools` Bash rules above — write it literally rather than
resolving it yourself, so the two stay byte-for-byte consistent
regardless of install depth (see `references/script-invocation.md`). On
a harness that doesn't perform this substitution (e.g. Cursor, which
doesn't consult `allowed-tools` for approval at all — every command
a harness that doesn't perform this substitution (e.g. Cursor or Codex,
neither of which consults `allowed-tools` for approval — every command
below still raises its own prompt there), replace it with the real
absolute path of this file's directory yourself, same as before.

Expand Down Expand Up @@ -140,10 +142,12 @@ user needs to know or act on:
- **Do not** announce that you're about to run the checklist, or name
which check comes first — not even generically ("I'll run the setup
checklist silently, starting with the JFrog CLI check" is itself a
violation: it names a step while claiming to be silent). Silently
means no preamble message at all. Say nothing until you have
something the user needs to act on (an ask, a red result) or the
final summary.
violation: it names a step while claiming to be silent). The same
applies to reading reference docs: "I'll start by reading the flow
docs" is a preamble. Silently means no preamble message at all — not
before running commands, not before reading files. Say nothing until
you have something the user needs to act on (an ask, a red result)
or the final summary.

Instead:

Expand Down Expand Up @@ -251,6 +255,11 @@ Read the output yourself, no JSON to parse:
- `node --version` ≥ 18 **and** `npx --version` succeeds → **green** →
proceed to Step 2.

**Never paste the raw shell output.** Translate to plain English —
"npx is not installed" not `` `command not found` ``, "Node.js v16 is
too old" not the version string verbatim. The raw output is for your
reasoning, not for the user.

On red, **stop and read `references/node-install-prompt.md` in full
before responding to the user.** It has the exact `AskUserQuestion`
payload, the forbidden phrases, and the install commands — required
Expand Down
2 changes: 1 addition & 1 deletion skills/jfrog-init/references/flow-diagram.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ flowchart TD
S4 -->|unreachable/timeout/other| STOP2["STOP: show raw error (network/URL hint included)"]:::stopBox
S4 -->|yes| S5

S5["5. Plugin mcp.json has mcpServers.jfrog? (auto-substitutes a JFROG_PLATFORM_URL/JFROG_URL placeholder inline, if present)"]:::stepBox
S5["5. Plugin mcp.json has a valid jfrog entry? (auto-substitutes a JFROG_PLATFORM_URL/JFROG_URL placeholder inline, if present)"]:::stepBox
S5 -->|substitution needed, server-id ambiguous| ASKSRV5["AskUserQuestion: pick server-id"]:::fixBox
ASKSRV5 --> S5
S5 -->|missing/invalid/no entry, incl. substitution failure| F5["Note: reinstall or update the JFrog plugin, or resolve jf config (non-blocking)"]:::fixBox
Expand Down
19 changes: 19 additions & 0 deletions skills/jfrog-init/references/how-to-ask-user.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# How to ask the user questions

When the skill needs a Yes/No answer, a selection, or any other input
from the user, use the **native interactive prompt tool** built into
your harness so the user can click or select rather than type:

| Harness | Preferred tool |
|-------------|------------------------|
| Claude Code | `AskUserQuestion` |
| Codex | `request_user_input` |

Each reference file specifies the question text and option labels; use
your harness's native tool to present them. Native prompt tools already
offer a free-text "Other" fallback for values not in the list — don't
add a duplicate "Other" option yourself.

**Fallback**: if no native prompt tool is available, or the tool
returns without a selection, surface the question as plain text in
your reply — never silently stop without presenting it.
37 changes: 25 additions & 12 deletions skills/jfrog-init/references/mcp-plugin-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,31 @@ resolved at runtime from an env var:
{"mcpServers": {"jfrog": {"url": "https://${JFROG_PLATFORM_URL}/mcp"}}}
```

Codex's plugin ships the same idea in a different shape — no
`mcpServers` wrapper, and angle brackets instead of `${...}`:

```json
{"jfrog": {"url": "https://<JFROG_PLATFORM_URL>/mcp"}}
```

Because we have that URL sitting in `jf config`, and because leaving
the placeholder in place means the MCP silently fails to load in the
IDE / agent, Step 5 auto-substitutes it. If the detector finds the
placeholder pattern anywhere in the file, it calls
`jfrog-substitute-mcp-placeholders.mjs`, which:

1. Parses the file as JSON and looks **only** at
`mcpServers.jfrog.url` — never a file-wide text replace, so an
unrelated MCP server entry or JSON value that happens to contain the
same placeholder text is never touched.
1. Parses the file as JSON and looks **only** at the `jfrog` entry's
`url` (nested under `mcpServers` on every harness but Codex, which
has no wrapper) — never a file-wide text replace, so an unrelated
MCP server entry or JSON value that happens to contain the same
placeholder text is never touched.
2. Reads the JPD URL from `jf config` (default server, or the one
passed as arg 2), normalizes it to the JPD root, and substitutes it
into that one `url` string.
3. Handles both the `https://${...}` form (where our own scheme would
double up) and the bare `${...}` form.
3. Replaces in two passes — first a placeholder preceded by a scheme
(`https://${...}`, where our own scheme would otherwise double up),
then a bare one. Each pass recognizes all three syntaxes: `${VAR}`,
`$VAR`, and Codex's `<VAR>`.
4. Re-serializes the whole file (`JSON.stringify(parsed, null, 2)`) and
writes atomically (temp file + rename) so a partial write cannot
corrupt the file. Original formatting/whitespace elsewhere in the
Expand All @@ -44,16 +54,19 @@ This is the ONLY place `/jfrog-init` writes to the plugin-owned
| Cursor | `~/.cursor/plugins/cache/cursor-public/jfrog/<sha>/mcp.json` (glob → newest) |
| VS Code | `~/.vscode/agent-plugins/github.com/jfrog/vscode-plugin/plugin/.mcp.json` |
| Claude Code | `~/.claude/plugins/cache/<marketplace>/jfrog/<version>/.mcp.json` (glob) |
| Codex | `$CODEX_HOME/plugins/cache/codex-plugin/jfrog/<version>/.mcp.json` (glob → newest; `$CODEX_HOME` defaults to `~/.codex`) |

Harness detection: `CLAUDECODE` / `CURSOR_TRACE_ID` / `VSCODE_PID` /
`TERM_PROGRAM`. Override with `JFROG_INIT_HARNESS=claude|cursor|vscode`
or a specific file via `JFROG_INIT_MCP_CONFIG=/abs/path`.
Harness detection (in priority order): `CODEX_SANDBOX` / `CLAUDECODE` /
`CURSOR_TRACE_ID` / `VSCODE_PID` / `TERM_PROGRAM`. Override with
`JFROG_INIT_HARNESS=claude|cursor|vscode|codex` or a specific file via
`JFROG_INIT_MCP_CONFIG=/abs/path`.

**What the detector verifies** (three things):

1. Plugin file exists and is non-empty at its harness-specific path.
2. Parses as valid JSON.
3. Contains an `mcpServers.jfrog` entry with a non-empty `url`.
3. Contains a `jfrog` entry (nested under `mcpServers` on every harness
but Codex, which has no wrapper) with a non-empty `url`.

It does NOT enforce any other `type`/`url` shape (each plugin owns its
own schema) and it does NOT probe the endpoint — a mis-configured MCP
Expand All @@ -67,12 +80,12 @@ reachable.
- **Exit 1 (red)** or **Exit 3 (error)** → **non-blocking** — proceed
to Step 6 as if green, but remember the cause for the Final Summary.
Steps 6 and 7 call the JPD's REST APIs directly with `jf config`
credentials, never through `mcpServers.jfrog`, so a broken or
credentials, never through the JFrog MCP, so a broken or
missing plugin `mcp.json` doesn't affect whether those checks are
accurate — there's nothing to gain by stopping the walk over it.
Tell the two red causes apart from the detector's `detail` for the
Final Summary note:
- Plugin file missing / empty / lacks `mcpServers.jfrog`. Fix:
- Plugin file missing / empty / lacks a valid `jfrog` entry. Fix:
**reinstall or update the JFrog plugin.** If the user asks why or
how to fix it, run:

Expand Down
28 changes: 9 additions & 19 deletions skills/jfrog-init/references/node-install-prompt.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
# Step 1 — the Node.js install prompt

**Required behavior for Step 1's red branch, not optional background.**
When `node --version` is missing, unparseable, or its major version is
`< 18`, call `AskUserQuestion` with this exact payload shape (fill in
`<reason>` with either `isn't installed` or the specific `` `<version>`
is too old (need ≥ 18) ``, matching whichever is actually true):
When `node --version` is missing/unparseable/`< 18`, or when `npx
--version` fails (node present but npx absent — same red branch), ask
the user (see `how-to-ask-user.md` for native tool and fallback rules):

```json
{
"questions": [
{
"question": "Node.js <reason>. Install it now?",
"header": "Install Node",
"multiSelect": false,
"options": [
{"label": "Yes", "description": "Install Node.js now. Adds a line to your shell startup file so future terminals can find it."},
{"label": "No", "description": "Cancel /jfrog-init."}
]
}
]
}
```
- **Question**: "Node.js `<note>`. Install it now?"
— where `<note>` is a brief, accurate description of what the tool
output actually showed (e.g. "isn't installed", "`v16.4.0` is too old
(need ≥ 18)", "is installed but npx is missing").
- **Yes**: "Install Node.js now. Adds a line to your shell startup file so future terminals can find it."
- **No**: "Cancel /jfrog-init."

**Do not** mention any install method (nvm, winget), a version manager
name, or any URL — not in the question, not in an option description.
Expand Down
2 changes: 1 addition & 1 deletion skills/jfrog-init/references/script-invocation.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ lookups), `node
commands, not the function calls a pre-approved script makes once
running, so the mutation executes with no prompt whenever Step 5 finds
a placeholder. This is intentional, not an oversight: the write is
narrowly scoped to `mcpServers.jfrog.url`, atomic (temp file + rename),
narrowly scoped to the jfrog entry's `url` field, atomic (temp file + rename),
and idempotent — see `mcp-plugin-config.md` for why that scope was
judged safe to run unattended, unlike the two mutations below.
- `jfrog-state-file.mjs`'s **`set`** mode (writes `~/.jfrog/setup.json`
Expand Down
33 changes: 11 additions & 22 deletions skills/jfrog-init/references/server-picker.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,20 @@ triggered this — is reasoning for you to follow silently, never to
narrate.** The user never sees why they're being asked, only the
`AskUserQuestion` payload itself.

Call `AskUserQuestion` with the **first two** entries of `candidates`
(in the order the detector returned them — never reordered, never
chosen by matching a hostname, git identity, or any other signal) as
the two options, and rely on the tool's built-in "Other" for typing a
different server-id:
Ask the user (see `how-to-ask-user.md` for native tool and fallback
rules) with the **first two** entries of `candidates` (in the order
the detector returned them — never reordered, never chosen by matching
a hostname, git identity, or any other signal) as the two options, and
include a free-text escape hatch for typing a different server-id:

```json
{
"questions": [
{
"question": "Which JFrog server do you want to use?",
"header": "Server",
"multiSelect": false,
"options": [
{"label": "<candidates[0]>", "description": "Server ID: <candidates[0]>"},
{"label": "<candidates[1]>", "description": "Server ID: <candidates[1]>"}
]
}
]
}
```
- **Question**: "Which JFrog server do you want to use?"
- **Option 1**: `<candidates[0]>` (description: "Server ID: `<candidates[0]>`")
- **Option 2**: `<candidates[1]>` (description: "Server ID: `<candidates[1]>`")
- **Other**: let the user type a different server-id if needed

**Never surface the full candidate list or a count** to the user in
any case — the picker's two options (plus "Other") are the entire
user-facing surface, same rule as the project picker.
any case — the picker's two options (plus the free-text escape hatch)
are the entire user-facing surface, same rule as the project picker.

On picking option 1 or 2, or typing a value via **Other** → re-invoke
**the same detector that emitted the ask** (never a different one)
Expand Down
17 changes: 7 additions & 10 deletions skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
// Verifies the JFrog PLUGIN'S OWN mcp.json (per harness) exists at its
// installed path AND contains an mcpServers.jfrog entry. This file is
// installed path AND contains a jfrog entry (under mcpServers.jfrog on every
// harness but Codex, which has a bare top-level jfrog key). This file is
// owned by the plugin — we NEVER write to it, with one exception:
// automatic placeholder substitution (see jfrog-substitute-mcp-placeholders.mjs).
// If it's missing, malformed, or lacks the jfrog entry, the correct fix
Expand Down Expand Up @@ -98,13 +99,9 @@ export function detectJfrogMcp(serverIdArg) {
return 3;
}

// Auto-substitute any `${JFROG_PLATFORM_URL}` / `${JFROG_URL}` placeholder
// with the real JPD URL from `jf config`. Left in place, the MCP would
// fail to load in the IDE/agent since the env var doesn't exist.
// Checked against mcpServers.jfrog.url specifically (not the raw file
// text) so a placeholder-shaped string elsewhere in the plugin's
// mcp.json — an unrelated MCP entry, say — can't trigger substitution
// for a jfrog.url that has none.
// Substitute a placeholder in the jfrog entry's own url with the real
// JPD URL from `jf config` — checked there specifically, not the raw
// file text, so an unrelated MCP entry can't trigger it.
const preSubstitutionUrl = jfrogMcpUrl(parsed);
if (typeof preSubstitutionUrl === "string" && hasMcpPlaceholder(preSubstitutionUrl)) {
const result = substituteMcpPlaceholders(target, SERVER_ID);
Expand Down Expand Up @@ -134,11 +131,11 @@ export function detectJfrogMcp(serverIdArg) {
const url = jfrogMcpUrl(parsed);
const hasUrl = typeof url === "string" && url.trim() !== "";
if (!hasUrl) {
emit("red", target, "plugin mcp.json has no valid mcpServers.jfrog entry (missing or empty url) — reinstall or update the JFrog plugin");
emit("red", target, "plugin mcp.json has no valid jfrog entry (missing or empty url) — reinstall or update the JFrog plugin");
return 1;
}

emit("green", target, "plugin mcp.json present with mcpServers.jfrog entry");
emit("green", target, "plugin mcp.json present with a jfrog entry");
return 0;
}

Expand Down
14 changes: 13 additions & 1 deletion skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// This script itself never writes to the plugin's mcp.json — the file is
// owned by the JFrog plugin (Cursor / VS Code / Claude). It only prints
// owned by the JFrog plugin (Cursor / VS Code / Claude / Codex). It only prints
// the diagnosis and the correct remedy for a plugin file that's missing
// or invalid: reinstall or update the plugin, with per-harness commands
// so the user isn't left guessing. (A placeholder-only problem — e.g. an
Expand Down Expand Up @@ -47,11 +47,21 @@ After install, restart Claude Code, then re-run /jfrog-init.`);

Restart VS Code, then re-run /jfrog-init.`);
break;
case "codex":
console.log(`Codex:
codex plugin remove jfrog@codex-plugin # if already installed
codex plugin marketplace add jfrog/codex-plugin # skip if already configured
codex plugin marketplace upgrade codex-plugin
codex plugin add jfrog@codex-plugin

Restart Codex, then re-run /jfrog-init.`);
break;
default:
console.log(`Reinstall the JFrog plugin in whichever IDE you're using:
Cursor: Settings → Plugins → search "JFrog" → reinstall.
VS Code: code --install-extension JFrog.jfrog-vscode-extension --force
Claude Code: claude plugin install jfrog-beta/jfrog
Codex: codex plugin marketplace add jfrog/codex-plugin && codex plugin add jfrog@codex-plugin

Restart the IDE afterwards, then re-run /jfrog-init.`);
}
Expand All @@ -62,6 +72,8 @@ Expected plugin-owned paths (for reference):
Cursor: ~/.cursor/plugins/cache/cursor-public/jfrog/<sha>/mcp.json
VS Code: ~/.vscode/agent-plugins/github.com/jfrog/vscode-plugin/plugin/.mcp.json
Claude: ~/.claude/plugins/cache/<marketplace>/jfrog/<version>/.mcp.json
Codex: $CODEX_HOME/plugins/cache/codex-plugin/jfrog/<version>/.mcp.json
($CODEX_HOME defaults to ~/.codex)
`);

if (resolved.path && existsSync(resolved.path)) {
Expand Down
Loading
Loading