Skip to content

Commit 91cd842

Browse files
chore: sync modules to v0.6.0
1 parent fbd2a33 commit 91cd842

22 files changed

Lines changed: 2766 additions & 2 deletions

.cursor-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
"metadata": {
88
"description": "JFrog Platform plugins for Cursor",
9-
"version": "0.5.6",
9+
"version": "0.5.7",
1010
"pluginRoot": "plugins"
1111
},
1212
"plugins": [
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"repo": "JFROG/jfrog-agent-hooks",
3+
"pin": "jfrog-agent-hooks/v0.6.0",
4+
"paths": [
5+
"modules"
6+
]
7+
}

plugins/jfrog/.cursor-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "jfrog",
33
"displayName": "JFrog Platform",
4-
"version": "0.5.6",
4+
"version": "0.5.7",
55
"description": "JFrog Platform integration with MCP, security skills, supply-chain best practices, and JFrog Agent Guard governance for adding, removing, and listing MCP servers.",
66
"author": {
77
"name": "JFrog",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"logLevel": "info",
3+
"packageResolution": {
4+
"enabled": false,
5+
"verifyRepos": true,
6+
"cacheTtlDays": 7,
7+
"defaultGlobalRepos": {
8+
"npm": "npm-virtual",
9+
"pypi": "pypi-virtual",
10+
"maven": "maven-virtual",
11+
"go": "go-virtual",
12+
"docker": "docker-virtual",
13+
"helm": "helm-virtual",
14+
"nuget": "nuget-virtual"
15+
},
16+
"enforceOnStartup": []
17+
}
18+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env node
2+
// Claude Code SessionStart hook runner.
3+
//
4+
// Usage: node claude-session-start.mjs <capability>
5+
// Example: node claude-session-start.mjs package-resolution
6+
//
7+
// stdout: JSON with hookSpecificOutput.additionalContext. No stdout is a no-op.
8+
9+
import process from "node:process";
10+
11+
import { runCapability } from "./core/run-capability.mjs";
12+
import { ensureAgentsConfigScaffold, agentsConfigLoadWarnings } from "./core/agents-config.mjs";
13+
import { readStdin, parseSessionId, detectHarness, parseWorkspaceRoots } from "./core/io.mjs";
14+
import { setLogContext, createLogger } from "./core/logger.mjs";
15+
16+
const HARNESS_ID = "claude_code";
17+
const log = createLogger("session-start");
18+
19+
/** @returns {string | null} JSON stdout payload, or null when there is nothing to inject. */
20+
function formatSessionStartStdout(text) {
21+
if (!text?.trim()) return null;
22+
return JSON.stringify({
23+
hookSpecificOutput: {
24+
hookEventName: "SessionStart",
25+
additionalContext: text,
26+
},
27+
});
28+
}
29+
30+
function writeStdout(payload) {
31+
if (payload !== null) process.stdout.write(payload);
32+
}
33+
34+
function writeNoOp() {
35+
// Claude SessionStart: no stdout on no-op.
36+
}
37+
38+
async function main() {
39+
const capability = process.argv[2];
40+
if (!capability) {
41+
writeNoOp();
42+
return;
43+
}
44+
45+
const startedAtMs = Date.now();
46+
const stdinRaw = await readStdin();
47+
const harness = detectHarness(stdinRaw);
48+
if (harness && harness !== HARNESS_ID) {
49+
setLogContext({ ide: HARNESS_ID, sessionId: parseSessionId(stdinRaw) });
50+
log.warn("harness mismatch; wrong adapter invoked", {
51+
expected: HARNESS_ID,
52+
detected: harness,
53+
adapter: "claude-session-start",
54+
});
55+
writeNoOp();
56+
return;
57+
}
58+
const sessionId = parseSessionId(stdinRaw);
59+
const workspaceRoots = parseWorkspaceRoots(stdinRaw);
60+
setLogContext({ ide: HARNESS_ID, sessionId });
61+
ensureAgentsConfigScaffold();
62+
for (const w of agentsConfigLoadWarnings()) {
63+
log.warn(w.message, { path: w.path });
64+
}
65+
const text = await runCapability(capability, {
66+
ide: HARNESS_ID,
67+
sessionId,
68+
workspaceRoots,
69+
startedAtMs,
70+
});
71+
writeStdout(formatSessionStartStdout(text));
72+
}
73+
74+
main().catch(() => {
75+
writeNoOp();
76+
process.exit(0);
77+
});
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
// Local admin config at ~/.jfrog/agents-conf.json (shipped template: assets/agents-default-conf.json).
2+
//
3+
// Read-only helpers — no network. Session starters call ensureAgentsConfigScaffold()
4+
// before capabilities run so first-time installs get a writable config file.
5+
6+
import {
7+
copyFileSync,
8+
existsSync,
9+
mkdirSync,
10+
readFileSync,
11+
statSync,
12+
} from "node:fs";
13+
import { homedir } from "node:os";
14+
import path from "node:path";
15+
import { fileURLToPath } from "node:url";
16+
17+
/** modules bundle root (parent of core/ and assets/). */
18+
const PLUGIN_ROOT = path.resolve(
19+
path.dirname(fileURLToPath(import.meta.url)),
20+
"..",
21+
);
22+
23+
const TEMPLATE_PATH = path.join(
24+
PLUGIN_ROOT,
25+
"assets",
26+
"agents-default-conf.json",
27+
);
28+
29+
const DEFAULT_LOG_LEVEL = "info";
30+
const DEFAULT_CACHE_TTL_DAYS = 7;
31+
let memoizedRaw = undefined;
32+
let memoizedForPath = null;
33+
/** @type {{ source: 'missing' | 'user' | 'template', parseFailed: boolean, path: string }} */
34+
let loadMeta = { source: "missing", parseFailed: false, path: "" };
35+
36+
function agentsConfigPath() {
37+
return path.join(homedir(), ".jfrog", "agents-conf.json");
38+
}
39+
40+
function resetLoadMeta(configPath) {
41+
loadMeta = { source: "missing", parseFailed: false, path: configPath };
42+
}
43+
44+
/**
45+
* Copy the shipped template to ~/.jfrog/agents-conf.json when missing.
46+
* Never overwrites an existing file.
47+
*/
48+
export function ensureAgentsConfigScaffold() {
49+
const configPath = agentsConfigPath();
50+
if (existsSync(configPath)) return { created: false, path: configPath };
51+
try {
52+
mkdirSync(path.dirname(configPath), { recursive: true });
53+
copyFileSync(TEMPLATE_PATH, configPath);
54+
memoizedRaw = undefined;
55+
return { created: true, path: configPath };
56+
} catch {
57+
return { created: false, path: configPath };
58+
}
59+
}
60+
61+
export { agentsConfigPath };
62+
63+
/** @returns {number | null} mtime in ms, or null when the file is absent */
64+
export function getAgentsConfigMtimeMs() {
65+
try {
66+
return statSync(agentsConfigPath()).mtimeMs;
67+
} catch {
68+
return null;
69+
}
70+
}
71+
72+
function parseAgentsJson(raw) {
73+
try {
74+
const parsed = JSON.parse(raw);
75+
return parsed && typeof parsed === "object" ? parsed : null;
76+
} catch {
77+
return null;
78+
}
79+
}
80+
81+
function readAgentsConfigRaw() {
82+
const configPath = agentsConfigPath();
83+
if (memoizedForPath !== configPath) {
84+
memoizedRaw = undefined;
85+
memoizedForPath = configPath;
86+
resetLoadMeta(configPath);
87+
}
88+
if (memoizedRaw !== undefined) return memoizedRaw;
89+
90+
const userExists = existsSync(configPath);
91+
if (userExists) {
92+
try {
93+
const parsed = parseAgentsJson(readFileSync(configPath, "utf8"));
94+
if (parsed) {
95+
memoizedRaw = parsed;
96+
loadMeta = { source: "user", parseFailed: false, path: configPath };
97+
return memoizedRaw;
98+
}
99+
loadMeta = { source: "template", parseFailed: true, path: configPath };
100+
} catch {
101+
loadMeta = { source: "template", parseFailed: true, path: configPath };
102+
}
103+
}
104+
105+
try {
106+
memoizedRaw = parseAgentsJson(readFileSync(TEMPLATE_PATH, "utf8"));
107+
if (!userExists) {
108+
loadMeta = {
109+
source: memoizedRaw ? "template" : "missing",
110+
parseFailed: false,
111+
path: configPath,
112+
};
113+
}
114+
} catch {
115+
memoizedRaw = null;
116+
if (!userExists)
117+
loadMeta = { source: "missing", parseFailed: false, path: configPath };
118+
}
119+
return memoizedRaw;
120+
}
121+
122+
/** Call after loadAgentsConfig() — surfaces user-file parse failures. */
123+
export function getAgentsConfigLoadMeta() {
124+
readAgentsConfigRaw();
125+
return { ...loadMeta };
126+
}
127+
128+
/** @returns {Array<{ message: string, path: string }>} */
129+
export function agentsConfigLoadWarnings() {
130+
loadAgentsConfig();
131+
if (!loadMeta.parseFailed) return [];
132+
return [
133+
{
134+
message: "agents-conf.json unreadable; using shipped template defaults",
135+
path: loadMeta.path,
136+
},
137+
];
138+
}
139+
140+
/** @returns {object | null} raw section or null */
141+
export function getAgentsConfigSection(name) {
142+
const config = readAgentsConfigRaw();
143+
if (!config) return null;
144+
const section = config[name];
145+
return section && typeof section === "object" ? section : null;
146+
}
147+
148+
/** @returns {{ logLevel: string, packageResolution: object }} merged with documented defaults */
149+
export function loadAgentsConfig() {
150+
const file = readAgentsConfigRaw() ?? {};
151+
const pr =
152+
file.packageResolution && typeof file.packageResolution === "object"
153+
? file.packageResolution
154+
: {};
155+
const defaultGlobalRepos =
156+
pr.defaultGlobalRepos && typeof pr.defaultGlobalRepos === "object"
157+
? normalizeRepoMap(pr.defaultGlobalRepos)
158+
: {};
159+
160+
return {
161+
logLevel: normalizeLogLevel(file.logLevel),
162+
packageResolution: {
163+
enabled: pr.enabled === true,
164+
verifyRepos: pr.verifyRepos !== false,
165+
cacheTtlDays: normalizeCacheTtlDays(pr.cacheTtlDays),
166+
defaultGlobalRepos,
167+
enforceOnStartup: normalizeEnforceOnStartup(pr.enforceOnStartup),
168+
},
169+
};
170+
}
171+
172+
export function getGlobalLogLevel() {
173+
return loadAgentsConfig().logLevel;
174+
}
175+
176+
/**
177+
* Package types the admin declares globally (governance source). Governance is
178+
* the UNION of these and any workspace `.jfrog/local` repositories; the workspace
179+
* side is added by the resolver (workspace-dependent, per-session).
180+
* @returns {string[]} defaultGlobalRepos keys (unordered)
181+
*/
182+
export function globalDeclaredTypes() {
183+
return Object.keys(loadAgentsConfig().packageResolution.defaultGlobalRepos);
184+
}
185+
186+
/**
187+
* Repo-agnostic "enforce on startup" policy check for a single package type.
188+
* `enforceOnStartup: true` means all governed types; an array names a subset.
189+
* NOTE: this is a pure policy check — the caller still gates on the type being
190+
* governed + resolved this session.
191+
* @param {string} type
192+
* @returns {boolean}
193+
*/
194+
export function isEnforceOnStartup(type) {
195+
const e = loadAgentsConfig().packageResolution.enforceOnStartup;
196+
if (e === true) return true;
197+
return Array.isArray(e) && e.includes(type);
198+
}
199+
200+
function normalizeLogLevel(level) {
201+
const s = typeof level === "string" ? level.toLowerCase() : "";
202+
const allowed = new Set(["silent", "debug", "info", "warn", "error"]);
203+
return allowed.has(s) ? s : DEFAULT_LOG_LEVEL;
204+
}
205+
206+
function normalizeCacheTtlDays(days) {
207+
if (days === 0) return 0;
208+
if (typeof days !== "number" || !Number.isFinite(days) || days < 0) {
209+
return DEFAULT_CACHE_TTL_DAYS;
210+
}
211+
return Math.floor(days);
212+
}
213+
214+
/**
215+
* Normalize the `enforceOnStartup` policy: `true` (all governed types) or an
216+
* array of type-name strings. Anything else -> `[]` (nothing eager). Malformed
217+
* array entries (non-strings / blanks) are dropped; whether a named type is
218+
* actually governed is validated later (per-session, where governance is known).
219+
* @returns {true | string[]}
220+
*/
221+
export function normalizeEnforceOnStartup(raw) {
222+
if (raw === true) return true;
223+
if (!Array.isArray(raw)) return [];
224+
const out = [];
225+
for (const t of raw) {
226+
if (typeof t === "string" && t.trim()) out.push(t.trim());
227+
}
228+
return out;
229+
}
230+
231+
/** Trim string repo keys; drop empty values. */
232+
export function normalizeRepoMap(raw) {
233+
if (!raw || typeof raw !== "object") return {};
234+
const out = {};
235+
for (const [type, key] of Object.entries(raw)) {
236+
if (typeof key === "string" && key.trim()) out[type] = key.trim();
237+
}
238+
return out;
239+
}

0 commit comments

Comments
 (0)