@genrupt/cli 0.1.0 → 0.1.2

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/dist/doctor.js ADDED
@@ -0,0 +1,225 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { getValidConfig } from "./auth.js";
4
+ import { CLI_VERSION } from "./constants.js";
5
+ import { listRemoteMcpTools } from "./mcpClient.js";
6
+ import { GENRUPT_SKILL_NAMES, computeInstalledSkillDigests, defaultTargetForAgent, detectAgent, getInstalledRuntimeManifestPath, readInstalledRuntimeManifest, } from "./skills.js";
7
+ import { emitRuntimeTelemetry } from "./telemetry.js";
8
+ import { CLI_PACKAGE_NAME, fetchLatestCliVersion, isVersionNewer } from "./version.js";
9
+ function normalizeAgent(agent) {
10
+ const normalized = agent?.trim().toLowerCase();
11
+ if (!normalized)
12
+ return detectAgent();
13
+ if (normalized === "claude-code")
14
+ return "claude";
15
+ if (normalized === "claude" || normalized === "codex" || normalized === "cursor") {
16
+ return normalized;
17
+ }
18
+ throw new Error("--agent must be claude, codex, or cursor.");
19
+ }
20
+ function icon(status) {
21
+ if (status === "ok")
22
+ return "OK";
23
+ if (status === "warn")
24
+ return "WARN";
25
+ return "FAIL";
26
+ }
27
+ function buildSkillsCheck(params) {
28
+ const missingSkills = GENRUPT_SKILL_NAMES.filter((skillName) => !existsSync(path.join(params.targetRoot, skillName, "SKILL.md")));
29
+ if (missingSkills.length > 0) {
30
+ return {
31
+ label: "Skills",
32
+ status: "fail",
33
+ detail: `Missing ${missingSkills.length} skill(s) for ${params.agent}: ${missingSkills.join(", ")}`,
34
+ remediation: `Run: npx -y ${CLI_PACKAGE_NAME}@latest agent install --agent ${params.agent}`,
35
+ };
36
+ }
37
+ return {
38
+ label: "Skills",
39
+ status: "ok",
40
+ detail: `Installed for ${params.agent} at ${params.targetRoot}`,
41
+ };
42
+ }
43
+ async function buildRuntimeManifestCheck(targetRoot) {
44
+ const manifest = await readInstalledRuntimeManifest(targetRoot);
45
+ if (!manifest) {
46
+ return {
47
+ label: "Runtime manifest",
48
+ status: "warn",
49
+ detail: `No installed runtime manifest found at ${getInstalledRuntimeManifestPath(targetRoot)}`,
50
+ remediation: `Run: npx -y ${CLI_PACKAGE_NAME}@latest agent install`,
51
+ };
52
+ }
53
+ return {
54
+ label: "Runtime manifest",
55
+ status: "ok",
56
+ detail: `Skills ${manifest.version}, requires ${manifest.cliPackage} >= ${manifest.requiresCliVersion}`,
57
+ };
58
+ }
59
+ async function buildSkillIntegrityCheck(targetRoot) {
60
+ const manifest = await readInstalledRuntimeManifest(targetRoot);
61
+ if (!manifest?.installedSkills?.length) {
62
+ return {
63
+ label: "Skill integrity",
64
+ status: "warn",
65
+ detail: "No installed skill digests recorded.",
66
+ remediation: `Run: npx -y ${CLI_PACKAGE_NAME}@latest agent install`,
67
+ };
68
+ }
69
+ try {
70
+ const currentDigests = await computeInstalledSkillDigests(targetRoot);
71
+ const expectedByName = new Map(manifest.installedSkills.map((skill) => [skill.name, skill.digest]));
72
+ const changed = currentDigests.filter((skill) => expectedByName.get(skill.name) !== skill.digest);
73
+ if (changed.length > 0) {
74
+ return {
75
+ label: "Skill integrity",
76
+ status: "fail",
77
+ detail: `Changed skill file(s): ${changed.map((skill) => skill.name).join(", ")}`,
78
+ remediation: `Run: npx -y ${CLI_PACKAGE_NAME}@latest agent install`,
79
+ };
80
+ }
81
+ return {
82
+ label: "Skill integrity",
83
+ status: "ok",
84
+ detail: `Verified ${currentDigests.length} installed skill digest(s).`,
85
+ };
86
+ }
87
+ catch (error) {
88
+ const message = error instanceof Error ? error.message : String(error);
89
+ return {
90
+ label: "Skill integrity",
91
+ status: "fail",
92
+ detail: message,
93
+ remediation: `Run: npx -y ${CLI_PACKAGE_NAME}@latest agent install`,
94
+ };
95
+ }
96
+ }
97
+ export async function runDoctor(options = {}) {
98
+ const startedAt = Date.now();
99
+ const checks = [];
100
+ const agent = normalizeAgent(options.agent);
101
+ const targetRoot = options.target?.trim() || defaultTargetForAgent(agent);
102
+ let latestCliVersion;
103
+ let staleCliDetected = false;
104
+ checks.push({
105
+ label: "CLI",
106
+ status: "ok",
107
+ detail: `${CLI_PACKAGE_NAME} ${CLI_VERSION}`,
108
+ });
109
+ try {
110
+ const latestVersion = await fetchLatestCliVersion();
111
+ latestCliVersion = latestVersion;
112
+ if (latestVersion && isVersionNewer(latestVersion, CLI_VERSION)) {
113
+ staleCliDetected = true;
114
+ checks.push({
115
+ label: "CLI update",
116
+ status: "warn",
117
+ detail: `Latest published CLI is ${latestVersion}; current is ${CLI_VERSION}`,
118
+ remediation: `Run: npm install -g ${CLI_PACKAGE_NAME}@latest`,
119
+ });
120
+ }
121
+ else {
122
+ checks.push({
123
+ label: "CLI update",
124
+ status: "ok",
125
+ detail: latestVersion
126
+ ? `No newer published CLI found (published: ${latestVersion}, current: ${CLI_VERSION}).`
127
+ : "No newer CLI found.",
128
+ });
129
+ }
130
+ }
131
+ catch (error) {
132
+ const message = error instanceof Error ? error.message : String(error);
133
+ checks.push({
134
+ label: "CLI update",
135
+ status: "warn",
136
+ detail: `Could not check npm registry: ${message}`,
137
+ });
138
+ }
139
+ let authenticated = false;
140
+ try {
141
+ const config = await getValidConfig();
142
+ authenticated = true;
143
+ checks.push({
144
+ label: "Auth",
145
+ status: "ok",
146
+ detail: `Authenticated for ${config.origin}`,
147
+ });
148
+ }
149
+ catch {
150
+ checks.push({
151
+ label: "Auth",
152
+ status: "fail",
153
+ detail: "Not authenticated.",
154
+ remediation: "Run: genrupt auth login",
155
+ });
156
+ }
157
+ if (authenticated) {
158
+ try {
159
+ const tools = await listRemoteMcpTools();
160
+ checks.push({
161
+ label: "Remote MCP",
162
+ status: "ok",
163
+ detail: `Listed ${tools.tools.length} tool(s).`,
164
+ });
165
+ }
166
+ catch (error) {
167
+ const message = error instanceof Error ? error.message : String(error);
168
+ checks.push({
169
+ label: "Remote MCP",
170
+ status: "fail",
171
+ detail: message,
172
+ remediation: "Run: genrupt auth login, then retry genrupt doctor",
173
+ });
174
+ }
175
+ }
176
+ checks.push(buildSkillsCheck({ agent, targetRoot }));
177
+ checks.push(await buildRuntimeManifestCheck(targetRoot));
178
+ checks.push(await buildSkillIntegrityCheck(targetRoot));
179
+ const failed = checks.some((check) => check.status === "fail");
180
+ const warned = checks.some((check) => check.status === "warn");
181
+ if (options.json) {
182
+ console.log(JSON.stringify({ ok: !failed, warned, agent, targetRoot, checks }, null, 2));
183
+ }
184
+ else {
185
+ console.log();
186
+ console.log("Genrupt doctor");
187
+ console.log();
188
+ for (const check of checks) {
189
+ console.log(`[${icon(check.status)}] ${check.label}: ${check.detail}`);
190
+ if (check.remediation) {
191
+ console.log(` ${check.remediation}`);
192
+ }
193
+ }
194
+ }
195
+ if (staleCliDetected && latestCliVersion) {
196
+ await emitRuntimeTelemetry({
197
+ eventType: "stale_cli_detected",
198
+ command: "doctor",
199
+ agent,
200
+ latestCliVersion,
201
+ durationMs: Date.now() - startedAt,
202
+ metadata: {
203
+ source: "doctor",
204
+ },
205
+ });
206
+ }
207
+ if (failed) {
208
+ const failedChecks = checks.filter((check) => check.status === "fail");
209
+ const warnedChecks = checks.filter((check) => check.status === "warn");
210
+ await emitRuntimeTelemetry({
211
+ eventType: "doctor_failed",
212
+ command: "doctor",
213
+ agent,
214
+ latestCliVersion,
215
+ durationMs: Date.now() - startedAt,
216
+ metadata: {
217
+ failedChecks: failedChecks.map((check) => check.label),
218
+ failedCount: failedChecks.length,
219
+ warnedChecks: warnedChecks.map((check) => check.label),
220
+ warnedCount: warnedChecks.length,
221
+ },
222
+ });
223
+ throw new Error("Genrupt doctor found failing checks.");
224
+ }
225
+ }
@@ -3,11 +3,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { CLI_VERSION } from "./constants.js";
5
5
  import { listRemoteMcpTools, callRemoteMcpTool } from "./mcpClient.js";
6
- import { uploadProductReferenceImages } from "./upload.js";
6
+ import { uploadProductReferenceImages, uploadReferenceMedia, } from "./upload.js";
7
+ import { classifyCliError, emitRuntimeTelemetry } from "./telemetry.js";
7
8
  const LOCAL_UPLOAD_TOOL = {
8
9
  name: "upload_product_reference_images",
9
10
  title: "Upload local product reference images",
10
- description: "Use when the user provides local image file paths from this computer and wants them uploaded to Genrupt as product reference assets. This local CLI MCP tool can read local files, uploads them through Genrupt's presigned upload flow, completes registration, and returns sourceAssetIds for create_product_reference_sheet. Supports JPEG, PNG, and WebP images up to 50 MB each.",
11
+ description: "Use when the user provides local image file or folder paths from this computer and needs ProductReferenceAsset sourceAssetIds for create_product_reference_sheet. For custom video reference images, videos, or audio, prefer upload_reference_media. This local CLI MCP tool can read local files/folders, uploads supported images through Genrupt's presigned upload flow, completes registration, and returns sourceAssetIds plus referenceImageUrls. Folder paths include JPEG, PNG, and WebP files directly inside the folder. Supports JPEG, PNG, and WebP images up to 50 MB each.",
11
12
  inputSchema: {
12
13
  type: "object",
13
14
  properties: {
@@ -15,7 +16,7 @@ const LOCAL_UPLOAD_TOOL = {
15
16
  type: "array",
16
17
  minItems: 1,
17
18
  items: { type: "string", minLength: 1 },
18
- description: "Absolute or working-directory-relative local image file paths.",
19
+ description: "Absolute or working-directory-relative local image file paths or folders containing images.",
19
20
  },
20
21
  asin: {
21
22
  type: "string",
@@ -42,6 +43,35 @@ const LOCAL_UPLOAD_TOOL = {
42
43
  additionalProperties: false,
43
44
  },
44
45
  };
46
+ const LOCAL_REFERENCE_MEDIA_UPLOAD_TOOL = {
47
+ name: "upload_reference_media",
48
+ title: "Upload local reference media",
49
+ description: "Use when the user provides local image, video, audio, or folder paths from this computer for custom video reference inputs. This local CLI MCP tool can read local files/folders, upload supported media through Genrupt's presigned reference-asset flow, and return typed referenceImageUrls, referenceVideoUrls, and referenceAudioUrls for generate_video. Folder paths include supported files directly inside the folder. Supports JPEG, PNG, WebP, MP4, MOV, WebM, MP3, WAV, M4A, and OGG files up to 50 MB each. This does not create sourceAssetIds for Product Reference Sheets; use upload_product_reference_images for that.",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ paths: {
54
+ type: "array",
55
+ minItems: 1,
56
+ items: { type: "string", minLength: 1 },
57
+ description: "Absolute or working-directory-relative local media file paths or folders containing supported media.",
58
+ },
59
+ mediaKind: {
60
+ type: "string",
61
+ enum: ["media", "image", "video", "audio"],
62
+ default: "media",
63
+ description: "Optional media filter. Use media to auto-detect supported image, video, and audio files.",
64
+ },
65
+ projectId: {
66
+ type: "string",
67
+ minLength: 1,
68
+ description: "Optional Genrupt project ID to scope uploaded reference media.",
69
+ },
70
+ },
71
+ required: ["paths"],
72
+ additionalProperties: false,
73
+ },
74
+ };
45
75
  function buildToolResult(payload) {
46
76
  return {
47
77
  content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
@@ -75,11 +105,11 @@ function buildToolError(error) {
75
105
  }
76
106
  function normalizeStringArray(value) {
77
107
  if (!Array.isArray(value)) {
78
- throw new Error("paths must be an array of image file paths.");
108
+ throw new Error("paths must be an array of local file or folder paths.");
79
109
  }
80
110
  const paths = value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
81
111
  if (paths.length === 0) {
82
- throw new Error("paths must include at least one image file path.");
112
+ throw new Error("paths must include at least one local file or folder path.");
83
113
  }
84
114
  return paths;
85
115
  }
@@ -97,6 +127,23 @@ async function handleLocalUploadTool(args) {
97
127
  });
98
128
  return buildToolResult(payload);
99
129
  }
130
+ function normalizeMediaKind(value) {
131
+ if (value === undefined || value === null || value === "") {
132
+ return "media";
133
+ }
134
+ if (value === "media" || value === "image" || value === "video" || value === "audio") {
135
+ return value;
136
+ }
137
+ throw new Error("mediaKind must be media, image, video, or audio.");
138
+ }
139
+ async function handleLocalReferenceMediaUploadTool(args) {
140
+ const payload = await uploadReferenceMedia({
141
+ paths: normalizeStringArray(args.paths),
142
+ mode: normalizeMediaKind(args.mediaKind),
143
+ projectId: getOptionalString(args, "projectId"),
144
+ });
145
+ return buildToolResult(payload);
146
+ }
100
147
  export async function serveLocalMcp() {
101
148
  const server = new Server({
102
149
  name: "genrupt-cli",
@@ -105,7 +152,7 @@ export async function serveLocalMcp() {
105
152
  capabilities: {
106
153
  tools: {},
107
154
  },
108
- instructions: "Genrupt local bridge. Use upload_product_reference_images for local file paths, then use Genrupt remote MCP tools for market analysis, listing images, A+ content, product reference sheets, and video workflows.",
155
+ instructions: "Genrupt local bridge. Use upload_reference_media for local image/video/audio reference paths in custom video workflows. Use upload_product_reference_images when product reference sheet workflows need sourceAssetIds. Then use Genrupt remote MCP tools for market analysis, listing images, A+ content, product reference sheets, and video workflows.",
109
156
  });
110
157
  server.setRequestHandler(ListToolsRequestSchema, async () => {
111
158
  try {
@@ -113,14 +160,16 @@ export async function serveLocalMcp() {
113
160
  return {
114
161
  tools: [
115
162
  LOCAL_UPLOAD_TOOL,
116
- ...remote.tools.filter((tool) => tool.name !== LOCAL_UPLOAD_TOOL.name),
163
+ LOCAL_REFERENCE_MEDIA_UPLOAD_TOOL,
164
+ ...remote.tools.filter((tool) => tool.name !== LOCAL_UPLOAD_TOOL.name &&
165
+ tool.name !== LOCAL_REFERENCE_MEDIA_UPLOAD_TOOL.name),
117
166
  ],
118
167
  };
119
168
  }
120
169
  catch (error) {
121
170
  console.error(`[genrupt] Failed to list remote MCP tools: ${error instanceof Error ? error.message : String(error)}`);
122
171
  return {
123
- tools: [LOCAL_UPLOAD_TOOL],
172
+ tools: [LOCAL_UPLOAD_TOOL, LOCAL_REFERENCE_MEDIA_UPLOAD_TOOL],
124
173
  };
125
174
  }
126
175
  });
@@ -132,9 +181,25 @@ export async function serveLocalMcp() {
132
181
  if (request.params.name === LOCAL_UPLOAD_TOOL.name) {
133
182
  return await handleLocalUploadTool(args);
134
183
  }
184
+ if (request.params.name === LOCAL_REFERENCE_MEDIA_UPLOAD_TOOL.name) {
185
+ return await handleLocalReferenceMediaUploadTool(args);
186
+ }
135
187
  return await callRemoteMcpTool(request.params.name, args);
136
188
  }
137
189
  catch (error) {
190
+ if (request.params.name === LOCAL_UPLOAD_TOOL.name ||
191
+ request.params.name === LOCAL_REFERENCE_MEDIA_UPLOAD_TOOL.name) {
192
+ await emitRuntimeTelemetry({
193
+ eventType: "upload_failed",
194
+ command: request.params.name,
195
+ errorCode: classifyCliError(error, "LOCAL_MCP_UPLOAD_FAILED"),
196
+ metadata: {
197
+ uploadType: request.params.name === LOCAL_REFERENCE_MEDIA_UPLOAD_TOOL.name
198
+ ? getOptionalString(args, "mediaKind") ?? "media"
199
+ : "product_reference_images",
200
+ },
201
+ });
202
+ }
138
203
  return buildToolError(error);
139
204
  }
140
205
  });
package/dist/skills.js ADDED
@@ -0,0 +1,219 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import crypto from "node:crypto";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { CLI_VERSION, DEFAULT_ORIGIN } from "./constants.js";
7
+ import { isVersionAtLeast } from "./version.js";
8
+ export const GENRUPT_SKILL_NAMES = [
9
+ "genrupt-listing-video",
10
+ "genrupt-custom-video",
11
+ "genrupt-listing-images",
12
+ "genrupt-aplus-content",
13
+ "genrupt-product-variations",
14
+ ];
15
+ const SKILL_FILES = [
16
+ "SKILL.md",
17
+ "agents/openai.yaml",
18
+ "references/local-reference-assets.md",
19
+ "assets/icon.svg",
20
+ ];
21
+ const CURSOR_PLUGIN_FILES = [
22
+ ".cursor-plugin/plugin.json",
23
+ "VERSION",
24
+ "README.md",
25
+ "runtime.json",
26
+ ];
27
+ function normalizeOrigin(origin) {
28
+ return (origin?.trim() || DEFAULT_ORIGIN).replace(/\/$/, "");
29
+ }
30
+ function normalizeAgent(agent) {
31
+ const normalized = agent?.trim().toLowerCase();
32
+ if (!normalized)
33
+ return undefined;
34
+ if (normalized === "claude-code")
35
+ return "claude";
36
+ if (normalized === "claude" || normalized === "codex" || normalized === "cursor") {
37
+ return normalized;
38
+ }
39
+ throw new Error("--agent must be claude, codex, or cursor.");
40
+ }
41
+ export function detectAgent() {
42
+ const home = os.homedir();
43
+ if (process.env.CLAUDECODE || process.env.CLAUDE_CODE)
44
+ return "claude";
45
+ if (process.env.CURSOR_TRACE_ID || process.env.CURSOR_SESSION_ID)
46
+ return "cursor";
47
+ if (process.env.CODEX_HOME || process.env.OPENAI_CODEX)
48
+ return "codex";
49
+ if (pathExistsSync(path.join(home, ".claude")))
50
+ return "claude";
51
+ if (pathExistsSync(path.join(home, ".codex")))
52
+ return "codex";
53
+ if (pathExistsSync(path.join(home, ".cursor")))
54
+ return "cursor";
55
+ return "claude";
56
+ }
57
+ function pathExistsSync(filePath) {
58
+ return existsSync(filePath);
59
+ }
60
+ export function defaultTargetForAgent(agent) {
61
+ const home = os.homedir();
62
+ if (agent === "claude")
63
+ return path.join(home, ".claude", "skills");
64
+ if (agent === "codex")
65
+ return path.join(home, ".codex", "skills");
66
+ return path.join(home, ".cursor", "plugins", "genrupt");
67
+ }
68
+ function targetRootForSkillInstall(params) {
69
+ return params.target?.trim() || defaultTargetForAgent(params.agent);
70
+ }
71
+ async function fetchText(url) {
72
+ const response = await fetch(url);
73
+ if (!response.ok) {
74
+ throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
75
+ }
76
+ return response.text();
77
+ }
78
+ async function downloadPublicFile(params) {
79
+ const relativePath = params.relativePath
80
+ .split("/")
81
+ .map((segment) => encodeURIComponent(segment))
82
+ .join("/");
83
+ return fetchText(`${params.origin}/downloads/genrupt-skills/${relativePath}`);
84
+ }
85
+ function parseRuntimeManifest(raw) {
86
+ const parsed = JSON.parse(raw);
87
+ if (parsed.name !== "genrupt-agent-runtime" ||
88
+ typeof parsed.version !== "string" ||
89
+ typeof parsed.requiresCliVersion !== "string" ||
90
+ typeof parsed.cliPackage !== "string" ||
91
+ typeof parsed.cliBinary !== "string" ||
92
+ !Array.isArray(parsed.skills)) {
93
+ throw new Error("Invalid Genrupt runtime manifest.");
94
+ }
95
+ return {
96
+ name: parsed.name,
97
+ version: parsed.version,
98
+ requiresCliVersion: parsed.requiresCliVersion,
99
+ cliPackage: parsed.cliPackage,
100
+ cliBinary: parsed.cliBinary,
101
+ skills: parsed.skills.filter((skill) => typeof skill === "string"),
102
+ installedSkills: Array.isArray(parsed.installedSkills)
103
+ ? parsed.installedSkills.flatMap((skill) => {
104
+ if (skill &&
105
+ typeof skill === "object" &&
106
+ "name" in skill &&
107
+ "digest" in skill &&
108
+ typeof skill.name === "string" &&
109
+ typeof skill.digest === "string") {
110
+ return [{ name: skill.name, digest: skill.digest }];
111
+ }
112
+ return [];
113
+ })
114
+ : undefined,
115
+ };
116
+ }
117
+ export async function downloadRuntimeManifest(origin) {
118
+ return parseRuntimeManifest(await downloadPublicFile({
119
+ origin,
120
+ relativePath: "runtime.json",
121
+ }));
122
+ }
123
+ export function getInstalledRuntimeManifestPath(targetRoot) {
124
+ return path.join(targetRoot, ".genrupt-runtime.json");
125
+ }
126
+ export async function readInstalledRuntimeManifest(targetRoot) {
127
+ try {
128
+ const raw = await readFile(getInstalledRuntimeManifestPath(targetRoot), "utf8");
129
+ return parseRuntimeManifest(raw.replace(/^\uFEFF/, ""));
130
+ }
131
+ catch (error) {
132
+ if (error.code === "ENOENT") {
133
+ return null;
134
+ }
135
+ return null;
136
+ }
137
+ }
138
+ async function computeInstalledSkillDigest(params) {
139
+ const hash = crypto.createHash("sha256");
140
+ for (const file of SKILL_FILES) {
141
+ const filePath = path.join(params.targetRoot, params.skillName, ...file.split("/"));
142
+ hash.update(file);
143
+ hash.update(await readFile(filePath));
144
+ }
145
+ return `sha256:${hash.digest("hex")}`;
146
+ }
147
+ export async function computeInstalledSkillDigests(targetRoot) {
148
+ return Promise.all(GENRUPT_SKILL_NAMES.map(async (skillName) => ({
149
+ name: skillName,
150
+ digest: await computeInstalledSkillDigest({ targetRoot, skillName }),
151
+ })));
152
+ }
153
+ async function writeInstalledRuntimeManifest(params) {
154
+ const installedSkills = await computeInstalledSkillDigests(params.targetRoot);
155
+ await writeFile(getInstalledRuntimeManifestPath(params.targetRoot), `${JSON.stringify({
156
+ ...params.runtime,
157
+ installedSkills,
158
+ agent: params.agent,
159
+ origin: params.origin,
160
+ installedCliVersion: CLI_VERSION,
161
+ installedAt: new Date().toISOString(),
162
+ }, null, 2)}\n`, "utf8");
163
+ }
164
+ function assertRuntimeSupported(runtime) {
165
+ if (isVersionAtLeast(CLI_VERSION, runtime.requiresCliVersion)) {
166
+ return;
167
+ }
168
+ throw new Error(`These Genrupt skills require ${runtime.cliPackage} >= ${runtime.requiresCliVersion}; installed CLI is ${CLI_VERSION}.\n` +
169
+ `Update with: npm install -g ${runtime.cliPackage}@latest\n` +
170
+ `Or run once with: npx -y ${runtime.cliPackage}@latest agent install`);
171
+ }
172
+ async function writeDownloadedFile(params) {
173
+ const contents = await downloadPublicFile({
174
+ origin: params.origin,
175
+ relativePath: params.relativePath,
176
+ });
177
+ await mkdir(path.dirname(params.targetPath), { recursive: true });
178
+ await writeFile(params.targetPath, contents, "utf8");
179
+ }
180
+ async function installSkillFolder(params) {
181
+ const skillTarget = path.join(params.targetRoot, params.skillName);
182
+ await rm(skillTarget, { recursive: true, force: true });
183
+ for (const file of SKILL_FILES) {
184
+ await writeDownloadedFile({
185
+ origin: params.origin,
186
+ relativePath: `${params.skillName}/${file}`,
187
+ targetPath: path.join(skillTarget, ...file.split("/")),
188
+ });
189
+ }
190
+ }
191
+ async function installCursorPluginShell(params) {
192
+ for (const file of CURSOR_PLUGIN_FILES) {
193
+ await writeDownloadedFile({
194
+ origin: params.origin,
195
+ relativePath: file,
196
+ targetPath: path.join(params.targetRoot, ...file.split("/")),
197
+ });
198
+ }
199
+ }
200
+ export async function installSkills(options = {}) {
201
+ const agent = normalizeAgent(options.agent) ?? detectAgent();
202
+ const targetRoot = targetRootForSkillInstall({ agent, target: options.target });
203
+ const origin = normalizeOrigin(options.origin);
204
+ const runtime = await downloadRuntimeManifest(origin);
205
+ if (!options.skipRuntimeCheck) {
206
+ assertRuntimeSupported(runtime);
207
+ }
208
+ await mkdir(targetRoot, { recursive: true });
209
+ if (agent === "cursor") {
210
+ await installCursorPluginShell({ origin, targetRoot });
211
+ }
212
+ for (const skillName of GENRUPT_SKILL_NAMES) {
213
+ await installSkillFolder({ origin, targetRoot, skillName });
214
+ }
215
+ await writeInstalledRuntimeManifest({ targetRoot, agent, origin, runtime });
216
+ console.log(`Installed ${GENRUPT_SKILL_NAMES.length} Genrupt skills for ${agent}.`);
217
+ console.log(`Runtime: ${runtime.version} (requires ${runtime.cliPackage} >= ${runtime.requiresCliVersion})`);
218
+ console.log(targetRoot);
219
+ }