identityforge
Advanced tools
| import { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js"; | ||
| import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; | ||
| import { CLI_VERSION } from "./api.js"; | ||
| import { buildMcpServer } from "./mcp.js"; | ||
| const CAPABILITY_GROUPS = { | ||
| designSystems: ["list_themes", "get_design_md", "get_tokens", "apply_theme"], | ||
| brandDelivery: [ | ||
| "create_brand_project", | ||
| "export_brand", | ||
| "share_brand_project", | ||
| ], | ||
| naming: ["create_naming_project", "generate_names", "list_name_candidates"], | ||
| domains: ["check_domains", "assess_domain_acquisition"], | ||
| trademarks: ["search_trademarks"], | ||
| }; | ||
| /** Exercise the current package over a real MCP transport without calling the API. */ | ||
| export async function inspectCurrentMcp() { | ||
| const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); | ||
| const client = new McpClient({ | ||
| name: "identityforge-doctor", | ||
| version: CLI_VERSION, | ||
| }); | ||
| const server = buildMcpServer(); | ||
| await Promise.all([ | ||
| client.connect(clientTransport), | ||
| server.connect(serverTransport), | ||
| ]); | ||
| try { | ||
| const { tools } = await client.listTools(); | ||
| const names = new Set(tools.map((tool) => tool.name)); | ||
| const capabilities = Object.fromEntries(Object.entries(CAPABILITY_GROUPS).map(([group, required]) => [ | ||
| group, | ||
| required.every((name) => names.has(name)), | ||
| ])); | ||
| const missingRequiredTools = Object.values(CAPABILITY_GROUPS) | ||
| .flat() | ||
| .filter((name) => !names.has(name)); | ||
| return { | ||
| version: CLI_VERSION, | ||
| toolCount: tools.length, | ||
| capabilities, | ||
| missingRequiredTools, | ||
| }; | ||
| } | ||
| finally { | ||
| await client.close(); | ||
| await server.close(); | ||
| } | ||
| } |
+1
-1
| import { randomUUID } from "node:crypto"; | ||
| import { resolveApiKey, resolveApiUrl } from "./config.js"; | ||
| import { isVersionGreater } from "./updateCheck.js"; | ||
| export const CLI_VERSION = "0.4.3"; | ||
| export const CLI_VERSION = "0.4.4"; | ||
| let apiClient = "cli"; | ||
@@ -6,0 +6,0 @@ const clientProcessReference = randomUUID(); |
+85
-6
@@ -37,3 +37,3 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; | ||
| kind: "codex", | ||
| path: () => join(homedir(), ".codex", "config.toml"), | ||
| path: (_cwd, homeDir) => join(homeDir, ".codex", "config.toml"), | ||
| }, | ||
@@ -123,4 +123,8 @@ vscode: { | ||
| const existing = existsSync(file) ? readFileSync(file, "utf8") : ""; | ||
| if (existing.includes(header)) | ||
| return; | ||
| if (existing.includes(header)) { | ||
| const entry = codexServerEntry(existing); | ||
| if (entry && codexEntryIsCurrent(entry)) | ||
| return; | ||
| throw new Error(`${file} already contains ${header}, but it does not run npx -y ${CLI_PACKAGE_SPEC} mcp. Repair or remove that table, then retry; Identity Forge did not overwrite it.`); | ||
| } | ||
| const envLine = apiUrl | ||
@@ -133,5 +137,76 @@ ? `\nenv = { IDENTITYFORGE_API_URL = "${apiUrl}" }` | ||
| } | ||
| export function configPathFor(client, cwd = process.cwd()) { | ||
| return CLIENTS[client].path(cwd); | ||
| function codexServerEntry(source) { | ||
| const header = `[mcp_servers.${SERVER_KEY}]`; | ||
| const start = source.indexOf(header); | ||
| if (start < 0) | ||
| return undefined; | ||
| const rest = source.slice(start + header.length); | ||
| const nextTable = rest.search(/\n\s*\[/); | ||
| return nextTable < 0 ? rest : rest.slice(0, nextTable); | ||
| } | ||
| function codexEntryIsCurrent(entry) { | ||
| return (/^\s*command\s*=\s*["']npx["']\s*$/m.test(entry) && | ||
| /^\s*args\s*=\s*\[\s*["']-y["']\s*,\s*["']identityforge@latest["']\s*,\s*["']mcp["']\s*\]\s*$/m.test(entry)); | ||
| } | ||
| function jsonServerIsCurrent(kind, json) { | ||
| if (kind === "opencode") { | ||
| const entry = json.mcp?.[SERVER_KEY]; | ||
| return (entry?.type === "local" && | ||
| entry.enabled === true && | ||
| Array.isArray(entry.command) && | ||
| entry.command.join("\0") === ["npx", ...NPX_ARGS].join("\0")); | ||
| } | ||
| const parent = kind === "vscode" ? "servers" : "mcpServers"; | ||
| const entry = json[parent]?.[SERVER_KEY]; | ||
| return ((kind !== "vscode" || entry?.type === "stdio") && | ||
| entry?.command === "npx" && | ||
| Array.isArray(entry.args) && | ||
| entry.args.join("\0") === NPX_ARGS.join("\0")); | ||
| } | ||
| export function configPathFor(client, cwd = process.cwd(), homeDir = homedir()) { | ||
| return CLIENTS[client].path(cwd, homeDir); | ||
| } | ||
| /** Read-only check that the client points at the rolling public MCP package. */ | ||
| export function inspectClientConfig(client, opts = {}) { | ||
| const spec = CLIENTS[client]; | ||
| const file = spec.path(opts.cwd ?? process.cwd(), opts.homeDir ?? homedir()); | ||
| if (!existsSync(file)) { | ||
| return { | ||
| client, | ||
| file, | ||
| configured: false, | ||
| current: false, | ||
| issue: "Configuration file does not exist.", | ||
| }; | ||
| } | ||
| try { | ||
| const source = readFileSync(file, "utf8"); | ||
| const current = spec.kind === "codex" | ||
| ? (() => { | ||
| const entry = codexServerEntry(source); | ||
| return entry !== undefined && codexEntryIsCurrent(entry); | ||
| })() | ||
| : jsonServerIsCurrent(spec.kind, JSON.parse(source)); | ||
| return { | ||
| client, | ||
| file, | ||
| configured: true, | ||
| current, | ||
| ...(current | ||
| ? {} | ||
| : { | ||
| issue: `Identity Forge is not configured as npx -y ${CLI_PACKAGE_SPEC} mcp.`, | ||
| }), | ||
| }; | ||
| } | ||
| catch (error) { | ||
| return { | ||
| client, | ||
| file, | ||
| configured: true, | ||
| current: false, | ||
| issue: error instanceof Error ? error.message : String(error), | ||
| }; | ||
| } | ||
| } | ||
| /** Write the Identity Forge MCP server config for a coding agent. Returns the file written. */ | ||
@@ -143,3 +218,3 @@ export function installClient(client, opts = {}) { | ||
| } | ||
| const file = spec.path(opts.cwd ?? process.cwd()); | ||
| const file = spec.path(opts.cwd ?? process.cwd(), opts.homeDir ?? homedir()); | ||
| switch (spec.kind) { | ||
@@ -159,3 +234,7 @@ case "mcpServers": | ||
| } | ||
| const inspection = inspectClientConfig(client, opts); | ||
| if (!inspection.current) { | ||
| throw new Error(`Identity Forge wrote ${file}, but verification failed: ${inspection.issue ?? "unknown configuration error"}`); | ||
| } | ||
| return file; | ||
| } |
+44
-3
@@ -6,3 +6,5 @@ import { get } from "node:https"; | ||
| function parseVersion(value) { | ||
| const match = value.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/); | ||
| const match = value | ||
| .trim() | ||
| .match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/); | ||
| if (!match) | ||
@@ -55,3 +57,5 @@ return undefined; | ||
| const right = parseVersion(current); | ||
| return left !== undefined && right !== undefined && compareVersions(left, right) > 0; | ||
| return (left !== undefined && | ||
| right !== undefined && | ||
| compareVersions(left, right) > 0); | ||
| } | ||
@@ -107,3 +111,4 @@ let updateNoticePrinted = false; | ||
| const cached = config.updateCheck; | ||
| if (cached?.latestVersion && isVersionGreater(cached.latestVersion, currentVersion)) { | ||
| if (cached?.latestVersion && | ||
| isVersionGreater(cached.latestVersion, currentVersion)) { | ||
| printUpdateNotice(currentVersion, cached.latestVersion); | ||
@@ -140,2 +145,38 @@ } | ||
| } | ||
| /** Perform an explicit registry check for scripts and `identityforge update-check`. */ | ||
| export async function getUpdateStatus(currentVersion) { | ||
| const cached = readConfig().updateCheck; | ||
| let latestVersion; | ||
| try { | ||
| latestVersion = await fetchLatestVersion(); | ||
| } | ||
| catch { | ||
| latestVersion = undefined; | ||
| } | ||
| if (latestVersion) { | ||
| const checkedAt = new Date().toISOString(); | ||
| try { | ||
| updateConfig({ updateCheck: { checkedAt, latestVersion } }); | ||
| } | ||
| catch { | ||
| // A read-only home still gets the answer; it merely cannot cache it. | ||
| } | ||
| return { | ||
| currentVersion, | ||
| latestVersion, | ||
| updateAvailable: isVersionGreater(latestVersion, currentVersion), | ||
| checkedAt, | ||
| source: "registry", | ||
| }; | ||
| } | ||
| return { | ||
| currentVersion, | ||
| latestVersion: cached?.latestVersion, | ||
| updateAvailable: cached?.latestVersion | ||
| ? isVersionGreater(cached.latestVersion, currentVersion) | ||
| : false, | ||
| checkedAt: cached?.checkedAt, | ||
| source: cached?.latestVersion ? "cache" : "unavailable", | ||
| }; | ||
| } | ||
| /** Start the check without giving the network request a chance to hold up a command. */ | ||
@@ -142,0 +183,0 @@ export function startUpdateCheck(currentVersion) { |
+3
-2
| { | ||
| "name": "identityforge", | ||
| "version": "0.4.3", | ||
| "version": "0.4.4", | ||
| "mcpName": "io.identityforge/mcp", | ||
@@ -32,3 +32,4 @@ "description": "Agent-native brand naming, domain research, and design systems through one CLI and MCP server.", | ||
| "typecheck": "tsc --noEmit -p tsconfig.json", | ||
| "prepublishOnly": "npm run build" | ||
| "smoke:mcp": "node scripts/smoke-mcp.mjs", | ||
| "prepublishOnly": "npm test && npm run typecheck && npm run build && npm run smoke:mcp" | ||
| }, | ||
@@ -35,0 +36,0 @@ "keywords": [ |
+18
-7
@@ -15,3 +15,4 @@ <p align="center"> | ||
| <a href="#mcp-tools">MCP tools</a> · | ||
| <a href="#cli-commands">CLI reference</a> | ||
| <a href="#cli-commands">CLI reference</a> · | ||
| <a href="https://identityforge.io/docs">Docs</a> | ||
| </p> | ||
@@ -25,2 +26,4 @@ | ||
| **Free to use.** 69 of 79 design kits are free, with all 6 export formats, the CLI, the MCP server, and the API. No account required. Pro unlocks 10 additional kits, 1,000 AI generations/month, and advanced features. | ||
| ## For people | ||
@@ -116,2 +119,5 @@ | ||
| All of them run the same local stdio server via `npx -y identityforge@latest mcp`. | ||
| After installation, `identityforge doctor --client <name>` checks that the client | ||
| points at the rolling package and initializes the package's MCP server without | ||
| calling the Identity Forge API or spending quota. | ||
@@ -149,3 +155,3 @@ ### Install the agent plugin and skill | ||
| Once connected, your agent gets 63 tools. Browsing free kits needs no key; scopes are noted where they apply. | ||
| Once connected, your agent gets 64 tools. Browsing free kits needs no key; scopes are noted where they apply. | ||
@@ -178,5 +184,5 @@ ### Find a design kit | ||
| current CLI or MCP process. A local apply reports one bounded result: files | ||
| written, artifacts already current, a safe conflict refusal, or the stage where | ||
| it failed. The report contains counts and classifications, never local paths or | ||
| error prose. Running as an MCP server, requests also carry the client name | ||
| written, artifacts already current, a safe conflict refusal, or the stage where it failed. The report | ||
| contains counts and classifications, never local paths or error prose. Running | ||
| as an MCP server, requests also carry the client name | ||
| your editor or agent already sends in the MCP handshake (`claude-code`, | ||
@@ -342,3 +348,4 @@ `cursor-vscode`, `codex`), so usage can be attributed to a product rather than to | ||
| - `check_domains`: DNS plus distinct RDAP, registrar, and optional self-hosted SERP evidence. Basic research costs one unit per unique domain; SERP adds one. Absent DNS records only mean a domain might be available. | ||
| - `search_trademarks`: EUIPO automation is coming soon and returns 503 without a provider call until production access is enabled. | ||
| - `assess_domain_acquisition`: state whether the goal is a new registration, an aftermarket purchase, or either. It reports registrar registration evidence and bounded public landing-page evidence separately, including literal sale, marketplace, reserved-page, and visible-price signals. It never validates the seller or guarantees purchase. Aftermarket evidence adds one unit per unique domain; an exact successful repeat within ten minutes costs zero. | ||
| - `search_trademarks`: the EUIPO official-API adapter runs only when the deployment reports live provider access. Otherwise it returns structured upstream/implementation/runtime status plus an official manual handoff and makes no provider request. The research context lists EUIPO, DPMA, WIPO, and USPTO coverage; screening is never legal clearance. | ||
@@ -390,2 +397,5 @@ ### Build a brand and share it with a client | ||
| identityforge whoami # plan, scopes, quota, credits, saved-kit slots (free) | ||
| identityforge usage # alias for whoami | ||
| identityforge update-check # current/latest package version as JSON | ||
| identityforge doctor --client codex # verify config and MCP capabilities without API usage | ||
| identityforge logout # remove stored credentials | ||
@@ -478,3 +488,4 @@ identityforge themes # list kits | ||
| identityforge naming search --file research-tasks.json | ||
| # Coming soon: returns 503 without a provider call until EUIPO production access is enabled | ||
| identityforge naming acquisition candidate.com --intent either | ||
| # Returns live EUIPO evidence when enabled, otherwise a structured official manual handoff | ||
| identityforge naming trademarks "Candidate name" --project <uuid> --candidate <uuid> --nice-classes 9,42 | ||
@@ -481,0 +492,0 @@ identityforge naming move <candidate-uuid> --project <uuid> --status finalist --notes "Strong market fit" |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
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.
370805
3.88%15
7.14%6278
4.62%568
1.97%