@bonnard/agentops
Advanced tools
+131
-9
@@ -7,2 +7,3 @@ #!/usr/bin/env node | ||
| import http from "node:http"; | ||
| import readline from "node:readline/promises"; | ||
| import open from "open"; | ||
@@ -155,2 +156,11 @@ import fs from "node:fs"; | ||
| } | ||
| var ApiError = class extends Error { | ||
| status; | ||
| code; | ||
| constructor(message, status, code) { | ||
| super(message); | ||
| this.status = status; | ||
| this.code = code; | ||
| } | ||
| }; | ||
| /** | ||
@@ -161,3 +171,12 @@ * Download a skill bundle with metadata headers (version, etc.). | ||
| const res = await fetchWithRefresh(`${baseUrl}${apiPath}`, { headers: getHeaders() }, baseUrl); | ||
| if (!res.ok) throw new Error(`Download failed: ${res.status}`); | ||
| if (!res.ok) { | ||
| let code; | ||
| let message = `Download failed: ${res.status}`; | ||
| try { | ||
| const body = await res.json(); | ||
| if (body.error?.message) message = body.error.message; | ||
| code = body.error?.code; | ||
| } catch {} | ||
| throw new ApiError(message, res.status, code); | ||
| } | ||
| const arrayBuf = await res.arrayBuffer(); | ||
@@ -213,6 +232,13 @@ const versionHeader = res.headers.get("x-skill-version"); | ||
| } | ||
| const creds = validateCredentials(await callbackRes.json()); | ||
| if (!creds) { | ||
| console.error(pc.red("Server returned an unexpected response format.")); | ||
| process.exit(1); | ||
| const data = await callbackRes.json(); | ||
| let creds; | ||
| if (data.needsOnboarding === true) { | ||
| creds = await onboardNewUser(data, baseUrl); | ||
| if (!creds) process.exit(1); | ||
| } else { | ||
| creds = validateCredentials(data); | ||
| if (!creds) { | ||
| console.error(pc.red("Server returned an unexpected response format.")); | ||
| process.exit(1); | ||
| } | ||
| } | ||
@@ -299,2 +325,57 @@ saveCredentials(creds); | ||
| } | ||
| /** | ||
| * First-time user path: the server returned needsOnboarding=true because | ||
| * the authenticated user has no WorkOS organization memberships. Prompt | ||
| * for an org name, call POST /api/auth/create-org with the pre-provisioned | ||
| * access token, and return the finalized credentials. | ||
| */ | ||
| async function onboardNewUser(pending, baseUrl) { | ||
| const accessToken = typeof pending.accessToken === "string" ? pending.accessToken : null; | ||
| const refreshToken = typeof pending.refreshToken === "string" ? pending.refreshToken : null; | ||
| const userFromCallback = pending.user; | ||
| const email = userFromCallback && typeof userFromCallback.email === "string" ? userFromCallback.email : null; | ||
| if (!accessToken || !email) { | ||
| console.error(pc.red("Server returned an unexpected onboarding response.")); | ||
| return null; | ||
| } | ||
| console.log(); | ||
| console.log(pc.bold(`Welcome, ${email}!`)); | ||
| console.log(pc.dim("You don't belong to an organization yet. Name one to get started.")); | ||
| console.log(); | ||
| const rl = readline.createInterface({ | ||
| input: process.stdin, | ||
| output: process.stdout | ||
| }); | ||
| let orgName; | ||
| try { | ||
| orgName = (await rl.question("Organization name: ")).trim(); | ||
| } finally { | ||
| rl.close(); | ||
| } | ||
| if (!orgName) { | ||
| console.error(pc.red("Organization name is required.")); | ||
| return null; | ||
| } | ||
| console.log(pc.dim(`Creating "${orgName}"...`)); | ||
| const res = await fetch(`${baseUrl}/api/auth/create-org`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| Authorization: `Bearer ${accessToken}` | ||
| }, | ||
| body: JSON.stringify({ name: orgName }) | ||
| }); | ||
| if (!res.ok) { | ||
| const body = await res.text(); | ||
| console.error(pc.red(`Failed to create organization: ${body}`)); | ||
| return null; | ||
| } | ||
| const result = await res.json(); | ||
| return { | ||
| accessToken, | ||
| refreshToken, | ||
| user: result.user, | ||
| org: result.org | ||
| }; | ||
| } | ||
| /** Static HTML page — never interpolate user/query data into this */ | ||
@@ -806,4 +887,7 @@ function resultPage(title, message) { | ||
| } catch (err) { | ||
| console.error(pc.red(`Download failed: ${err.message}`)); | ||
| console.log(pc.dim(` The skill exists but has no published bundle yet.`)); | ||
| if (err instanceof ApiError) { | ||
| console.error(pc.red(err.message)); | ||
| if (err.code === "feature_gated") console.log(pc.dim(" agentops whoami — check your current plan")); | ||
| else if (err.code === "invalid_state") console.log(pc.dim(" The skill exists but has no published bundle yet.")); | ||
| } else console.error(pc.red(`Download failed: ${err.message}`)); | ||
| process.exit(1); | ||
@@ -906,2 +990,3 @@ } | ||
| if (err.error?.code === "not_found") console.log(pc.dim(` Search the library: agentops skills search ${name}`)); | ||
| else if (err.error?.code === "feature_gated") console.log(pc.dim(" agentops whoami — check your current plan")); | ||
| process.exit(1); | ||
@@ -1161,3 +1246,3 @@ } | ||
| message: `Large file: ${relPath} (${(size / 1024 / 1024).toFixed(1)}MB)`, | ||
| hint: "Skill bundles are limited to 10MB total. Consider assets/ for large resources." | ||
| hint: "Consider assets/ for large resources. Check your plan's bundle limit with: agentops whoami" | ||
| }); | ||
@@ -1413,3 +1498,3 @@ } | ||
| const seatsLimit = limits.maxSeats ?? "∞"; | ||
| const storageLimit = limits.storageQuotaBytes ? `${(limits.storageQuotaBytes / 1024 / 1024 / 1024).toFixed(0)} GB` : "∞"; | ||
| const storageLimit = limits.storageQuotaBytes ? formatBytes(limits.storageQuotaBytes) : "∞"; | ||
| const storageUsed = formatBytes(me.usage.storageBytes); | ||
@@ -1468,2 +1553,38 @@ const bundleLimit = `${limits.maxBundleSizeBytes / 1024 / 1024} MB`; | ||
| //#endregion | ||
| //#region src/commands/rollback.ts | ||
| async function rollbackCommand(spec, opts) { | ||
| if (!loadCredentials()) { | ||
| console.log(pc.yellow("Not logged in. Run: agentops login")); | ||
| process.exit(1); | ||
| } | ||
| let parsed; | ||
| try { | ||
| parsed = parseSkillSpec(spec); | ||
| } catch (err) { | ||
| console.error(pc.red(err.message)); | ||
| process.exit(1); | ||
| } | ||
| if (typeof parsed.version !== "number") { | ||
| console.error(pc.red("A specific version is required: agentops skills rollback <name>@v<N>")); | ||
| console.log(pc.dim(` e.g. agentops skills rollback ${parsed.name}@v1`)); | ||
| process.exit(1); | ||
| } | ||
| const baseUrl = getBaseUrl(opts.url); | ||
| console.log(pc.dim(`Rolling back "${parsed.name}" to v${parsed.version}...`)); | ||
| const res = await post(`/api/skills/${encodeURIComponent(parsed.name)}/rollback`, { version: parsed.version }, baseUrl); | ||
| if (!res.ok) { | ||
| const err = await res.json(); | ||
| console.error(pc.red(err.error?.message ?? `Error: ${res.status}`)); | ||
| if (err.error?.code === "feature_gated") { | ||
| console.log(pc.dim(` Version rollback requires the pro plan or higher.`)); | ||
| console.log(pc.dim(` Manage your subscription at https://agentops.bonnard.ai`)); | ||
| } else if (err.error?.code === "not_found") console.log(pc.dim(` Check available versions: agentops skills history ${parsed.name}`)); | ||
| else if (err.error?.code === "forbidden") console.log(pc.dim(` Only the skill author or an admin can roll back.`)); | ||
| process.exit(1); | ||
| } | ||
| const result = await res.json(); | ||
| console.log(pc.green(`✓ "${parsed.name}" rolled back to v${result.rolledBackFrom} — now published as v${result.version}`)); | ||
| console.log(pc.dim(` Install: agentops skills install ${parsed.name}`)); | ||
| } | ||
| //#endregion | ||
| //#region src/commands/delete.ts | ||
@@ -1521,2 +1642,3 @@ async function deleteCommand(name, opts) { | ||
| skills.command("history <name>").description("Show version history for a skill").option("--url <url>", "AgentOps server URL").action(historyCommand); | ||
| skills.command("rollback <spec>").description("Re-publish an older version as the new latest — use <name>@v<N> (pro+)").option("--url <url>", "AgentOps server URL").action(rollbackCommand); | ||
| skills.command("authored").description("Show skills you've authored (draft, submitted, published, rejected)").option("--url <url>", "AgentOps server URL").action(authoredCommand); | ||
@@ -1523,0 +1645,0 @@ skills.command("publish <name>").description("Publish a submitted skill (admin only)").option("--url <url>", "AgentOps server URL").action(publishCommand); |
+1
-1
| { | ||
| "name": "@bonnard/agentops", | ||
| "version": "0.7.7", | ||
| "version": "0.7.8", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "bin": { |
63018
7.79%1638
8.05%5
25%