identityforge
Advanced tools
| import { get } from "node:https"; | ||
| import { readConfig, updateConfig } from "./config.js"; | ||
| const REGISTRY_URL = "https://registry.npmjs.org/identityforge/latest"; | ||
| const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; | ||
| function parseVersion(value) { | ||
| const match = value.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/); | ||
| if (!match) | ||
| return undefined; | ||
| return { | ||
| major: Number(match[1]), | ||
| minor: Number(match[2]), | ||
| patch: Number(match[3]), | ||
| prerelease: match[4]?.split(".") ?? [], | ||
| }; | ||
| } | ||
| function compareVersions(left, right) { | ||
| for (const [a, b] of [ | ||
| [left.major, right.major], | ||
| [left.minor, right.minor], | ||
| [left.patch, right.patch], | ||
| ]) { | ||
| if (a !== b) | ||
| return a > b ? 1 : -1; | ||
| } | ||
| if (!left.prerelease.length || !right.prerelease.length) { | ||
| return left.prerelease.length === right.prerelease.length | ||
| ? 0 | ||
| : left.prerelease.length | ||
| ? -1 | ||
| : 1; | ||
| } | ||
| for (let i = 0; i < Math.max(left.prerelease.length, right.prerelease.length); i++) { | ||
| const a = left.prerelease[i]; | ||
| const b = right.prerelease[i]; | ||
| if (a === undefined) | ||
| return -1; | ||
| if (b === undefined) | ||
| return 1; | ||
| if (a === b) | ||
| continue; | ||
| const aNumber = /^\d+$/.test(a); | ||
| const bNumber = /^\d+$/.test(b); | ||
| if (aNumber && bNumber) | ||
| return Number(a) > Number(b) ? 1 : -1; | ||
| if (aNumber !== bNumber) | ||
| return aNumber ? -1 : 1; | ||
| return a > b ? 1 : -1; | ||
| } | ||
| return 0; | ||
| } | ||
| export function isVersionGreater(candidate, current) { | ||
| const left = parseVersion(candidate); | ||
| const right = parseVersion(current); | ||
| return left !== undefined && right !== undefined && compareVersions(left, right) > 0; | ||
| } | ||
| let updateNoticePrinted = false; | ||
| function printUpdateNotice(currentVersion, latestVersion) { | ||
| if (updateNoticePrinted) | ||
| return; | ||
| updateNoticePrinted = true; | ||
| process.stderr.write(`Identity Forge update available: ${currentVersion} -> ${latestVersion}. Update with npm i -g identityforge@latest.\n`); | ||
| } | ||
| function fetchLatestVersion() { | ||
| return new Promise((resolve) => { | ||
| let settled = false; | ||
| const finish = (version) => { | ||
| if (settled) | ||
| return; | ||
| settled = true; | ||
| resolve(version); | ||
| }; | ||
| const request = get(REGISTRY_URL, { headers: { Accept: "application/json" } }, (response) => { | ||
| let body = ""; | ||
| response.setEncoding("utf8"); | ||
| response.on("data", (chunk) => { | ||
| body += chunk; | ||
| }); | ||
| response.on("end", () => { | ||
| if (response.statusCode !== 200) | ||
| return finish(); | ||
| try { | ||
| const version = JSON.parse(body).version; | ||
| finish(typeof version === "string" && parseVersion(version) | ||
| ? version | ||
| : undefined); | ||
| } | ||
| catch { | ||
| finish(); | ||
| } | ||
| }); | ||
| response.on("error", () => finish()); | ||
| }); | ||
| request.on("socket", (socket) => socket.unref()); | ||
| request.on("error", () => finish()); | ||
| const timeout = setTimeout(() => { | ||
| request.destroy(); | ||
| finish(); | ||
| }, 3000); | ||
| timeout.unref(); | ||
| }); | ||
| } | ||
| export async function checkForUpdate(currentVersion) { | ||
| const config = readConfig(); | ||
| const cached = config.updateCheck; | ||
| if (cached?.latestVersion && isVersionGreater(cached.latestVersion, currentVersion)) { | ||
| printUpdateNotice(currentVersion, cached.latestVersion); | ||
| } | ||
| const checkedAt = cached ? Date.parse(cached.checkedAt) : Number.NaN; | ||
| if (Number.isFinite(checkedAt) && Date.now() - checkedAt < CHECK_INTERVAL_MS) | ||
| return; | ||
| let latestVersion; | ||
| try { | ||
| latestVersion = await fetchLatestVersion(); | ||
| if (latestVersion) { | ||
| if (isVersionGreater(latestVersion, currentVersion)) { | ||
| printUpdateNotice(currentVersion, latestVersion); | ||
| } | ||
| } | ||
| } | ||
| catch { | ||
| return; | ||
| } | ||
| finally { | ||
| try { | ||
| updateConfig({ | ||
| updateCheck: { | ||
| checkedAt: new Date().toISOString(), | ||
| ...(latestVersion ? { latestVersion } : {}), | ||
| }, | ||
| }); | ||
| } | ||
| catch { | ||
| // An unwritable config must never affect the command being run. | ||
| } | ||
| } | ||
| } | ||
| /** Start the check without giving the network request a chance to hold up a command. */ | ||
| export function startUpdateCheck(currentVersion) { | ||
| void checkForUpdate(currentVersion).catch(() => { }); | ||
| } |
+49
-1
| import { resolveApiKey, resolveApiUrl } from "./config.js"; | ||
| export const CLI_VERSION = "0.3.3"; | ||
| import { isVersionGreater } from "./updateCheck.js"; | ||
| export const CLI_VERSION = "0.3.5"; | ||
| let apiClient = "cli"; | ||
@@ -190,2 +191,12 @@ /** Select the client identity used by subsequent API requests. */ | ||
| } | ||
| let minimumCliWarningPrinted = false; | ||
| function noteMinimumCliVersion(res) { | ||
| if (minimumCliWarningPrinted) | ||
| return; | ||
| const minimum = res.headers.get("x-identityforge-min-cli"); | ||
| if (!minimum || !isVersionGreater(minimum, CLI_VERSION)) | ||
| return; | ||
| minimumCliWarningPrinted = true; | ||
| process.stderr.write(`Identity Forge server requires CLI ${minimum} or newer; installed CLI is ${CLI_VERSION}. Update with npm i -g identityforge@latest.\n`); | ||
| } | ||
| async function requestJson(path, init) { | ||
@@ -196,2 +207,3 @@ const res = await fetch(`${resolveApiUrl()}${path}`, { | ||
| }); | ||
| noteMinimumCliVersion(res); | ||
| if (!res.ok) | ||
@@ -326,2 +338,3 @@ throw await readError(res, path); | ||
| }); | ||
| noteMinimumCliVersion(res); | ||
| if (!res.ok) | ||
@@ -355,2 +368,3 @@ throw await readError(res, path); | ||
| }); | ||
| noteMinimumCliVersion(res); | ||
| if (!res.ok) | ||
@@ -382,2 +396,3 @@ throw await readError(res, path); | ||
| }); | ||
| noteMinimumCliVersion(res); | ||
| if (!res.ok) | ||
@@ -411,2 +426,3 @@ throw await readError(res, path); | ||
| }); | ||
| noteMinimumCliVersion(res); | ||
| if (!res.ok) | ||
@@ -512,2 +528,9 @@ throw await readError(res, path); | ||
| } | ||
| export async function searchTrademarks(input) { | ||
| return requestJson("/api/v1/naming/trademarks/search", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify(input), | ||
| }); | ||
| } | ||
| /** Create a private design kit from scratch or by forking a catalog kit. */ | ||
@@ -544,2 +567,6 @@ export async function createTheme(input) { | ||
| } | ||
| export async function deleteTheme(identifier) { | ||
| const json = await requestJson(`/api/v1/kits/${encodeURIComponent(identifier)}`, { method: "DELETE" }); | ||
| return { ...json.data, ...json.meta }; | ||
| } | ||
| export async function createBrandProject(input) { | ||
@@ -601,2 +628,22 @@ const json = await requestJson("/api/v1/brand-projects", { | ||
| } | ||
| export async function generateMockups(input) { | ||
| const { projectId, idempotencyKey, ...body } = input; | ||
| const json = await requestJson(`/api/v1/brand-projects/${encodeURIComponent(projectId)}/mockups`, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}), | ||
| }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
| return json.data; | ||
| } | ||
| export async function listMockupJobs(projectId) { | ||
| const json = await requestJson(`/api/v1/brand-projects/${encodeURIComponent(projectId)}/mockups`); | ||
| return json.data; | ||
| } | ||
| export async function getMockupJob(projectId, jobId) { | ||
| const json = await requestJson(`/api/v1/brand-projects/${encodeURIComponent(projectId)}/mockups/${encodeURIComponent(jobId)}`); | ||
| return json.data; | ||
| } | ||
| /** Change an existing share WITHOUT reissuing its URL. `password: null` clears | ||
@@ -777,2 +824,3 @@ * the protection; omitting it leaves whatever is set alone. 404 when the | ||
| }); | ||
| noteMinimumCliVersion(res); | ||
| if (!res.ok) | ||
@@ -779,0 +827,0 @@ throw await readError(res, path); |
+3
-3
@@ -7,3 +7,3 @@ import { spawn } from "node:child_process"; | ||
| import { resolveApiUrl } from "./config.js"; | ||
| const LOGIN_TIMEOUT_MS = 5 * 60 * 1000; | ||
| const LOGIN_TIMEOUT_MS = 10 * 60 * 1000; | ||
| const base64url = (buf) => buf.toString("base64url"); | ||
@@ -93,3 +93,3 @@ function openBrowser(url) { | ||
| const timer = setTimeout(() => { | ||
| done(() => reject(new Error("Timed out waiting for browser sign-in (5 minutes)."))); | ||
| done(() => reject(new Error("Timed out waiting for browser sign-in (10 minutes)."))); | ||
| }, LOGIN_TIMEOUT_MS); | ||
@@ -106,3 +106,3 @@ server.on("error", (e) => done(() => reject(e))); | ||
| const href = authorizeUrl.toString(); | ||
| process.stdout.write(`Opening your browser to sign in…\nIf it doesn't open, paste this URL:\n ${href}\n`); | ||
| process.stdout.write(`Opening your browser to sign in…\nNo account yet? Create one there and confirm the email; this login resumes automatically.\nIf it doesn't open, paste this URL:\n ${href}\n`); | ||
| openBrowser(href); | ||
@@ -109,0 +109,0 @@ }); |
+1
-1
| { | ||
| "name": "identityforge", | ||
| "version": "0.3.3", | ||
| "version": "0.3.5", | ||
| "description": "Agent-native brand naming, domain research, and design systems through one CLI and MCP server.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+25
-9
@@ -29,2 +29,6 @@ # identityforge | ||
| No account yet? The browser offers signup and waits for the confirmation email. | ||
| Follow the email link and confirm on the page; the pending CLI authorization then | ||
| resumes automatically. | ||
| Then tell your agent: | ||
@@ -38,6 +42,10 @@ | ||
| - `list_themes({ use: "data-dashboard" })` returns only kits genuinely fit for dashboards, ranked by fitness, each with a score from 0 to 100 computed from that kit's own tokens against the lane's criteria. | ||
| - `list_themes({ use: "data-dashboard" })` re-orders the catalog by fitness for dashboards rather than narrowing it, each kit carrying a score from 0 to 100 computed from its own tokens against the lane's criteria. | ||
| - `list_themes({ q: "calm fintech dashboard" })` runs a synonym-aware ranked search across moods, industries, and use cases. | ||
| - `search_themes` returns the whole catalog unranked so the agent can weigh a subtle brief itself. | ||
| Read the order, not the number. The score measures how well a kit is built against a lane's criteria, and every catalog kit is well built, so the scores cluster high and the documented cut excludes almost nothing. A lane changes which kits come first, not which kits come back. | ||
| For the data lanes there is a better answer than the score. Every kit summary carries a `charts` block measured on the mode the kit ships in: `minDeltaE` and `cvdMinDeltaE` (the closest pair of series colors, plain and under colorblind simulation), `distinct`, `hueFamilies`, `severityHeadroom` (how close any series comes to the destructive, warning and success roles — 0 means a category color IS a status color), `sequentialReady`, and `designed`, which is `false` when the kit defines no chart slots and the five were cycled from its brand roles. Those are measurements, so unlike the fitness score they can be stated to a user as the reason for a recommendation. | ||
| Use-case lanes: `data-dashboard`, `admin-internal-tool`, `saas-marketing`, `landing-page`, `ecommerce-store`, `portfolio`, `editorial-blog`, `docs-knowledge-base`, `mobile-app`, `business-services`, `community-social`, `ai-agent-chat`. | ||
@@ -65,8 +73,8 @@ | ||
| Once connected, your agent gets 56 tools. Browsing free kits needs no key; scopes are noted where they apply. | ||
| Once connected, your agent gets 61 tools. Browsing free kits needs no key; scopes are noted where they apply. | ||
| ### Find a design kit | ||
| - `list_themes`: browse the catalog as compact summaries. Filter to a judged use-case lane with `use`, run ranked search with `q`, page with `offset`, sort by `featured`, `popular`, `recent`, `name`, or `fit`. | ||
| - `search_themes`: return the whole catalog unranked, for briefs too subtle to filter on. | ||
| - `list_themes`: browse the catalog as compact summaries. Rank by use-case lane with `use`, run ranked search with `q`, page with `offset`, sort by `featured`, `popular`, `recent`, `name`, or `fit`. | ||
| - `search_themes`: return the whole catalog unranked, for briefs too subtle to rank against a lane. | ||
| - `similar_themes`: given a kit slug, find neighbours by palette, tags, and audience. | ||
@@ -169,6 +177,8 @@ - `match_palette`: given existing brand colors, rank kits by perceptual color distance. | ||
| - `update_theme`: edit one of your saved kits in place, keeping its slug and publication state so existing consumers follow the change. Overwrites the stored kit, but every save mints a version, so the replaced state stays readable through the version tools below. The slug itself cannot be renamed here, and `expectedUpdatedAt` turns a concurrent edit into a 409 instead of a silent overwrite. | ||
| - `delete_theme`: permanently delete one of your saved kits. Pass `confirm: true`; a kit still referenced by a brand project is refused with `409 kit_in_use`, so retire or repoint those references first. | ||
| - `create_brand_project` and `list_brand_projects`: the container for brand variations and a client share. | ||
| - `add_brand_variation`: attach a proposal to a project, with a kit plus optional name, domain, label, and notes. | ||
| - `update_brand_variation`: revise one proposal in place, including repointing it at a different kit. The client sees it on their next view. | ||
| - `remove_brand_variation`: permanently delete one proposal and its comments. Not undoable. | ||
| - `remove_brand_variation`: permanently delete one proposal and its comments. Pass `confirm: true`; it is not undoable. | ||
| - `revoke_brand_share`: permanently withdraw a client link. Pass `confirm: true`; sharing again mints a new token. | ||
| - `reorder_brand_variations`: set the order the client meets the directions in. Must list every variation exactly once. | ||
@@ -184,3 +194,3 @@ - `share_brand_project`: create or rotate a read-only `/p/<token>` client share link, optionally password protected. | ||
| - `add_brand_layer`: compose one record onto the brand, recording the revision it is at now so a later read can report a change rather than apply it silently. One tool for all three axes via `axis`. Image direction and interface style hold one each; a second is refused with 409 unless you pass `replace: true`, which is also how you accept a drifted revision. | ||
| - `remove_brand_layer`: take one off. Names the record rather than the axis, so a stale view cannot clear a layer it never saw, and repeating it is a no-op. | ||
| - `remove_brand_layer`: take one off. Pass `confirm: true`; it names the record rather than the axis, so a stale view cannot clear a layer it never saw, and repeating it is a no-op. | ||
| - `export_brand` (`kits:read`): the brand as ONE document, ready to build from — the kit's `DESIGN.md` with every pinned layer written into it, under the precedence rule that decides which wins when they disagree (the kit owns identity, a layer owns application). Use it instead of merging the kit and each layer yourself. A layer the key cannot open is named with its page and an upgrade path rather than dropped; a brand with no chosen kit answers 409 instead of returning a placeholder nobody picked. | ||
@@ -240,3 +250,3 @@ | ||
| Everything an agent creates it can also revise. The write tools overwrite live, client-visible state and none of them ask first, so read the feedback before acting on it. | ||
| Everything an agent creates it can also revise. The write tools change live, client-visible state; destructive tools require `confirm: true`, so read the feedback before acting on it. | ||
@@ -287,2 +297,3 @@ New keys carry `kits:write` by default. A key minted before that scope existed will 403 until you re-run `identityforge login` or create a new scoped key. | ||
| identityforge themes remix <id|slug> --overrides o.json # copies; original untouched | ||
| identityforge themes delete <id|slug> --yes # permanent; refuses kits still in use | ||
| identityforge themes similar <id|slug> # nearby published kits | ||
@@ -310,3 +321,3 @@ identityforge themes match "#1d4ed8" "#f97316" # kits closest to colors you hold | ||
| identityforge brand add-layer --project <uuid> --axis imageDirection --record <id> | ||
| identityforge brand remove-layer --project <uuid> --axis imageDirection --record <id> | ||
| identityforge brand remove-layer --project <uuid> --axis imageDirection --record <id> --yes | ||
| # The kit's DESIGN.md with every pinned layer composed into it: the one document to build from | ||
@@ -320,2 +331,6 @@ identityforge brand export --project <uuid> > DESIGN.md | ||
| identityforge brand recommend --project <uuid> # candidates (3 units, needs a key) | ||
| # Queue mockups: one AI credit per variation and scene combination | ||
| identityforge brand mockups generate --project <uuid> --variation <uuid> --item tshirt:front | ||
| identityforge brand mockups list --project <uuid> | ||
| identityforge brand mockups get --project <uuid> --job <uuid> | ||
@@ -356,2 +371,3 @@ identityforge brand versions --project <uuid> # project history | ||
| identityforge naming search --file research-tasks.json | ||
| identityforge naming trademarks "Candidate name" --project <uuid> --candidate <uuid> --nice-classes 9,42 | ||
| identityforge naming move <candidate-uuid> --project <uuid> --status finalist --notes "Strong market fit" | ||
@@ -410,3 +426,3 @@ identityforge naming rank <candidate-uuid>=1 <candidate-uuid>=2 --project <uuid> | ||
| Free kits and naming-recipe discovery work without a key. Owned naming projects and domain research use `naming:read`, generation and board edits use `naming:write`, reading design systems uses `kits:read`, and creating or remixing kits plus building shareable brand projects uses `kits:write`. API calls count against the plan's API quota, while generation separately spends AI credits for successfully persisted unique candidates. Manage keys at <https://identityforge.io/account/api-keys>. | ||
| Free kits and naming-recipe discovery work without a key. Sign in to keep persistent projects and saved work under an authenticated quota. Owned naming projects and domain research use `naming:read`, generation and board edits use `naming:write`, reading design systems uses `kits:read`, and creating or remixing kits plus building shareable brand projects uses `kits:write`. API calls count against the plan's API quota, while generation separately spends AI credits for successfully persisted unique candidates. Manage keys at <https://identityforge.io/account/api-keys>. | ||
@@ -413,0 +429,0 @@ Existing design-only keys are not silently upgraded. If a key reports that it is missing `naming:read` or `naming:write`, create a scoped key or run browser login again. |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
334814
6.71%14
7.69%5713
8.55%440
3.77%9
12.5%