diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 3f26dc6..beaab50 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "JFrog Platform plugins for Cursor", - "version": "0.6.1", + "version": "0.6.2", "pluginRoot": "plugins" }, "plugins": [ diff --git a/.github/scripts/sync-skills-vendor.json b/.github/scripts/sync-skills-vendor.json index 19c5843..8fe85d5 100644 --- a/.github/scripts/sync-skills-vendor.json +++ b/.github/scripts/sync-skills-vendor.json @@ -1,6 +1,6 @@ { "repo": "jfrog/jfrog-skills", - "pin": "v0.32.0", + "pin": "v0.33.0", "paths": [ "skills" ] diff --git a/plugins/jfrog/.cursor-plugin/plugin.json b/plugins/jfrog/.cursor-plugin/plugin.json index def03d4..34a1221 100644 --- a/plugins/jfrog/.cursor-plugin/plugin.json +++ b/plugins/jfrog/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "jfrog", "displayName": "JFrog Platform", - "version": "0.6.1", + "version": "0.6.2", "description": "JFrog Platform integration with MCP, security skills, Agent Package Resolution, supply-chain best practices, and JFrog Agent Guard governance for adding, removing, and listing MCP servers.", "author": { "name": "JFrog", diff --git a/plugins/jfrog/skills/jfrog-init/SKILL.md b/plugins/jfrog/skills/jfrog-init/SKILL.md index 80760bd..de2727f 100644 --- a/plugins/jfrog/skills/jfrog-init/SKILL.md +++ b/plugins/jfrog/skills/jfrog-init/SKILL.md @@ -4,7 +4,7 @@ description: Set up and verify the JFrog plugin. Run on first install, to comple disable-model-invocation: true compatibility: >- Requires Node.js 18 or newer, and network access to the JFrog platform. -allowed-tools: Bash(node --version) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-catalog-runtime.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jf-cli.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jf-config.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jfrog-mcp.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-project.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-server-ping.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-re*.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-state-file.mjs" get*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-state-file.mjs" path*) Bash(node -e "import('${CLAUDE_SKILL_DIR}/scripts/jfrog-resolve-mcp-config.mjs').then(function(m){console.log(m.detectHarness())})") Bash(npx --version) Bash(uname:*) AskUserQuestion +allowed-tools: Bash(node --version) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-catalog-runtime.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jf-cli.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jf-config.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jfrog-mcp.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-project.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-server-ping.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-reinstall-jfrog-plugin.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-state-file.mjs" get*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-state-file.mjs" path*) Bash(node -e "import('${CLAUDE_SKILL_DIR}/scripts/jfrog-resolve-mcp-config.mjs').then(function(m){console.log(m.detectHarness())})") Bash(npx --version) Bash(uname:*) AskUserQuestion metadata: role: workflow --- @@ -72,8 +72,9 @@ absolute path of this file's directory yourself, same as before. Node (Step 1) or `jf` (Step 2); `AskUserQuestion` picker for web-login vs. token (Step 3/4); `AskUserQuestion` picker for project selection (Step 6). Everything else is read-only except Step 5's - placeholder substitution, Step 8's `~/.netrc` write, and the Final - summary's state write. + placeholder substitution (plus, for kiro-cli, creating or merging the + `jfrog` entry into `~/.kiro/settings/mcp.json`), Step 8's `~/.netrc` + write, and the Final summary's state write. - **Never surface the checklist.** Run silently — no step narration, no raw JSON/exit codes, no branch-reasoning said out loud. See [Customer-facing output](#customer-facing-output). @@ -139,6 +140,11 @@ user needs to know or act on: follow silently, not to summarize out loud — the field names in it are never user-facing. The only output the user sees at an ask point is the prompt itself. +- **Do not** narrate whether the `AskUserQuestion` tool is available in + the current harness before falling back to the plain-text prompt + (e.g. "the AskUserQuestion tool isn't available here, I'll present + this as a plain question instead"). If it isn't available, silently + use the plain-text fallback already documented for that ask point. - **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 @@ -331,7 +337,14 @@ blocks) — required behavior, not optional background. ## Step 5: JFrog MCP plugin file has a jfrog entry? +If you are Kiro or Kiro CLI, prefix the command with +`JFROG_INIT_HARNESS=kiro` or `JFROG_INIT_HARNESS=kiro-cli` on the +same line — no other harness needs this. Silent, like every other +setup command in this walk. + ```bash +JFROG_INIT_HARNESS=kiro node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jfrog-mcp.mjs" "[server-id]"; rc=$?; true +JFROG_INIT_HARNESS=kiro-cli node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jfrog-mcp.mjs" "[server-id]"; rc=$?; true node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jfrog-mcp.mjs" "[server-id]"; rc=$?; true ``` @@ -342,7 +355,9 @@ it for the placeholder fix instead of re-resolving from scratch. **Read-only against the JFrog plugin's own `mcp.json` — with one exception: automatic placeholder substitution** of an unresolved `${JFROG_PLATFORM_URL}` / `${JFROG_URL}` with the real JPD URL from -`jf config`. +`jf config` (plus, for kiro-cli, adding the `jfrog` entry to +`~/.kiro/settings/mcp.json` when no plugin has put one there — see +`references/mcp-plugin-config.md`). **Stop and read `references/mcp-plugin-config.md` in full** — exactly how the substitution works, the per-harness plugin-config paths, and @@ -361,24 +376,8 @@ node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-project.mjs" "[server-id]" "[proj the exact "reuse ``?" `AskUserQuestion` and the jpdUrl-drift check this step requires, not optional background. -**Where the project list comes from.** `jfrog-detect-project.mjs` fetches -`GET /access/api/v1/projects` (the -[GetProjectsList](https://docs.jfrog.com/projects/reference/getprojectslist) -endpoint, authenticated with credentials from `jf config export`) once -per walk and caches it in memory for a short TTL (`lib/project-cache.mjs`) -— the interactive picker re-invokes this script once per user attempt, -and re-enumerating on every typed guess would be wasted network traffic. -This is the list every "enumerated project list" / `candidatesWithNames` -reference below draws from. - -**Name-or-key input.** The user answers with **either** the project's -canonical key OR its display name — whichever is easier for them. -`jfrog-detect-project.mjs` resolves it against the enumerated project -list (exact key, exact name, then progressively fuzzier tiers — see -`references/project-matching.md` for the exact algorithm), confirms -existence, and emits the canonical key on green in the JSON -`resolvedKey` field. An ambiguous input exits red with `candidates` -listing the tied keys. +Name-or-key input is resolved via exact match, then progressively +fuzzier tiers — see `references/project-matching.md` for the algorithm. **Picking a project, interactively.** Whenever the detector needs the user to choose — no input was passed, the typed input didn't match diff --git a/plugins/jfrog/skills/jfrog-init/references/how-to-ask-user.md b/plugins/jfrog/skills/jfrog-init/references/how-to-ask-user.md index db3cbf2..bc8e526 100644 --- a/plugins/jfrog/skills/jfrog-init/references/how-to-ask-user.md +++ b/plugins/jfrog/skills/jfrog-init/references/how-to-ask-user.md @@ -4,10 +4,11 @@ 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` | +| Harness | Preferred tool | +|-----------------|------------------------| +| Claude Code | `AskUserQuestion` | +| Codex | `request_user_input` | +| Kiro / Kiro CLI | none — no native prompt tool exists; go straight to the plain-text fallback | Each reference file specifies the question text and option labels; use your harness's native tool to present them. Native prompt tools already @@ -17,3 +18,9 @@ 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. + +**Kiro / Kiro CLI**: check `$JFROG_INIT_HARNESS` (already exported by +Step 5) before reaching for `AskUserQuestion` — if it's `kiro` or +`kiro-cli`, skip the tool call entirely and use the plain-text fallback +directly. Calling it anyway surfaces a "tool does not exist" error to +the user before you fall back. diff --git a/plugins/jfrog/skills/jfrog-init/references/mcp-plugin-config.md b/plugins/jfrog/skills/jfrog-init/references/mcp-plugin-config.md index 39ac6dc..59a52b2 100644 --- a/plugins/jfrog/skills/jfrog-init/references/mcp-plugin-config.md +++ b/plugins/jfrog/skills/jfrog-init/references/mcp-plugin-config.md @@ -44,8 +44,10 @@ placeholder pattern anywhere in the file, it calls file is not preserved byte-for-byte. 5. Is idempotent — subsequent runs find no placeholder and no-op. -This is the ONLY place `/jfrog-init` writes to the plugin-owned -`mcp.json`. Everything else in Step 5 is read-only. +This is the only place `/jfrog-init` writes to a harness's plugin-owned +`mcp.json` — with one further exception for Kiro CLI: ensuring a `jfrog` +entry exists in `~/.kiro/settings/mcp.json`, which no plugin ships (see +below). Everything else in Step 5 is read-only. **Per-harness plugin-owned config file:** @@ -55,11 +57,26 @@ This is the ONLY place `/jfrog-init` writes to the plugin-owned | VS Code | `~/.vscode/agent-plugins/github.com/jfrog/vscode-plugin/plugin/.mcp.json` | | Claude Code | `~/.claude/plugins/cache//jfrog//.mcp.json` (glob) | | Codex | `$CODEX_HOME/plugins/cache/codex-plugin/jfrog//.mcp.json` (glob → newest; `$CODEX_HOME` defaults to `~/.codex`) | +| Kiro (IDE) | `~/.kiro/powers/installed/jfrog-kiro-power/mcp.json` (stable path) | +| Kiro CLI | `~/.kiro/settings/mcp.json` — Kiro's own global MCP config, not shipped by any plugin, so the `jfrog` entry is **created or merged in** with a placeholder url, then substituted like every other row above | + +The Kiro CLI merge is additive and never destructive: the file normally +holds the user's other MCP servers, so a `jfrog` entry that already has a +url is left untouched (a placeholder in it is the substitution step's +job), other servers and the file's mode are preserved, a symlinked config +stays a symlink, and a file that isn't valid JSON is reported rather than +rewritten. 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`. +`JFROG_INIT_HARNESS=claude|cursor|vscode|codex|kiro|kiro-cli` or a +specific file via `JFROG_INIT_MCP_CONFIG=/abs/path`. Neither Kiro target +has an auto-detect signal yet — both are reachable only via the +`JFROG_INIT_HARNESS=kiro` / `kiro-cli` overrides. + +`SKILL.md`'s Step 5 already has you export `JFROG_INIT_HARNESS=kiro` / +`kiro-cli` up front when you're running as one of those two — before +the detector ever runs, so Exit 3 below isn't the trigger for it. **What the detector verifies** (three things): @@ -83,7 +100,7 @@ reachable. 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 + Tell the red causes apart from the detector's `detail` for the Final Summary note: - Plugin file missing / empty / lacks a valid `jfrog` entry. Fix: **reinstall or update the JFrog plugin.** If the user asks why or @@ -98,8 +115,22 @@ reachable. - Plugin file has a placeholder and automatic substitution failed with no url set for the resolved server-id. Fix: **resolve `jf config`**. Reinstalling the plugin does not fix this. + - Kiro CLI only: it could not create or update its own + `~/.kiro/settings/mcp.json` — no plugin ships this file, so there's + nothing to reinstall. The detail names the actual cause (e.g. the + parent path blocked by a non-directory, or a permissions error). + Fix: **correct the file or parent-directory permissions/path**, + then re-run. - (Exit 3 only) Harness could not be detected, or plugin file is invalid JSON / unreadable. Show the raw detector error in the note. + **Do not react to this by guessing a harness or trying + `JFROG_INIT_HARNESS` values to see what resolves it.** If this is + Kiro or Kiro CLI, the override was already exported before the + detector's first run (top of Step 5), so it should not reach Exit + 3 for that cause at all. Otherwise this is Exit 3, non-blocking + like every other cause above: note it and move on to Step 6 in the + same turn, with zero visible pause — do not stop to read this file + or any other reference doc over it. - **Exit 2 (`ask`)** → the one outcome that still blocks: placeholder present, but the jf server-id is ambiguous — every step from here on needs a resolved server-id, so there's nothing to skip ahead to. diff --git a/plugins/jfrog/skills/jfrog-init/references/project-picker.md b/plugins/jfrog/skills/jfrog-init/references/project-picker.md index 9988505..c1e827c 100644 --- a/plugins/jfrog/skills/jfrog-init/references/project-picker.md +++ b/plugins/jfrog/skills/jfrog-init/references/project-picker.md @@ -102,6 +102,16 @@ over the generic first-two when present: accepted input format (no *"(name or key)"*, no *"you can type a key or name"*, no *"either the display name or key works"*). +- **If `AskUserQuestion` is not available as a tool at all** (some + harnesses don't expose it — check `$JFROG_INIT_HARNESS`: `kiro` / + `kiro-cli` never have it, so skip the tool call and go straight to + the fallback instead of calling it and surfacing its error), use the + plain-text fallback line above — + *"Which project do you want to use?"*, nothing else — regardless of + how many entries `candidatesWithNames` has. Never explain that the + tool is unavailable, never mention the candidate count or that a + picker would otherwise have been used; the user only ever sees the + one line. - **Never surface the full candidate list or a count** to the user in any case — the picker's two options (plus "Other") or the plain-text fallback are the entire user-facing surface. diff --git a/plugins/jfrog/skills/jfrog-init/references/project-resolution-branches.md b/plugins/jfrog/skills/jfrog-init/references/project-resolution-branches.md index 4e4d1d3..501c7c9 100644 --- a/plugins/jfrog/skills/jfrog-init/references/project-resolution-branches.md +++ b/plugins/jfrog/skills/jfrog-init/references/project-resolution-branches.md @@ -4,6 +4,13 @@ full whenever `jfrog-detect-project.mjs` returns anything other than a clean exit 0. +**Everything below — exit codes, `unresolved`, and which bullet you +land on — is reasoning for you to follow silently, never to narrate.** +Never repeat this table's own words back to the user (e.g. "Exit 2, +no `unresolved`" or "this is an ask with no input"). The only output +the user sees is the resulting prompt itself, the raw detector error +where one is shown, or the Final Summary. + - **Exit 2 (`ask`) with `"unresolved": "server"`** → not a project ask — the server-id is ambiguous. Follow "Resolving `` for Steps 4-7" in `SKILL.md` (prompt for a server from `candidates`), then diff --git a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-detect-all.mjs b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-detect-all.mjs index 7efdbcd..4c8c1c1 100755 --- a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-detect-all.mjs +++ b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-detect-all.mjs @@ -1,7 +1,10 @@ #!/usr/bin/env node // Runs the detectors in dependency order and reports one JSON summary line. -// Idempotent, read-only, zero mutation (aside from the state-file hint on -// green) — safe to run repeatedly. +// Idempotent and safe to run repeatedly. Mostly read-only — with two +// exceptions: (1) for the kiro-cli harness, Step 5 (detectJfrogMcp → +// resolveMcpConfig) creates or merges ~/.kiro/settings/mcp.json when the +// jfrog entry is absent, the same write that jfrog-detect-jfrog-mcp.mjs +// performs; (2) on green, a state-file hint is written to ~/.jfrog/setup.json. // // Usage: node jfrog-detect-all.mjs [server-id] [project-input] // diff --git a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs index f115185..30a52ca 100755 --- a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs +++ b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs @@ -13,10 +13,17 @@ // Always exits 0 after printing. import { existsSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; import { detectHarness, resolveMcpConfig } from "./jfrog-resolve-mcp-config.mjs"; -const resolved = resolveMcpConfig(); const harness = detectHarness(); +// kiro-cli's resolveMcpConfig() writes to disk as a side effect (ensureKiroCliJfrogEntry), +// which this diagnostic-only script must never do — derive the path directly instead. +const resolved = + harness === "kiro-cli" + ? { path: join(homedir(), ".kiro", "settings", "mcp.json") } + : resolveMcpConfig(); console.log(`The JFrog MCP entry lives inside the JFrog plugin's own mcp.json file. This script never writes to it — it only diagnoses and prints the fix. @@ -56,12 +63,27 @@ Restart VS Code, then re-run /jfrog-init.`); Restart Codex, then re-run /jfrog-init.`); break; + case "kiro": + console.log(`Kiro IDE: + Open the Powers panel → Add Custom Power → Import from GitHub → + enter the JFrog Kiro Power repository URL. + Restart Kiro, then re-run /jfrog-init.`); + break; + case "kiro-cli": + console.log(`Kiro CLI: + No plugin reinstall needed — the jfrog entry in ~/.kiro/settings/mcp.json + is created automatically by /jfrog-init. Re-run /jfrog-init to recreate it. + If /jfrog-init reports the file is invalid, open ~/.kiro/settings/mcp.json, + fix the JSON (keep the other MCP server entries), 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 + Kiro: Powers panel → Add Custom Power → Import from GitHub. + Kiro CLI: no reinstall needed — entry is created automatically by /jfrog-init. Restart the IDE afterwards, then re-run /jfrog-init.`); } @@ -74,6 +96,8 @@ Expected plugin-owned paths (for reference): Claude: ~/.claude/plugins/cache//jfrog//.mcp.json Codex: $CODEX_HOME/plugins/cache/codex-plugin/jfrog//.mcp.json ($CODEX_HOME defaults to ~/.codex) + Kiro: ~/.kiro/powers/installed/jfrog-kiro-power/mcp.json + Kiro CLI: ~/.kiro/settings/mcp.json (not plugin-owned; created by /jfrog-init) `); if (resolved.path && existsSync(resolved.path)) { diff --git a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs index 14d7e0e..b675d76 100755 --- a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs +++ b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs @@ -2,7 +2,8 @@ // Resolves the PLUGIN-OWNED mcp.json for the CURRENT harness and returns its // absolute path. This is the file the JFrog plugin ships with — NOT the // user's project- or user-scope MCP config. This skill never touches the -// customer's own mcp.json; only the one owned by the JFrog plugin. +// customer's own mcp.json; only the one owned by the JFrog plugin — with +// one exception, kiro-cli, which has no plugin behind it (see below). // // Plugin-owned paths per harness: // Cursor: ~/.cursor/plugins/cache/cursor-public/jfrog//mcp.json @@ -16,6 +17,11 @@ // Codex: $CODEX_HOME/plugins/cache/codex-plugin/jfrog//.mcp.json // (multiple dirs may exist; most-recently-modified // wins. $CODEX_HOME defaults to ~/.codex.) +// Kiro (IDE): ~/.kiro/powers/installed/jfrog-kiro-power/mcp.json (stable path) +// Kiro CLI: ~/.kiro/settings/mcp.json (not plugin-shipped; this is +// Kiro's own global MCP config, so the jfrog entry is +// created — or merged into an existing file — with a +// placeholder url. See ensureKiroCliJfrogEntry() below.) // // NOTE (Claude): the current released Claude plugin (jfrog-beta/0.3.0-beta.1) // does NOT ship a .mcp.json — the source repo has one, but the packager @@ -37,8 +43,12 @@ // detectHarness() is the single JS implementation — exported and reused // by every other script in this skill that needs harness information. // +// Kiro (IDE and CLI) has no detect signal yet — reachable only via the +// JFROG_INIT_HARNESS=kiro / kiro-cli overrides below. +// // Overrides: -// - JFROG_INIT_HARNESS=claude|cursor|vscode|codex forces one specific harness. +// - JFROG_INIT_HARNESS=claude|cursor|vscode|codex|kiro|kiro-cli forces +// one specific harness. // - JFROG_INIT_MCP_CONFIG=/abs/path forces one specific path. // (Escape hatch — bypasses the plugin-path resolution entirely.) // - CODEX_HOME=/abs/path Codex's own var, honored by @@ -51,17 +61,17 @@ // Exit 2 -> harness detected, but the plugin's mcp.json is not installed import { execFileSync } from "node:child_process"; -import { existsSync, readdirSync, statSync } from "node:fs"; +import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; -import { join } from "node:path"; -import { isMainModule } from "./lib/jf.mjs"; +import { dirname, join } from "node:path"; +import { isMainModule, jfrogMcpEntry } from "./lib/jf.mjs"; -const VALID_HARNESSES = new Set(["claude", "cursor", "vscode", "codex"]); +const VALID_HARNESSES = new Set(["claude", "cursor", "vscode", "codex", "kiro", "kiro-cli"]); // One entry per harness, in priority order (see doc comment above) — used // both as the signal check and as the static fallback when the ancestry -// tie-break can't resolve it. Adding a harness (Kiro, OpenCode, ...) is -// just a new entry here. +// tie-break can't resolve it. Adding a harness (OpenCode, ...) is just a +// new entry here — Kiro has none yet, see the header note above. const HARNESS_SIGNALS = [ { name: "codex", signaled: () => process.env.CODEX_SANDBOX || process.env.CODEX_THREAD_ID || process.env.CODEX_CI }, { name: "claude", signaled: () => process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT || process.env.CLAUDE_CODE_SESSION_ID }, @@ -229,6 +239,143 @@ function resolveCodexPath() { return { path: match }; } +function resolveKiroPath() { + const p = join(homedir(), ".kiro", "powers", "installed", "jfrog-kiro-power", "mcp.json"); + if (!existsSync(p)) { + return { + error: `JFrog Kiro Power's mcp.json not found at ${p}\n install the JFrog Power in Kiro (Powers panel -> Add Custom Power -> Import from GitHub) to make it available.`, + code: 2, + }; + } + return { path: p }; +} + +// The url every other harness's plugin ships; the generic substitution +// step rewrites it with the real JPD from `jf config`. +const KIRO_CLI_PLACEHOLDER_URL = "https://${JFROG_PLATFORM_URL}/mcp"; + +// Overwrites an existing file via temp+rename (preserves symlinks and mode). +function replaceKiroCliConfig(target, content) { + const real = realpathSync(target); + const tmp = `${real}.tmp.${process.pid}`; + try { + // "wx" refuses to follow/overwrite anything already at tmp. + writeFileSync(tmp, content, { flag: "wx", mode: 0o600 }); + chmodSync(tmp, statSync(real).mode & 0o777); + renameSync(tmp, real); + } catch (err) { + // A run killed between write and rename leaves tmp behind, and the + // name is only unique per PID — clean up so the next run isn't stuck + // on EEXIST forever. + try { + unlinkSync(tmp); + } catch { + // Never created, already renamed, or not ours to remove. + } + throw err; + } +} + +// kiro-cli is the one target with no plugin behind it: ~/.kiro/settings/mcp.json +// is Kiro's own global MCP config, so a missing jfrog entry is something to +// add rather than an install error. Additive only — the file normally holds +// the user's other MCP servers, and an existing jfrog entry is left exactly +// as it is (a placeholder in its url is the substitution step's job, not +// this one's). Returns an error result, or null when the file is ready. +// +// `retryAfterRace` guards the one path that can legitimately need a second +// look: see the EEXIST branch below. +function ensureKiroCliJfrogEntry(target, retryAfterRace = true) { + let raw = null; + try { + raw = readFileSync(target, "utf8"); + } catch (err) { + if (err.code !== "ENOENT") { + return { error: `could not read ${target}: ${err.message}`, code: 2 }; + } + } + + // Nothing usable on disk — write the whole document rather than merge. + // An empty file counts: Kiro treats it as no config, and JSON.parse of + // "" would only send us down the invalid-JSON path below. + if (raw === null || raw.trim() === "") { + const content = JSON.stringify({ mcpServers: { jfrog: { url: KIRO_CLI_PLACEHOLDER_URL } } }, null, 2) + "\n"; + try { + if (raw === null) { + mkdirSync(dirname(target), { recursive: true }); + writeFileSync(target, content, { flag: "wx", mode: 0o600 }); + } else { + replaceKiroCliConfig(target, content); + } + } catch (err) { + if (err.code !== "EEXIST") { + return { error: `could not write ${target}: ${err.message}`, code: 2 }; + } + // "wx" raises EEXIST for a race-created file or a dangling symlink; + // retry to tell them apart (a real file now parses; an unreadable path + // comes back here with the retry spent and is reported as an error). + if (retryAfterRace) return ensureKiroCliJfrogEntry(target, false); + return { error: `could not write ${target}: something already at that path cannot be read as a file`, code: 2 }; + } + return null; + } + + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { error: `${target} is not valid JSON — refusing to modify it; fix the file, then re-run.`, code: 2 }; + } + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + return { error: `${target} is not a JSON object — refusing to modify it; fix the file, then re-run.`, code: 2 }; + } + + // Kiro CLI reads only mcpServers.jfrog — a top-level jfrog key (Codex + // shape) is invisible to it. Use a direct lookup instead of jfrogMcpEntry() + // so a bare top-level entry doesn't falsely satisfy the check and skip the + // merge that would write the mcpServers shape Kiro CLI actually needs. + const existing = parsed.mcpServers && typeof parsed.mcpServers === "object" && !Array.isArray(parsed.mcpServers) + ? parsed.mcpServers.jfrog + : undefined; + if (existing && typeof existing === "object" && !Array.isArray(existing) && typeof existing.url === "string" && existing.url.trim() !== "") { + return null; + } + + if (!("mcpServers" in parsed) || parsed.mcpServers === undefined || parsed.mcpServers === null) { + parsed.mcpServers = {}; + } else if (typeof parsed.mcpServers !== "object" || Array.isArray(parsed.mcpServers)) { + return { error: `${target} has a non-object "mcpServers" — refusing to modify it; fix the file, then re-run.`, code: 2 }; + } + + // Spread rather than assign: a half-written entry may already carry + // fields of the user's own (e.g. "disabled") that aren't ours to drop. + const entry = parsed.mcpServers.jfrog; + parsed.mcpServers.jfrog = { + ...(entry !== null && typeof entry === "object" && !Array.isArray(entry) ? entry : {}), + url: KIRO_CLI_PLACEHOLDER_URL, + }; + + try { + replaceKiroCliConfig(target, JSON.stringify(parsed, null, 2) + "\n"); + } catch (err) { + return { error: `could not write ${target}: ${err.message}`, code: 2 }; + } + return null; +} + +function resolveKiroCliPath() { + const p = join(homedir(), ".kiro", "settings", "mcp.json"); + const err = ensureKiroCliJfrogEntry(p); + if (err) return err; + // Resolve after ensure so the substitution step downstream always + // receives the real path — never a symlink that renameSync would replace. + try { + return { path: realpathSync(p) }; + } catch (e) { + return { error: `could not resolve real path of ${p}: ${e.message}`, code: 2 }; + } +} + export function resolveMcpConfig() { if (process.env.JFROG_INIT_MCP_CONFIG) { return { path: process.env.JFROG_INIT_MCP_CONFIG }; @@ -242,7 +389,7 @@ export function resolveMcpConfig() { // set the very variable they already set. if (process.env.JFROG_INIT_HARNESS && !VALID_HARNESSES.has(harness)) { return { - error: `JFROG_INIT_HARNESS=${process.env.JFROG_INIT_HARNESS} is not one of: claude, cursor, vscode, codex.`, + error: `JFROG_INIT_HARNESS=${process.env.JFROG_INIT_HARNESS} is not one of: claude, cursor, vscode, codex, kiro, kiro-cli.`, code: 1, }; } @@ -256,11 +403,15 @@ export function resolveMcpConfig() { return resolveVscodePath(); case "codex": return resolveCodexPath(); + case "kiro": + return resolveKiroPath(); + case "kiro-cli": + return resolveKiroCliPath(); default: return { error: "could not detect current harness (Claude Code / Cursor / VS Code / Codex).\n" + - " Set JFROG_INIT_HARNESS=claude|cursor|vscode|codex, or\n" + + " Set JFROG_INIT_HARNESS=claude|cursor|vscode|codex|kiro|kiro-cli, or\n" + " JFROG_INIT_MCP_CONFIG=/absolute/path/to/mcp.json to override.", code: 1, }; diff --git a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs index c75b6f0..a058e3a 100755 --- a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs +++ b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs @@ -37,7 +37,7 @@ // server-id passed — ambiguous, caller must ask the user // Exit 3 -> read/write error, or jf missing -import { existsSync, readFileSync, writeFileSync, renameSync, statSync, chmodSync, unlinkSync } from "node:fs"; +import { existsSync, readFileSync, realpathSync, writeFileSync, renameSync, statSync, chmodSync, unlinkSync } from "node:fs"; import { emit as emitJf, isMainModule, jfAvailable, jfConfigShow, urlForServer, normalizeJpdUrl, mcpPlaceholderRegexes, jfrogMcpEntry, jfrogMcpUrl, hasMcpPlaceholder, askServerResult, describeJfUnavailable } from "./lib/jf.mjs"; import { resolveJfServer } from "./jfrog-resolve-jf-server.mjs"; @@ -110,20 +110,23 @@ export function substituteMcpPlaceholders(target, serverIdOverride) { jfrogMcpEntry(parsed).url = newUrl; const rewritten = JSON.stringify(parsed, null, 2) + "\n"; - const tmp = `${target}.tmp.${process.pid}`; + const real = realpathSync(target); + const tmp = `${real}.tmp.${process.pid}`; try { // "wx" refuses to follow/overwrite anything already at tmp (e.g. a // pre-planted symlink) — same symlink-safe pattern as - // lib/project-cache.mjs's writeCachedProjectList(). - writeFileSync(tmp, rewritten, { flag: "wx" }); + // lib/project-cache.mjs's writeCachedProjectList(). mode: 0o600 closes + // the window between writeFileSync and chmodSync where sibling MCP tokens + // in the same file would be world-readable. + writeFileSync(tmp, rewritten, { flag: "wx", mode: 0o600 }); // rename() replaces the target's inode wholesale, so without this the // file would silently pick up writeFileSync's default umask-derived // mode instead of the target's own — e.g. a 0600 mcp.json holding // another MCP server's secrets in its env block would come back 0644 // (world-readable) after a substitution that has nothing to do with // that other entry. - chmodSync(tmp, statSync(target).mode & 0o777); - renameSync(tmp, target); + chmodSync(tmp, statSync(real).mode & 0o777); + renameSync(tmp, real); } catch (err) { // A run killed between the write and the rename (Ctrl-C, OOM, harness // timeout) leaves tmp behind; the name is only unique per PID, so the diff --git a/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md b/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md index c49aded..1703760 100644 --- a/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md +++ b/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md @@ -14,7 +14,7 @@ compatibility: >- Requires network access to the JFrog Agent Guard npm registry (not public npm) and the JFrog platform, and ~/.jfrog/ write access for OAuth login and cache cleanup. -allowed-tools: Bash(claude mcp list) Bash(cursor agent mcp list) Bash(codex mcp list) Bash(opencode mcp list) Bash(kiro-cli mcp list) Bash(kiro-cli mcp list workspace) Bash(kiro-cli mcp list global) Bash(kiro-cli mcp status) Read Edit Write +allowed-tools: Bash(claude mcp list) Bash(cursor agent mcp list) Bash(codex mcp list) Bash(opencode mcp list) Bash(kiro-cli mcp list) Bash(kiro-cli mcp list workspace) Bash(kiro-cli mcp list global) Bash(kiro-cli mcp status --name*) Read Edit Write metadata: role: workflow --- diff --git a/plugins/jfrog/skills/jfrog-mcp-management/references/harness-common.md b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-common.md index 00ec744..2144dcb 100644 --- a/plugins/jfrog/skills/jfrog-mcp-management/references/harness-common.md +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-common.md @@ -40,7 +40,7 @@ editor UI or `mcp.json`, so it falls through to the Fallback section. | OpenCode | `OPENCODE` | [harness-opencode.md](harness-opencode.md) | | Devin | Your system prompt / system instructions identify you as **Devin** (Devin Desktop / Devin Local / Devin CLI / Cognition). | [harness-devin.md](harness-devin.md) | | Kiro | Your system prompt / system instructions identify you as **Kiro** (Kiro IDE / `kiro-cli`). | [harness-kiro.md](harness-kiro.md) | -| VS Code editor | `TERM_PROGRAM=vscode` **and no `CURSOR_*` var is set** **and no `OPENCODE` var is set** **and no `CODEX_*` var is set** **and no `CLAUDECODE`/`CLAUDE_CODE_ENTRYPOINT` var is set** **and no `GEMINI_CLI` / `GOOSE_TERMINAL` / `COPILOT_CLI` var is set** **and** your system prompt / system instructions do **not** identify you as Devin or Kiro | [harness-vscode.md](harness-vscode.md) | +| VS Code editor | `TERM_PROGRAM=vscode` **and `TERM_PROGRAM` is not `kiro`** **and no `CURSOR_*` var is set** **and no `OPENCODE` var is set** **and no `CODEX_*` var is set** **and no `CLAUDECODE`/`CLAUDE_CODE_ENTRYPOINT` var is set** **and no `GEMINI_CLI` / `GOOSE_TERMINAL` / `COPILOT_CLI` var is set** **and** your system prompt / system instructions do **not** identify you as Devin or Kiro | [harness-vscode.md](harness-vscode.md) | | anything else | none of the above | **Fallback** section below — no harness file exists | Once you know your harness, use ONLY these fields from its file: `Config files` diff --git a/plugins/jfrog/skills/jfrog/SKILL.md b/plugins/jfrog/skills/jfrog/SKILL.md index 377deb1..8a0ed1b 100644 --- a/plugins/jfrog/skills/jfrog/SKILL.md +++ b/plugins/jfrog/skills/jfrog/SKILL.md @@ -19,7 +19,7 @@ compatibility: >- (CLI) and Tier 3 (jf api) operations; without it, only MCP (Tier 1) is available. metadata: role: base - version: "0.32.0" + version: "0.33.0" --- # JFrog Skill