From c9377b158f1baf514644ac5f63611c5be1b0f388 Mon Sep 17 00:00:00 2001 From: mega123-art Date: Fri, 19 Jun 2026 20:42:10 +0530 Subject: [PATCH 1/7] Add skill management features: implement skill disposal and re-equipping, enhance skill detail view with required skills, and update state management for new actions. --- surfaces/localhost/src/index.ts | 176 ++++++------------ surfaces/webview/src/market/MarketScreen.tsx | 1 + .../webview/src/market/SkillDetailView.tsx | 24 ++- surfaces/webview/src/state/store.tsx | 18 ++ surfaces/webview/src/transport/protocol.ts | 7 +- 5 files changed, 109 insertions(+), 117 deletions(-) diff --git a/surfaces/localhost/src/index.ts b/surfaces/localhost/src/index.ts index c30f9bc..c012b64 100644 --- a/surfaces/localhost/src/index.ts +++ b/surfaces/localhost/src/index.ts @@ -58,6 +58,8 @@ import { maskedHeliusKey, hasDasRpc, getNetwork, + getSkillShopping, + setSkillShopping, saveGithubToken, loadGithubToken, maskedGithubToken, @@ -389,9 +391,67 @@ function attachChat(id: string, c: Client, rt: AgentRuntime) { onRecv: (cb: (m: any) => void) => { c.recvs.push(cb); }, }; const approval = new TransportApprovalChannel(transport); + const marketPromise = wallet ? marketplaceEnv(wallet) : null; const chat = createChatSession(rt, transport, { cwd: () => process.cwd(), approval, + searchSkills: async (query, kind) => (await marketPromise)?.searchSkills(query, kind) ?? [], + getSkillDetail: async (mint) => { + const mkt = await marketPromise; + if (!mkt) throw new Error("Wallet not connected."); + return mkt.getSkillDetail(mint); + }, + getSkillDoc: async (name) => (await marketPromise)?.getSkillDoc(name) ?? null, + buySkill: async (skillId, creatorWallet) => { + const mkt = await marketPromise; + return mkt ? mkt.buySkill(skillId, creatorWallet) : { ok: false, error: "Wallet not connected." }; + }, + disposeSkill: async (skillId) => { + const mkt = await marketPromise; + return mkt ? mkt.disposeSkill(skillId) : { ok: false, error: "Wallet not connected." }; + }, + reEquipSkill: async (skillId) => { + const mkt = await marketPromise; + return mkt ? mkt.reEquipSkill(skillId) : { ok: false, error: "Wallet not connected." }; + }, + disposedSkillMints: async () => (await marketPromise)?.disposedSkillMints() ?? {}, + postNote: async (skillId, skillType, text, gitLink) => { + const mkt = await marketPromise; + return mkt ? mkt.postNote(skillId, skillType, text, gitLink) : { ok: false, error: "Wallet not connected." }; + }, + ownedSkills: async () => (await marketPromise)?.ownedSkills() ?? [], + ownedNftSkills: async () => (await marketPromise)?.ownedNftSkills() ?? [], + ownedSkillMints: async () => (await marketPromise)?.ownedSkillMints() ?? {}, + listAgents: async () => (await marketPromise)?.listAgents() ?? [], + getAgentProfile: async (w) => { + const mkt = await marketPromise; + if (!mkt) throw new Error("Wallet not connected."); + return mkt.getAgentProfile(w); + }, + buyAllSkills: async (w) => { + const mkt = await marketPromise; + return mkt ? mkt.buyAllSkills(w) : { ok: false, bought: 0, failed: 0, error: "Wallet not connected." }; + }, + postAgentNote: async (w, t, l) => { + const mkt = await marketPromise; + return mkt ? mkt.postAgentNote(w, t, l) : { ok: false, error: "Wallet not connected." }; + }, + solBalance: async () => (await marketPromise)?.solBalance() ?? null, + publishSkill: async (input) => { + const mkt = await marketPromise; + return mkt ? mkt.publishSkill(input) : { ok: false, error: "Wallet not connected." }; + }, + loadOwnedSkills: async () => (await marketPromise)?.loadOwnedSkills() ?? [], + setHeliusKey: async () => { + c.send({ type: "toast", text: "Open Markets and choose Add Helius key to enter it on this device." }); + }, + useDefaultRpc: async () => { await saveHeliusKey(""); }, + rpcStatus: async () => { + const masked = await maskedHeliusKey(); + return { dasReady: await hasDasRpc(), hasKey: !!masked, masked, network: getNetwork() }; + }, + getSkillShopping: () => getSkillShopping(), + setSkillShopping: (on) => setSkillShopping(on), walletAddress: () => walletAddress, storageInfo: async () => ({ info: await getStorageInfo(), options: STORAGE_OPTIONS, googleCredsConfigured: await hasGoogleCreds() }), connectCloud: async (cfg) => { @@ -428,64 +488,11 @@ function attachChat(id: string, c: Client, rt: AgentRuntime) { if (m?.type === "ready") await pushCliStatus(c); }); - // ── market handlers ── - // Mirror exactly what VSCode's extension.ts passes to createChatSession, but as a - // standalone recv subscriber rather than chatSession options — localhost wires market - // messages here (HTTP surface can't use native dialogs, so setHeliusKey = submitHeliusKey). - const mktPromise = wallet ? marketplaceEnv(wallet) : null; + // Mobile/web text entry for the Helius key. All market/search/buy/comment/publish + // messages are handled by the shared chat dispatcher above, matching VS Code. c.recvs.push(async (m: any) => { if (!m?.type) return; - const mkt = await mktPromise; - if (!mkt) { c.send({ type: "toast", text: "Wallet not connected." }); return; } switch (m.type) { - case "searchSkills": { - try { - const results = await mkt.searchSkills(m.query ?? "", m.kind); - c.send({ type: "searchResults", results }); - } catch (e) { - c.send({ type: "searchError", message: (e as Error).message }); - } - return; - } - case "getSkillDetail": { - try { - const detail = await mkt.getSkillDetail(m.mint); - c.send({ type: "skillDetail", detail }); - } catch (e) { - c.send({ type: "toast", text: "Failed to load skill: " + (e as Error).message }); - } - return; - } - case "buySkill": { - try { - const r = await mkt.buySkill(m.skillId, m.creatorWallet); - c.send({ type: "buyResult", skillId: m.skillId, ...r }); - } catch (e) { - c.send({ type: "buyResult", skillId: m.skillId, ok: false, error: (e as Error).message }); - } - return; - } - case "ownedSkills": { - try { - const r = await mkt.ownedSkills(); - c.send({ type: "ownedSkills", ...r }); - } catch (e) { - c.send({ type: "toast", text: "Failed to load owned skills: " + (e as Error).message }); - } - return; - } - case "getBalance": { - try { - const lamports = await mkt.solBalance(); - c.send({ type: "balance", lamports }); - } catch { c.send({ type: "balance", lamports: null }); } - return; - } - case "getRpcStatus": { - const masked = await maskedHeliusKey(); - c.send({ type: "rpcStatus", status: { dasReady: await hasDasRpc(), hasKey: !!masked, masked, network: getNetwork() } }); - return; - } case "submitHeliusKey": { if (typeof m.key === "string" && m.key.trim()) { await saveHeliusKey(m.key.trim()); @@ -495,65 +502,6 @@ function attachChat(id: string, c: Client, rt: AgentRuntime) { } return; } - case "useDefaultRpc": { - await saveHeliusKey(""); - c.send({ type: "rpcStatus", status: { dasReady: false, hasKey: false, masked: null, network: getNetwork() } }); - return; - } - case "publishSkill": { - try { - const r = await mkt.publishSkill({ name: m.name, description: m.description, text: m.text, category: m.category, hashtags: m.hashtags, priceSol: m.priceSol, image: m.image }); - c.send({ type: "publishResult", ...r }); - } catch (e) { - c.send({ type: "publishResult", ok: false, error: (e as Error).message }); - } - return; - } - case "postNote": { - try { - const r = await mkt.postNote(m.skillId, m.skillType, m.text, m.gitLink); - c.send({ type: "postNoteResult", skillId: m.skillId, ...r }); - } catch (e) { - c.send({ type: "postNoteResult", skillId: m.skillId, ok: false, error: (e as Error).message }); - } - return; - } - case "listAgents": { - try { - const r = await mkt.listAgents(); - c.send({ type: "agents", agents: r }); - } catch (e) { - c.send({ type: "toast", text: "Failed to list agents: " + (e as Error).message }); - } - return; - } - case "getAgentProfile": { - try { - const profile = await mkt.getAgentProfile(m.wallet); - c.send({ type: "agentProfile", profile }); - } catch (e) { - c.send({ type: "toast", text: "Failed to load agent: " + (e as Error).message }); - } - return; - } - case "buyAllSkills": { - try { - const r = await mkt.buyAllSkills(m.wallet); - c.send({ type: "buyAllResult", wallet: m.wallet, ...r }); - } catch (e) { - c.send({ type: "buyAllResult", wallet: m.wallet, ok: false, bought: 0, failed: 0, error: (e as Error).message }); - } - return; - } - case "postAgentNote": { - try { - const r = await mkt.postAgentNote(m.agentWallet, m.text, m.gitLink); - c.send({ type: "agentNoteResult", agentWallet: m.agentWallet, ...r }); - } catch (e) { - c.send({ type: "agentNoteResult", agentWallet: m.agentWallet, ok: false, error: (e as Error).message }); - } - return; - } } }); diff --git a/surfaces/webview/src/market/MarketScreen.tsx b/surfaces/webview/src/market/MarketScreen.tsx index 8a31e9a..20adc62 100644 --- a/surfaces/webview/src/market/MarketScreen.tsx +++ b/surfaces/webview/src/market/MarketScreen.tsx @@ -49,6 +49,7 @@ export function MarketScreen() { detail={state.marketDetail} owned={state.marketOwned.includes(state.marketDetail.card.name)} onBack={() => { send({ type: "ownedSkills" }); clearMarketDetail(); }} + onOpenSkill={(card) => send({ type: "getSkillDetail", mint: card.id })} /> {state.buyCelebrate && } diff --git a/surfaces/webview/src/market/SkillDetailView.tsx b/surfaces/webview/src/market/SkillDetailView.tsx index de325e9..13574cc 100644 --- a/surfaces/webview/src/market/SkillDetailView.tsx +++ b/surfaces/webview/src/market/SkillDetailView.tsx @@ -1,15 +1,16 @@ import { useState } from "react"; import { useStore } from "../state/store"; -import type { SkillDetail } from "../transport/protocol"; +import type { SkillCard, SkillDetail } from "../transport/protocol"; import { SkillIcon } from "../icons"; interface Props { detail: SkillDetail; owned: boolean; onBack: () => void; + onOpenSkill?: (card: SkillCard) => void; } -export function SkillDetailView({ detail, owned, onBack }: Props) { +export function SkillDetailView({ detail, owned, onBack, onOpenSkill }: Props) { const { send } = useStore(); const [buying, setBuying] = useState(false); const [noteText, setNoteText] = useState(""); @@ -68,6 +69,25 @@ export function SkillDetailView({ detail, owned, onBack }: Props) { )} + {Array.isArray(detail.requiredCards) && detail.requiredCards.length > 0 && ( +
+

Required skills

+
+ {detail.requiredCards.map((req) => ( + + ))} +
+
+ )} + {Array.isArray(notes) && notes.length > 0 && (

Comments

diff --git a/surfaces/webview/src/state/store.tsx b/surfaces/webview/src/state/store.tsx index 53b20f3..652f5d2 100644 --- a/surfaces/webview/src/state/store.tsx +++ b/surfaces/webview/src/state/store.tsx @@ -352,6 +352,18 @@ function reducer(state: State, ev: Action): State { marketOwned: ev.ok ? [...state.marketOwned, ev.slug ?? ev.skillId] : state.marketOwned, buyCelebrate: ev.ok ? true : state.buyCelebrate, }; + case "disposeResult": + return { + ...state, + toast: ev.ok ? "Skill removed." : `Remove failed: ${ev.error ?? "unknown"}`, + marketOwned: ev.ok ? state.marketOwned.filter((name) => name !== (ev.slug ?? ev.skillId)) : state.marketOwned, + }; + case "reEquipResult": + return { + ...state, + toast: ev.ok ? "Skill re-equipped." : `Re-equip failed: ${ev.error ?? "unknown"}`, + marketOwned: ev.ok ? [...state.marketOwned, ev.slug ?? ev.skillId] : state.marketOwned, + }; case "ownedSkills": return { ...state, marketOwned: ev.names }; case "balance": @@ -373,6 +385,12 @@ function reducer(state: State, ev: Action): State { case "agentNoteResult": return { ...state, toast: ev.ok ? "Note posted." : `Note failed: ${ev.error ?? "unknown"}` }; case "notes": + if (!state.marketDetail || state.marketDetail.card.id !== ev.skillId) return state; + return { + ...state, + marketDetail: { ...state.marketDetail, notes: ev.notes as SkillDetail["notes"] }, + }; + case "skillDoc": return state; case "githubStatus": return { ...state, githubStatus: { hasToken: ev.hasToken, masked: ev.masked } }; diff --git a/surfaces/webview/src/transport/protocol.ts b/surfaces/webview/src/transport/protocol.ts index 852c404..b9e1022 100644 --- a/surfaces/webview/src/transport/protocol.ts +++ b/surfaces/webview/src/transport/protocol.ts @@ -123,6 +123,8 @@ export type ClientMessage = | { type: "searchSkills"; query: string; kind?: "skill" | "workflow" } | { type: "getSkillDetail"; mint: string } | { type: "buySkill"; skillId: string; creatorWallet?: string } + | { type: "disposeSkill"; skillId: string } + | { type: "reEquipSkill"; skillId: string } | { type: "ownedSkills" } | { type: "getBalance" } | { type: "getRpcStatus" } @@ -185,8 +187,11 @@ export type ServerMessage = | { type: "searchResults"; results: import("@iqlabs-official/agent-sdk").SkillCard[] } | { type: "searchError"; message: string } | { type: "skillDetail"; detail: import("@iqlabs-official/agent-sdk").SkillDetail } + | { type: "skillDoc"; name: string; text: string | null } | { type: "buyResult"; skillId: string; ok: boolean; slug?: string; error?: string } - | { type: "ownedSkills"; names: string[]; mints?: Record } + | { type: "disposeResult"; skillId: string; ok: boolean; slug?: string; error?: string } + | { type: "reEquipResult"; skillId: string; ok: boolean; slug?: string; error?: string } + | { type: "ownedSkills"; names: string[]; mints?: Record; disposedMints?: Record } | { type: "balance"; lamports: number | null } | { type: "skillActive"; name: string } | { type: "rpcStatus"; status: import("@iqlabs-official/agent-sdk").RpcStatus } From 2eb7ea34fd6cfb2ad41cb410e444f4688a317f49 Mon Sep 17 00:00:00 2001 From: mega123-art Date: Sat, 20 Jun 2026 02:32:47 +0530 Subject: [PATCH 2/7] Add plugin NFT collection plumbing --- packages/core/src/chat/marketMessages.ts | 19 ++-- packages/core/src/chat/session.ts | 5 +- packages/core/src/core/dasSource.spec.ts | 3 +- packages/core/src/core/seed.spec.ts | 11 ++ packages/core/src/core/seed.ts | 26 +++-- packages/core/src/core/skillSource.spec.ts | 71 ++++++++++++- packages/core/src/core/skillSource.ts | 61 ++++++++--- packages/core/src/core/types.ts | 16 ++- packages/core/src/index.ts | 4 +- packages/core/src/search/search.ts | 4 +- packages/core/src/skill-market/index.ts | 18 ++-- packages/core/src/skill-market/ingest/env.ts | 21 ++-- plans/onchain-format/skill-nft-json.md | 46 +++++++++ plans/onchain-format/tables.md | 20 ++-- plans/plugin-nft.md | 101 +++++++++++++++++++ surfaces/cli/src/views/SkillMarket.tsx | 6 +- surfaces/webview/src/transport/protocol.ts | 5 +- 17 files changed, 370 insertions(+), 67 deletions(-) create mode 100644 plans/plugin-nft.md diff --git a/packages/core/src/chat/marketMessages.ts b/packages/core/src/chat/marketMessages.ts index ff08ac7..102da50 100644 --- a/packages/core/src/chat/marketMessages.ts +++ b/packages/core/src/chat/marketMessages.ts @@ -10,15 +10,15 @@ // // Direction is in the name: *Request = UI -> host; *Event = host -> UI. -import type { Note, Reputation } from "../core/types.js"; +import type { MarketItemType, Note, Reputation } from "../core/types.js"; /** One item row as the UI renders it (cards + detail). Mirrors the subset of `Skill` - * the UI needs — kept here so host (env callbacks) and UI agree. Covers both kinds: - * `type` splits the Skills / Workflows tabs; `requiredSkills` (workflows only) are - * the prerequisite skill mint ids the detail view renders as clickable links. */ + * the UI needs — kept here so host (env callbacks) and UI agree. Covers all marketplace + * kinds: `type` identifies skills, workflows, and future plugin cards. `requiredSkills` + * is workflow-only; plugin cards use engine/provenance fields for later badges/details. */ export interface SkillCard { id: string; // mint address - type?: "skill" | "workflow"; + type?: MarketItemType; name: string; description?: string; category?: string; @@ -28,6 +28,11 @@ export interface SkillCard { price?: string; // lamports as decimal string (for buy-all cost summing) creator?: string; // wallet (paid on a priced buy) requiredSkills?: string[]; // workflows only: prerequisite skill mint ids + engines?: string[]; // plugins only: engine badges such as "claude", "codex", "mcp" + iqGitPda?: string; // plugins only: canonical IQ Git provenance anchor + version?: string; + capabilities?: string[]; + permissions?: string[]; } /** Full agent profile payload sent to the UI on getAgentProfile. */ @@ -71,7 +76,7 @@ export interface RpcStatus { // ── UI -> host (requests) ─────────────────────────────────────────────────── export type MarketRequest = - | { type: "searchSkills"; query: string; kind?: "skill" | "workflow" } // kind = the active tab + | { type: "searchSkills"; query: string; kind?: MarketItemType } // kind = the active tab / future plugin tab | { type: "getSkillDetail"; mint: string } // open the detail view for one item | { type: "getSkillDoc"; name: string } // read an installed skill's local SKILL.md by name | { type: "buySkill"; skillId: string; creatorWallet?: string } @@ -83,7 +88,7 @@ export type MarketRequest = | { type: "useDefaultRpc" } // clear any key, fall back to the default | { type: "getRpcStatus" } // ask the host to (re)send rpcStatus // issue #34: post a comment on a skill (holder-gated client-side) - | { type: "postNote"; skillId: string; skillType?: "skill" | "workflow"; text: string; gitLink?: string } + | { type: "postNote"; skillId: string; skillType?: MarketItemType; text: string; gitLink?: string } // issue #35: agent directory + profile | { type: "listAgents" } | { type: "getAgentProfile"; wallet: string } diff --git a/packages/core/src/chat/session.ts b/packages/core/src/chat/session.ts index 7e16d32..336b0e1 100644 --- a/packages/core/src/chat/session.ts +++ b/packages/core/src/chat/session.ts @@ -13,6 +13,7 @@ import type { AgentRuntime, SessionHandle } from "../runtime/contract.js"; import type { ApprovalChannel } from "../runtime/approval/channel.js"; +import type { MarketItemType } from "../core/types.js"; import type { SkillCard, MarketRequest } from "./marketMessages.js"; // The two-way pipe to ONE chat UI (one panel / one socket). Messages both ways are @@ -45,7 +46,7 @@ export interface ChatEnv { // are host-held (the extension owns them), so they're delegated like wallet/cloud. // buySkill installs the bought skill's SKILL.md into the runtime skills dir as part // of the buy (the host calls SkillSync.installBought), returning the installed slug. - searchSkills?(query: string, kind?: "skill" | "workflow"): Promise; + searchSkills?(query: string, kind?: MarketItemType): Promise; getSkillDetail?(mint: string): Promise; getSkillDoc?(name: string): Promise; buySkill?(skillId: string, creatorWallet?: string): Promise<{ ok: boolean; slug?: string; error?: string }>; @@ -56,7 +57,7 @@ export interface ChatEnv { // slug -> mint for disposed (un-pinned) skills — the UI greys these + offers Re-equip. disposedSkillMints?(): Promise>; // issue #34: post a comment on a skill (holder-gated), returns refreshed notes on success - postNote?(skillId: string, skillType: "skill" | "workflow" | undefined, text: string, gitLink?: string): Promise<{ ok: boolean; notes?: import("./marketMessages.js").Note[]; error?: string }>; + postNote?(skillId: string, skillType: MarketItemType | undefined, text: string, gitLink?: string): Promise<{ ok: boolean; notes?: import("./marketMessages.js").Note[]; error?: string }>; ownedSkills?(): Promise; // skill names already installed (panel fill) // The wallet's soulbound NFT skills, by display name (catalog ∩ holdings). This is // what the "Equipped skills" panel shows: skills you OWN on-chain, not local dirs. diff --git a/packages/core/src/core/dasSource.spec.ts b/packages/core/src/core/dasSource.spec.ts index 3e12802..b974419 100644 --- a/packages/core/src/core/dasSource.spec.ts +++ b/packages/core/src/core/dasSource.spec.ts @@ -5,9 +5,10 @@ const RPC = "DAS_RPC_URL"; const SOL = "SOLANA_RPC_URL"; const SKILLS = "AGENTNET_SKILLS_COLLECTION_PUBKEY"; const WORKFLOWS = "AGENTNET_WORKFLOWS_COLLECTION_PUBKEY"; +const PLUGINS = "AGENTNET_PLUGINS_COLLECTION_PUBKEY"; function clearEnv() { - for (const k of [RPC, SOL, SKILLS, WORKFLOWS]) delete process.env[k]; + for (const k of [RPC, SOL, SKILLS, WORKFLOWS, PLUGINS]) delete process.env[k]; // isolate AGENTNET_HOME so resolveRpcUrl() can't pick up a real ~/.agentnet Helius key process.env.AGENTNET_HOME = "/tmp/agentnet-dassource-test"; } diff --git a/packages/core/src/core/seed.spec.ts b/packages/core/src/core/seed.spec.ts index f259f37..ccb34c0 100644 --- a/packages/core/src/core/seed.spec.ts +++ b/packages/core/src/core/seed.spec.ts @@ -4,6 +4,8 @@ import { mysessionsHint, reviewsHint, reviewsAgentHint, + collectionFor, + getPluginsCollectionMint, networkFromRpcUrl, getGatewayUrl, ENDPOINTS, @@ -30,6 +32,15 @@ describe("core/seed", () => { expect(reviewsAgentHint("AgentWallet123")).toBe("reviews:agent:AgentWallet123"); }); + it("maps plugin items to the plugin collection when configured", () => { + const prev = process.env.AGENTNET_PLUGINS_COLLECTION_PUBKEY; + process.env.AGENTNET_PLUGINS_COLLECTION_PUBKEY = "PluginsCollection"; + expect(getPluginsCollectionMint()).toBe("PluginsCollection"); + expect(collectionFor("plugin")).toBe("PluginsCollection"); + if (prev === undefined) delete process.env.AGENTNET_PLUGINS_COLLECTION_PUBKEY; + else process.env.AGENTNET_PLUGINS_COLLECTION_PUBKEY = prev; + }); + describe("networkFromRpcUrl — gateway follows the live RPC, not the static switch", () => { it("reads devnet from common devnet RPC hosts", () => { expect(networkFromRpcUrl("https://api.devnet.solana.com")).toBe("devnet"); diff --git a/packages/core/src/core/seed.ts b/packages/core/src/core/seed.ts index 55d1d6f..c1db448 100644 --- a/packages/core/src/core/seed.ts +++ b/packages/core/src/core/seed.ts @@ -5,6 +5,8 @@ // `iqlabs.writer.createTable`. Keeping the naming convention in one place // prevents silent drift between writer and reader. +import type { MarketItemType } from "./types.js"; + /** DbRoot id for every agentnet table. Bootstrap and every caller share this. */ export const AGENTNET_ROOT_ID = "agentnet-root"; @@ -20,7 +22,7 @@ export function mysessionsHint(wallet: string): string { /** * Hint for reviews on an item NFT inside a collection. * - * The collection is the umbrella (skills / workflows / future kinds); the NFT + * The collection is the umbrella (skills / workflows / plugins / future kinds); the NFT * mint is the individual item under it. Keying by collection THEN item keeps * reviews partitioned per umbrella, so a new collection kind extends the same * structure without a new table shape. @@ -134,6 +136,9 @@ export function getPublicRpcUrl(): string { /** Devnet test ids — the single source. Swap here (or via env) to retarget. */ export const SKILLS_COLLECTION_MINT = "5TPKvxXTpPVFrj9MUnFUr6XiGFEdtetsTvwRh6bKQ9Qg"; export const WORKFLOWS_COLLECTION_MINT = "F474VEn2uevpCotRqrPEbZ4XvWyqrqL4iGmNnmp9zvNe"; +// Placeholder until the plugin umbrella collection is minted. Override with +// AGENTNET_PLUGINS_COLLECTION_PUBKEY when testing a real plugin collection. +export const PLUGINS_COLLECTION_MINT = ""; /** agent-workflow-nft gate program — publish_workflow / buy_workflow. */ export const WORKFLOW_GATE_PROGRAM_ID = "3ptXj4yuaQG51WTA3SZZ37jGvYFgMhgXnSKWJLASJNkt"; /** @@ -160,6 +165,14 @@ export function getWorkflowsCollectionMint(): string | null { return process.env.AGENTNET_WORKFLOWS_COLLECTION_PUBKEY || WORKFLOWS_COLLECTION_MINT; } +/** + * The TokenGroup mint plugins are enrolled into. Env override wins; otherwise + * null until the plugin collection is minted for the target network. + */ +export function getPluginsCollectionMint(): string | null { + return process.env.AGENTNET_PLUGINS_COLLECTION_PUBKEY || PLUGINS_COLLECTION_MINT || null; +} + /** The workflow gate program id (env override wins). */ export function getWorkflowGateProgramId(): string { return process.env.AGENTNET_WORKFLOW_GATE_PROGRAM_ID || WORKFLOW_GATE_PROGRAM_ID; @@ -172,15 +185,16 @@ export function getFeeTreasury(): string { /** * The umbrella collection mint for an item type — the ONE place that maps - * "skill" / "workflow" → its collection. There are only these two collections; - * every item of a given type shares the same collection (a skill is one big - * collection, a workflow another). New kinds get a branch here, nowhere else. + * "skill" / "workflow" / "plugin" → its collection. Every item of a given type + * shares the same collection. New kinds get a branch here, nowhere else. * * Returns "" when the collection isn't configured yet — reviewsHint still * produces a stable key, so reads/writes work before bootstrap. */ -export function collectionFor(type: "skill" | "workflow" | undefined): string { - return (type === "workflow" ? getWorkflowsCollectionMint() : getSkillsCollectionMint()) ?? ""; +export function collectionFor(type: MarketItemType | undefined): string { + if (type === "workflow") return getWorkflowsCollectionMint() ?? ""; + if (type === "plugin") return getPluginsCollectionMint() ?? ""; + return getSkillsCollectionMint() ?? ""; } // ===== Table column declarations ===== diff --git a/packages/core/src/core/skillSource.spec.ts b/packages/core/src/core/skillSource.spec.ts index 968ed73..2c3d180 100644 --- a/packages/core/src/core/skillSource.spec.ts +++ b/packages/core/src/core/skillSource.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { dasSource, indexerSource, ownedAssetIds, ownedSkillMints } from "./skillSource.js"; +import { dasSource, indexerSource, ownedAssetIds, ownedSkillMints, traitsFromAttributes } from "./skillSource.js"; import type { Skill } from "./types.js"; // dasSource enumerates the TokenGroup collections via a DAS RPC. We mock fetch @@ -12,6 +12,7 @@ describe("core/skillSource — dasSource", () => { process.env.DAS_RPC_URL = "https://das.example/rpc"; process.env.AGENTNET_SKILLS_COLLECTION_PUBKEY = "SkillsCollection"; delete process.env.AGENTNET_WORKFLOWS_COLLECTION_PUBKEY; + delete process.env.AGENTNET_PLUGINS_COLLECTION_PUBKEY; // isolate AGENTNET_HOME so resolveRpcUrl() can't read a real ~/.agentnet Helius key process.env.AGENTNET_HOME = "/tmp/agentnet-skillsource-test"; }); @@ -91,6 +92,27 @@ describe("core/skillSource — dasSource", () => { // ids, so a collection is always configured unless explicitly overridden.) }); +describe("core/skillSource — marketplace traits", () => { + it("parses plugin tags, repeated engine badges, and IQ Git PDA", () => { + const traits = traitsFromAttributes([ + { trait_type: "category", value: "developer-tools" }, + { trait_type: "plugin", value: "git" }, + { trait_type: "plugin", value: "review" }, + { trait_type: "engine", value: "claude" }, + { trait_type: "engine", value: "codex" }, + { trait_type: "iqGitPda", value: "IqGitPda111" }, + ]); + + expect(traits).toEqual({ + category: "developer-tools", + hashtags: ["git", "review"], + requiredSkills: [], + engines: ["claude", "codex"], + iqGitPda: "IqGitPda111", + }); + }); +}); + describe("core/skillSource — indexerSource", () => { afterEach(() => vi.restoreAllMocks()); @@ -136,6 +158,52 @@ describe("core/skillSource — indexerSource", () => { }); }); + it("maps plugin /items with engine badges and provenance fields", async () => { + const src = indexerSource("https://nft-index.iqlabs.dev/"); + + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + items: [ + { + mint: "pluginMint", + type: "plugin", + name: "iq-git-reviewer", + description: "Review with IQ Git context.", + creator: "alice", + supply: 7, + price: "0", + version: "1.2.3", + capabilities: ["git.read", "review.write"], + permissions: ["fs.read"], + attributes: [ + { trait_type: "category", value: "developer-tools" }, + { trait_type: "plugin", value: "git" }, + { trait_type: "engine", value: "claude" }, + { trait_type: "engine", value: "codex" }, + { trait_type: "iqGitPda", value: "IqGitPda111" }, + ], + }, + ], + }), + }), + ); + + const items = await src.listSkills(); + expect(items[0]).toMatchObject({ + id: "pluginMint", + type: "plugin", + engines: ["claude", "codex"], + iqGitPda: "IqGitPda111", + version: "1.2.3", + capabilities: ["git.read", "review.write"], + permissions: ["fs.read"], + hashtags: ["git"], + }); + }); + it("throws on a non-ok indexer response (caller falls back to dasSource)", async () => { const src = indexerSource("https://nft-index.iqlabs.dev"); vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 503 })); @@ -156,6 +224,7 @@ describe("core/skillSource — ownedAssetIds / ownedSkillMints", () => { process.env.DAS_RPC_URL = "https://das.example/rpc"; process.env.AGENTNET_SKILLS_COLLECTION_PUBKEY = "SkillsCollection"; delete process.env.AGENTNET_WORKFLOWS_COLLECTION_PUBKEY; + delete process.env.AGENTNET_PLUGINS_COLLECTION_PUBKEY; process.env.AGENTNET_HOME = "/tmp/agentnet-owned-test"; }); afterEach(() => { process.env = { ...origEnv }; }); diff --git a/packages/core/src/core/skillSource.ts b/packages/core/src/core/skillSource.ts index 8f1584d..09c3757 100644 --- a/packages/core/src/core/skillSource.ts +++ b/packages/core/src/core/skillSource.ts @@ -1,4 +1,4 @@ -// SkillSource — the enumeration seam for "which skills/workflows exist". +// SkillSource — the enumeration seam for "which marketplace items exist". // // skill-nft-structure.md §2 is emphatic: "No skills registry table. The NFT // collection IS the skill list." The canonical truth is each mint's `uri` @@ -13,7 +13,7 @@ // is a real, visible answer — not a hidden failure). The seam stays so a // gateway enumerator can replace it later without changing search/reviews. -import type { Skill } from "./types.js"; +import type { MarketItemType, Skill } from "./types.js"; import { resolveRpcUrl } from "./rpc.js"; /** Token-2022 program id — our skill/workflow mints all live under it (NonTransferable + @@ -33,7 +33,7 @@ type DasRpcResponse = { }; export interface SkillSource { - /** Enumerate all known skills/workflows (id set + cached metadata snapshot). */ + /** Enumerate all known skills/workflows/plugins (id set + cached metadata snapshot). */ listSkills(limit?: number): Promise; /** * True when listSkills() returns live `supply` already filled (e.g. an indexer @@ -47,20 +47,33 @@ export interface SkillSource { * (skill-nft-json.md §4/§4b): `category` (single), `skill` (repeated hashtags), * and `requiredSkill` (repeated prerequisite mint ids, workflows only). DAS * surfaces these under content.metadata.attributes after resolving the uri. */ -function traitsFromAttributes( +export function traitsFromAttributes( attributes: unknown, -): { category: string; hashtags: string[]; requiredSkills: string[] } { - if (!Array.isArray(attributes)) return { category: "", hashtags: [], requiredSkills: [] }; +): { + category: string; + hashtags: string[]; + requiredSkills: string[]; + engines: string[]; + iqGitPda?: string; +} { + if (!Array.isArray(attributes)) { + return { category: "", hashtags: [], requiredSkills: [], engines: [] }; + } let category = ""; const hashtags: string[] = []; const requiredSkills: string[] = []; + const engines: string[] = []; + let iqGitPda: string | undefined; for (const a of attributes) { if (!a || typeof a.value !== "string") continue; if (a.trait_type === "category") category = a.value; else if (a.trait_type === "skill") hashtags.push(a.value); + else if (a.trait_type === "plugin") hashtags.push(a.value); + else if (a.trait_type === "engine") engines.push(a.value); + else if (a.trait_type === "iqGitPda") iqGitPda = a.value; else if (a.trait_type === "requiredSkill") requiredSkills.push(a.value); } - return { category, hashtags, requiredSkills }; + return { category, hashtags, requiredSkills, engines, iqGitPda }; } /** @@ -81,19 +94,20 @@ export const dasSource: SkillSource = { // Note: the default lacks DAS, so reads come back empty there — a Helius key is // what actually surfaces skills (the UI flags this). const rpcUrl = await resolveRpcUrl(); - const { getSkillsCollectionMint, getWorkflowsCollectionMint } = await import("./seed.js"); + const { getSkillsCollectionMint, getWorkflowsCollectionMint, getPluginsCollectionMint } = await import("./seed.js"); const skillsCollection = getSkillsCollectionMint(); const workflowsCollection = getWorkflowsCollectionMint(); + const pluginsCollection = getPluginsCollectionMint(); - if (!skillsCollection && !workflowsCollection) { + if (!skillsCollection && !workflowsCollection && !pluginsCollection) { throw new Error( - "dasSource: no collection mints configured (AGENTNET_SKILLS_COLLECTION_PUBKEY / _WORKFLOWS_)", + "dasSource: no collection mints configured (AGENTNET_SKILLS_COLLECTION_PUBKEY / _WORKFLOWS_ / _PLUGINS_)", ); } const skills: Skill[] = []; - async function fetchGroup(group: string, type: "skill" | "workflow") { + async function fetchGroup(group: string, type: MarketItemType) { const response = await fetch(rpcUrl!, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -115,18 +129,24 @@ export const dasSource: SkillSource = { // hashtags come straight from the scan (skill-nft-json.md §4), no // per-mint re-read. supply is the one field DAS doesn't carry live, so // search.ts still hydrates that from the mint. - const { category, hashtags, requiredSkills } = traitsFromAttributes(item.content?.metadata?.attributes); + const { category, hashtags, requiredSkills, engines, iqGitPda } = traitsFromAttributes(item.content?.metadata?.attributes); + const metadata = item.content?.metadata ?? {}; skills.push({ id: item.id, type, - name: item.content?.metadata?.name || "Unknown", - description: item.content?.metadata?.description || "", + name: metadata.name || "Unknown", + description: metadata.description || "", // Token-2022 has no Metaplex `creators`; fall back to the asset // authority (= update authority). May be empty — caller tolerates. creator: item.authorities?.[0]?.address || "", category, hashtags, requiredSkills, + engines, + iqGitPda, + version: typeof metadata.version === "string" ? metadata.version : undefined, + capabilities: Array.isArray(metadata.capabilities) ? metadata.capabilities.filter((v: unknown): v is string => typeof v === "string") : undefined, + permissions: Array.isArray(metadata.permissions) ? metadata.permissions.filter((v: unknown): v is string => typeof v === "string") : undefined, price: "0", supply: 0, // hydrated by getMintSupply (live counter, not in the scan) uriTxid: item.content?.json_uri || "", @@ -137,6 +157,7 @@ export const dasSource: SkillSource = { if (skillsCollection) await fetchGroup(skillsCollection, "skill"); if (workflowsCollection) await fetchGroup(workflowsCollection, "workflow"); + if (pluginsCollection) await fetchGroup(pluginsCollection, "plugin"); return skills.slice(0, limit); }, @@ -368,13 +389,16 @@ export async function ownedSkills( * core stays independent of the indexer repo — we only depend on its wire JSON. */ interface IndexerItem { mint: string; - type: "skill" | "workflow"; + type: MarketItemType; name: string; description: string; creator: string | null; supply: number; price: string | null; // lamports (decimal string) from the on-chain ItemConfig PDA attributes: { trait_type: string; value: string }[]; + version?: string | null; + capabilities?: string[]; + permissions?: string[]; } /** @@ -398,7 +422,7 @@ export function indexerSource(baseUrl: string): SkillSource { if (!res.ok) throw new Error(`indexer /items → HTTP ${res.status}`); const { items } = (await res.json()) as { items: IndexerItem[] }; return items.map((it) => { - const { category, hashtags, requiredSkills } = traitsFromAttributes(it.attributes); + const { category, hashtags, requiredSkills, engines, iqGitPda } = traitsFromAttributes(it.attributes); return { id: it.mint, type: it.type, @@ -408,6 +432,11 @@ export function indexerSource(baseUrl: string): SkillSource { category, hashtags, requiredSkills, + engines, + iqGitPda, + version: it.version ?? undefined, + capabilities: it.capabilities, + permissions: it.permissions, price: it.price ?? undefined, // on-chain price (lamports); absent if unpriced supply: it.supply, // live — already hydrated by the indexer uriTxid: "", diff --git a/packages/core/src/core/types.ts b/packages/core/src/core/types.ts index b6321eb..cba89c1 100644 --- a/packages/core/src/core/types.ts +++ b/packages/core/src/core/types.ts @@ -15,10 +15,13 @@ export interface Session { title?: string; } -/** Skill NFT metadata. */ +/** Marketplace NFT umbrella kind. */ +export type MarketItemType = "skill" | "workflow" | "plugin"; + +/** Skill / workflow / plugin NFT metadata. */ export interface Skill { id: string; // NFT mint address - type?: "skill" | "workflow"; // distinguishes between skills and workflows in search + type?: MarketItemType; // distinguishes between skills, workflows, and plugins in search name: string; description: string; creator: string; // wallet address @@ -28,6 +31,13 @@ export interface Skill { // traits, skill-nft-json.md §4b). Empty/absent for a plain skill. Carried on // Skill so an enumeration result is enough to compute "what can I unlock". requiredSkills?: string[]; + // Plugins only: engine badges and provenance/install hints from plugin NFT JSON. + // `engines` is what the UI later renders as "Claude" / "Codex" / "MCP" support. + engines?: string[]; + iqGitPda?: string; + version?: string; + capabilities?: string[]; + permissions?: string[]; price?: string; // lamports as decimal string (bigint isn't JSON-serializable) supply: number; // mint supply = popularity uriTxid: string; // codeIn txid holding the skill text @@ -37,7 +47,7 @@ export interface Skill { /** Workflow NFT metadata — a skill bundle with gates. */ export interface Workflow { id: string; // NFT mint address - type?: "skill" | "workflow"; + type?: MarketItemType; name: string; description: string; creator: string; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 41b239c..c01dd90 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,7 +14,7 @@ export type { // ─── On-chain marketplace layer (from Step-0 core PR: nft/search/notes/etc.) ── // chain + seed + domain types -export type { SignerInput, Session, Skill, Workflow, Note, Row, ReadOptions } from "./core/types.js"; +export type { SignerInput, Session, MarketItemType, Skill, Workflow, Note, Row, ReadOptions } from "./core/types.js"; export { init as initChain, ensureDbRoot, @@ -28,7 +28,7 @@ export { getTablePdaRef, signerAddress, } from "./core/chain.js"; -export { AGENTNET_ROOT_ID, mysessionsHint, reviewsHint, reviewsAgentHint } from "./core/seed.js"; +export { AGENTNET_ROOT_ID, mysessionsHint, reviewsHint, reviewsAgentHint, collectionFor, getPluginsCollectionMint } from "./core/seed.js"; // skill / workflow NFTs (Token-2022 + code-in) export { publishSkill, diff --git a/packages/core/src/search/search.ts b/packages/core/src/search/search.ts index 837099b..86d0ebb 100644 --- a/packages/core/src/search/search.ts +++ b/packages/core/src/search/search.ts @@ -14,7 +14,7 @@ // below is literal substring, not semantic. import type { Connection } from "@solana/web3.js"; -import type { Skill } from "../core/types.js"; +import type { MarketItemType, Skill } from "../core/types.js"; import { dasSource, type SkillSource } from "../core/skillSource.js"; import { getMintSupply, readSkillMintMetadata } from "../nft/token2022.js"; @@ -22,7 +22,7 @@ export interface SearchFilters { keyword?: string; category?: string; hashtags?: string[]; - type?: "skill" | "workflow"; + type?: MarketItemType; } export type SortBy = "supply" | "name" | "recent"; diff --git a/packages/core/src/skill-market/index.ts b/packages/core/src/skill-market/index.ts index 8f8961a..c1b753e 100644 --- a/packages/core/src/skill-market/index.ts +++ b/packages/core/src/skill-market/index.ts @@ -25,6 +25,7 @@ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk"; import { z } from "zod"; import type { Connection } from "@solana/web3.js"; import type { SignerInput } from "@iqlabs-official/solana-sdk/utils"; +import type { MarketItemType } from "../core/types.js"; import { searchSkills } from "../search/search.js"; import { publishSkill } from "../nft/skill.js"; import { readSkillText } from "../nft/token2022.js"; @@ -139,11 +140,11 @@ export const agentNetAllowedTools = (): string[] => const SKILL_TOOLS: { name: string; description: string; schema: z.ZodRawShape }[] = [ { name: "search_skills", - description: "Search the AgentNet marketplace for available skills and workflows.", + description: "Search the AgentNet marketplace for available skills, workflows, and plugin NFTs.", schema: { keyword: z.string().optional().describe("Optional keyword to search in skill names and descriptions."), category: z.string().optional().describe("Optional category to filter skills by (e.g. 'ai', 'frontend')."), - type: z.enum(["skill", "workflow"]).optional().describe("Whether to search for individual skills or workflow bundles."), + type: z.enum(["skill", "workflow", "plugin"]).optional().describe("Whether to search for individual skills, workflow bundles, or plugin packages."), }, }, { @@ -172,7 +173,7 @@ const SKILL_TOOLS: { name: string; description: string; schema: z.ZodRawShape }[ description: "Post a comment/review on a skill. You must hold ≥1 of the skill's token to comment.", schema: { skillId: z.string().describe("The base58 mint address of the skill to comment on."), - collectionId: z.string().optional().describe("The collection mint address the skill belongs to (skills or workflows collection). Omit to use the default skills collection."), + collectionId: z.string().optional().describe("The collection mint address the item belongs to (skills, workflows, or plugins collection). Omit to use the default skills collection."), text: z.string().describe("The comment text (markdown supported)."), gitLink: z.string().optional().describe("Optional GitHub or on-chain git URL to attach to the comment."), }, @@ -238,11 +239,16 @@ export async function handleToolCall( if (name === "search_skills") { const keyword = args?.keyword as string | undefined; const category = args?.category as string | undefined; - const typeFilter = args?.type as "skill" | "workflow" | undefined; + const typeFilter = args?.type as MarketItemType | undefined; const skills = await searchSkills(conn, { filters: { keyword, category, type: typeFilter } }); if (skills.length === 0) return { content: [{ type: "text", text: "No matching skills found." }] }; const formatted = skills - .map((s) => `- ID: ${s.id}\n Name: ${s.name}\n Type: ${s.type ?? "skill"}\n Category: ${s.category}\n Creator: ${s.creator}\n Description: ${s.description}`) + .map((s) => { + const pluginBits = s.type === "plugin" + ? `\n Engines: ${(s.engines ?? []).join(", ") || "unspecified"}${s.iqGitPda ? `\n IQ Git PDA: ${s.iqGitPda}` : ""}` + : ""; + return `- ID: ${s.id}\n Name: ${s.name}\n Type: ${s.type ?? "skill"}\n Category: ${s.category}\n Creator: ${s.creator}\n Description: ${s.description}${pluginBits}`; + }) .join("\n\n"); return { content: [{ type: "text", text: `Found ${skills.length} results:\n\n${formatted}` }] }; } @@ -397,4 +403,4 @@ export function createAgentSdkMcpServer( ); return createSdkMcpServer({ name: AGENTNET_MCP_SERVER, version: "0.0.1", tools }); -} \ No newline at end of file +} diff --git a/packages/core/src/skill-market/ingest/env.ts b/packages/core/src/skill-market/ingest/env.ts index adf2a46..e9cc449 100644 --- a/packages/core/src/skill-market/ingest/env.ts +++ b/packages/core/src/skill-market/ingest/env.ts @@ -23,9 +23,9 @@ import { classifySkills, readSkillManifest } from "../registry.js"; import { resolveRpcUrl } from "../../core/rpc.js"; import { init as initChain } from "../../core/chain.js"; import type { AgentProfile, SkillCard, SkillDetail } from "../../chat/marketMessages.js"; -import type { Skill } from "../../core/types.js"; +import type { MarketItemType, Skill } from "../../core/types.js"; import { readNotes, postNote as corePostNote, readAgentNotes, postAgentNote as corePostAgentNote } from "../../notes/notes.js"; -import { getSkillsCollectionMint, getWorkflowsCollectionMint, getIndexerUrl } from "../../core/seed.js"; +import { getSkillsCollectionMint, getWorkflowsCollectionMint, getPluginsCollectionMint, getIndexerUrl } from "../../core/seed.js"; import { getLeaderboard, getReputation } from "../../reputation/reputation.js"; import { SkillSync } from "./index.js"; @@ -36,6 +36,8 @@ function toCard(s: Skill): SkillCard { id: s.id, type: s.type, name: s.name, description: s.description, category: s.category, hashtags: s.hashtags, supply: s.supply, price: s.price, creator: s.creator, requiredSkills: s.requiredSkills, + engines: s.engines, iqGitPda: s.iqGitPda, version: s.version, + capabilities: s.capabilities, permissions: s.permissions, }; } @@ -51,8 +53,11 @@ function toCard(s: Skill): SkillCard { // down. Override the URL with AGENTNET_INDEXER_URL. const INDEXER_URL = getIndexerUrl(); -function collectionIdFor(type?: "skill" | "workflow"): Promise { - const id = type === "workflow" ? getWorkflowsCollectionMint() : getSkillsCollectionMint(); +function collectionIdFor(type?: MarketItemType): Promise { + const id = + type === "workflow" ? getWorkflowsCollectionMint() + : type === "plugin" ? getPluginsCollectionMint() + : getSkillsCollectionMint(); return Promise.resolve(id); } @@ -98,7 +103,7 @@ export async function marketplaceEnv(wallet: Wallet) { // Run a search via the indexer (fast, has supply+traits); fall back to a direct DAS // scan only if it errors (server down). `kind` = the active Skills/Workflows tab. - async function runSearch(query: string, kind?: "skill" | "workflow"): Promise { + async function runSearch(query: string, kind?: MarketItemType): Promise { const filters = { keyword: query, ...(kind ? { type: kind } : {}) }; try { return await searchSkills(conn, { source: indexerSource(INDEXER_URL), filters }); @@ -108,7 +113,7 @@ export async function marketplaceEnv(wallet: Wallet) { } return { - async searchSkills(query: string, kind?: "skill" | "workflow"): Promise { + async searchSkills(query: string, kind?: MarketItemType): Promise { return (await runSearch(query, kind)).map(toCard); }, @@ -147,9 +152,9 @@ export async function marketplaceEnv(wallet: Wallet) { }, // Post a comment on a skill (issue #34). collectionId resolved from skillType. - async postNote(skillId: string, skillType: "skill" | "workflow" | undefined, text: string, gitLink?: string) { + async postNote(skillId: string, skillType: MarketItemType | undefined, text: string, gitLink?: string) { const collectionId = await collectionIdFor(skillType); - if (!collectionId) return { ok: false, error: "Skills collection not configured" }; + if (!collectionId) return { ok: false, error: `${skillType ?? "skill"} collection not configured` }; try { await corePostNote(conn, wallet, { collectionId, skillId, text, gitLink }); const notes = await readNotes(collectionId, skillId).catch(() => []); diff --git a/plans/onchain-format/skill-nft-json.md b/plans/onchain-format/skill-nft-json.md index bfd4a1f..9baa867 100644 --- a/plans/onchain-format/skill-nft-json.md +++ b/plans/onchain-format/skill-nft-json.md @@ -173,6 +173,50 @@ mints (§3 there). Both are trivial because the trait is already the mint id. --- +## 4c. Plugin NFT — same standard JSON, plugin-specific extensions + +A **plugin** is the third marketplace umbrella collection beside skills and +workflows ([`../plugin-nft.md`](../plugin-nft.md)). It keeps the same Token-2022 + +code-in shape, but its JSON describes an installable plugin package/version +instead of a skill body or workflow recipe. + +```jsonc +{ + "name": "iq-git-reviewer", + "image": "", + "description": "Review code with IQ Git context.", + "attributes": [ + { "trait_type": "category", "value": "developer-tools" }, + { "trait_type": "plugin", "value": "git" }, + { "trait_type": "plugin", "value": "review" }, + { "trait_type": "engine", "value": "claude" }, + { "trait_type": "engine", "value": "codex" }, + { "trait_type": "iqGitPda", "value": "IqGitPda111..." } + ], + "version": "1.2.3", + "iqGitPda": "IqGitPda111...", + "engines": ["claude", "codex"], + "capabilities": ["git.read", "review.write"], + "permissions": ["fs.read"], + "pluginManifest": { + "id": "iq-git-reviewer", + "entrypoint": ".codex-plugin/plugin.json" + } +} +``` + +- **`engine`** → one row per supported runtime, e.g. `claude`, `codex`, `mcp`. + This is the marketplace badge source; a plugin is not assumed to be Claude-only. +- **`plugin`** → repeated tag rows, parallel to skill hashtag traits. +- **`iqGitPda`** → the canonical provenance anchor for the plugin package. +- **`pluginManifest`** → install metadata. Buyers/equippers must still validate it + before writing local plugin files. + +This section defines the data contract only. Plugin publish/install UI is a later +slice. + +--- + ## 5. `uri = txid`, kept pure — no gateway URL baked in `uri` holds the **code-in txid**, never a gateway URL. @@ -207,6 +251,8 @@ this on-chain format. We don't design it in now. 7. Workflow NFT = same ② JSON in its own collection; `requiredSkill` = one repeated trait per prerequisite, valued by the skill's **NFT id (mint addr)**, not its name (§4b). +8. Plugin NFT = same ② JSON in its own collection; `engine` traits badge Claude, + Codex, MCP, and future runtimes; IQ Git PDA is the provenance anchor (§4c). ## 8. Still open (not decided — carry forward) diff --git a/plans/onchain-format/tables.md b/plans/onchain-format/tables.md index 49daa0d..5312cc5 100644 --- a/plans/onchain-format/tables.md +++ b/plans/onchain-format/tables.md @@ -23,7 +23,7 @@ mints themselves are NOT tables (the Token-2022 collection IS the registry; see | Table (seed hint) | Key | Holds | Writers | |---|---|---|---| | `mysessions:{wallet}` | wallet | session **pointer** list (sessionId), not the blob | owner only | -| `reviews:{collectionId}:{nft}` | collection mint + item mint | comments on a skill/workflow item | (gate §3) | +| `reviews:{collectionId}:{nft}` | collection mint + item mint | comments on a skill/workflow/plugin item | (gate §3) | | `reviews:agent:{wallet}` | agent wallet | comments on an agent + the owner's self-notes (blog) | (gate §3) | That's the entire table surface. The hint strings are produced by functions in @@ -45,14 +45,18 @@ That's the entire table surface. The hint strings are produced by functions in collection (the umbrella) item (under it) ┌─ skills collection mint ──┬── skill NFT mint ── reviews:{skillsColl}:{skillNft} │ └── skill NFT mint ── reviews:{skillsColl}:{skillNft} - └─ workflows collection ────┬── workflow NFT ──── reviews:{wfColl}:{wfNft} + ├─ workflows collection ────┬── workflow NFT ──── reviews:{wfColl}:{wfNft} + │ └── … + └─ plugins collection ──────┬── plugin NFT ────── reviews:{pluginColl}:{pluginNft} └── … ``` -- **`collectionId`** = the umbrella collection mint (skills / workflows / a future - kind). zo: "컬렉션은 skills or workflow or 미래에 들어갈 수 있는 다른 우산이고, +- **`collectionId`** = the umbrella collection mint (skills / workflows / plugins + / a future kind). zo: "컬렉션은 skills or workflow or 미래에 들어갈 수 있는 다른 우산이고, mint는 그 안에 각각 다른 아이템들이 스킬이 되는 것." -- **There are only two collections today** (skills, workflows). The mapping +- **There are three marketplace collection kinds** (skills, workflows, plugins). + The plugin collection may be unminted/unconfigured until the plugin market slice. + The mapping "item type → collection mint" is hardcoded in **one place** — `collectionFor(type)` in `seed.ts` (reads the configured collection pubkeys). A new umbrella adds a branch there, nowhere else. zo: "이건 어디 중앙화된 곳에 @@ -108,7 +112,7 @@ real on-chain enforcement waits on SDK Token-2022 ATA support. There is **no `skills:index`** and no off-chain index/cache designed into the structure at all (`501ac84`). -- "Which skills/workflows exist" = scan the Token-2022 **collection** via DAS +- "Which skills/workflows/plugins exist" = scan the Token-2022 **collection** via DAS `getAssetsByGroup` (`dasSource` in `core/skillSource.ts`, now the only + default `SkillSource`). The mint is the registry; nothing is mirrored into a table. - `publishSkill` / `publishWorkflow` no longer write an index row — they just mint. @@ -145,8 +149,8 @@ zo: "민팅 수량이나 그런 걸 읽은 다음에 notes 등을 읽을 테니 1. Tables under `agentnet-root` = **`mysessions`, `reviews:*`** — that's all. 2. `reviews` keyed by **collection THEN item**; `collectionId` = the - umbrella collection mint. Only two collections (skills, workflows); the - type→collection map is hardcoded in `collectionFor(type)` in `seed.ts`. + umbrella collection mint. Marketplace collections are skills, workflows, and + plugins; the type→collection map is hardcoded in `collectionFor(type)` in `seed.ts`. 3. notes → **reviews** (rename). **No `audit` table** — skill safety is verified reader-side (buyer's agent runs a "verify" skill before buying), not an on-chain admin/QAgent record. diff --git a/plans/plugin-nft.md b/plans/plugin-nft.md new file mode 100644 index 0000000..10ab2c7 --- /dev/null +++ b/plans/plugin-nft.md @@ -0,0 +1,101 @@ +# Plugin NFT + +> Sibling collections: [`skill-nft-structure.md`](skill-nft-structure.md) and +> [`workflow-nft.md`](workflow-nft.md). A plugin is an installable capability bundle +> for one or more agent engines, anchored to IQ Git provenance. + +--- + +## 0. What a plugin is + +A **skill** is an ability the agent reads and follows. A **workflow** is a gated +recipe that proves an agent owns the required skills. A **plugin** is an +installable package: skills, MCP servers, hooks, apps, manifests, or other runtime +capabilities bundled behind a versioned plugin manifest. + +Plugin NFTs are not Claude-only. Claude, Codex, MCP, and future runtimes are +represented as engine badges in metadata. + +## 1. Same Token-2022 pattern, separate collection + +Plugins use the same NFT foundation as skills/workflows: + +| | Plugin collection | +|---|---| +| token | one Token-2022 mint per plugin package/version | +| soulbound | `NonTransferable` | +| popularity | `mint.supply` | +| content (`uri`) | code-in plugin NFT JSON | +| provenance | IQ Git PDA in JSON + `iqGitPda` trait | +| engine badges | repeated `engine` traits, e.g. `claude`, `codex`, `mcp` | + +The plugin collection is its own umbrella collection. It must not be mixed into +skills or workflows because plugin search, install, and trust checks have +different semantics. + +## 2. Plugin JSON shape + +The code-in `uri` points at a standard NFT JSON object with AgentNet plugin +extensions: + +```jsonc +{ + "name": "iq-git-reviewer", + "image": "", + "description": "Review code with IQ Git context.", + "attributes": [ + { "trait_type": "category", "value": "developer-tools" }, + { "trait_type": "plugin", "value": "git" }, + { "trait_type": "plugin", "value": "review" }, + { "trait_type": "engine", "value": "claude" }, + { "trait_type": "engine", "value": "codex" }, + { "trait_type": "iqGitPda", "value": "IqGitPda111..." } + ], + "version": "1.2.3", + "iqGitPda": "IqGitPda111...", + "engines": ["claude", "codex"], + "capabilities": ["git.read", "review.write"], + "permissions": ["fs.read"], + "pluginManifest": { + "id": "iq-git-reviewer", + "entrypoint": ".codex-plugin/plugin.json" + } +} +``` + +Rules: + +- `engine` is repeated once per supported runtime. This is what surfaces render + as Claude/Codex/MCP badges. +- `iqGitPda` is the canonical provenance anchor. URLs may be display helpers, but + they are not the identity. +- `plugin` traits are tag-like labels, parallel to `skill` hashtag traits. +- `pluginManifest` is descriptive in this v1 slice. Installers must still + validate the manifest and permissions before materializing anything locally. + +## 3. Buy/equip model + +The intended v1 UX is **buy equals equip**, matching skills. Buying proves the +wallet owns the plugin package, and the local runtime may then install/equip it +after manifest and permission validation. + +This document does not implement minting, publishing, or local install. It only +settles the collection and metadata shape those flows will use. + +## 4. Reviews + +Plugin comments reuse the existing review table shape: + +``` +reviews:{pluginsCollectionMint}:{pluginMint} +``` + +No plugin-specific review table is added. + +## 5. Build order + +1. Add plugin collection/type plumbing and metadata parsing. +2. Add plugin browse/detail UI with engine badges and IQ Git PDA display. +3. Add plugin publish from IQ Git PDA + plugin manifest. +4. Add buy/equip that validates manifest, permissions, and engine compatibility. +5. Show owned/equipped plugins on agent profiles beside skills and workflows. diff --git a/surfaces/cli/src/views/SkillMarket.tsx b/surfaces/cli/src/views/SkillMarket.tsx index 79ad4e8..3b5bde8 100644 --- a/surfaces/cli/src/views/SkillMarket.tsx +++ b/surfaces/cli/src/views/SkillMarket.tsx @@ -1,15 +1,15 @@ import React, { useEffect, useState } from "react"; import { Box, Text, useInput } from "ink"; -import type { SkillCard, SkillDetail } from "@iqlabs-official/agent-sdk"; +import type { MarketItemType, SkillCard, SkillDetail } from "@iqlabs-official/agent-sdk"; import type { Reputation, AgentProfile } from "@iqlabs-official/agent-sdk"; import { colors, glyph } from "../theme.js"; export interface MarketApi { - searchSkills(query: string, kind?: "skill" | "workflow"): Promise; + searchSkills(query: string, kind?: MarketItemType): Promise; getSkillDetail(mint: string): Promise; buySkill(skillId: string, creatorWallet?: string): Promise<{ ok: boolean; slug?: string; error?: string }>; solBalance(): Promise; - postNote(skillId: string, skillType: "skill" | "workflow" | undefined, text: string, gitLink?: string): Promise<{ ok: boolean; error?: string }>; + postNote(skillId: string, skillType: MarketItemType | undefined, text: string, gitLink?: string): Promise<{ ok: boolean; error?: string }>; publishSkill(input: { name: string; description: string; text: string; category?: string; hashtags?: string[]; priceSol: string }): Promise<{ ok: boolean; mint?: string; error?: string }>; listAgents(): Promise; getAgentProfile(wallet: string): Promise; diff --git a/surfaces/webview/src/transport/protocol.ts b/surfaces/webview/src/transport/protocol.ts index 852c404..6559906 100644 --- a/surfaces/webview/src/transport/protocol.ts +++ b/surfaces/webview/src/transport/protocol.ts @@ -6,6 +6,7 @@ // Market types are defined once in packages/core and re-exported here so every surface // that imports from this file gets compile-time checking on the market message contract. +import type { MarketItemType } from "@iqlabs-official/agent-sdk"; export type { SkillCard, SkillDetail, MarketRequest, MarketEvent, RpcStatus, AgentProfile, Reputation } from "@iqlabs-official/agent-sdk"; // ── shared payload shapes ── @@ -120,7 +121,7 @@ export type ClientMessage = | { type: "setGoogleCredentials"; clientId: string; clientSecret?: string } | { type: "toast"; text: string } // ── market (UI→server) ── - | { type: "searchSkills"; query: string; kind?: "skill" | "workflow" } + | { type: "searchSkills"; query: string; kind?: MarketItemType } | { type: "getSkillDetail"; mint: string } | { type: "buySkill"; skillId: string; creatorWallet?: string } | { type: "ownedSkills" } @@ -131,7 +132,7 @@ export type ClientMessage = | { type: "listAgents" } | { type: "getAgentProfile"; wallet: string } | { type: "buyAllSkills"; wallet: string } - | { type: "postNote"; skillId: string; skillType?: "skill" | "workflow"; text: string; gitLink?: string } + | { type: "postNote"; skillId: string; skillType?: MarketItemType; text: string; gitLink?: string } | { type: "postAgentNote"; agentWallet: string; text: string; gitLink?: string } | { type: "publishSkill"; From f95f03060b533f28e11a8392ebddb8a77ee78a19 Mon Sep 17 00:00:00 2001 From: mega123-art Date: Sat, 20 Jun 2026 02:37:45 +0530 Subject: [PATCH 3/7] Enhance market functionality to support plugins in SkillMarket and related components --- packages/core/src/chat/ui/webview.ts | 60 +++++++++++++++---- surfaces/cli/src/views/SkillMarket.tsx | 43 +++++++++---- surfaces/webview/src/market/MarketScreen.tsx | 13 ++-- surfaces/webview/src/market/SkillCardTile.tsx | 4 ++ .../webview/src/market/SkillDetailView.tsx | 28 ++++++++- surfaces/webview/src/state/store.tsx | 7 ++- 6 files changed, 123 insertions(+), 32 deletions(-) diff --git a/packages/core/src/chat/ui/webview.ts b/packages/core/src/chat/ui/webview.ts index 66a2a59..e47fe1e 100644 --- a/packages/core/src/chat/ui/webview.ts +++ b/packages/core/src/chat/ui/webview.ts @@ -679,7 +679,7 @@ export function chatHtml(): string { /* a card body is clickable (opens detail); the Buy button stops propagation */ .mktCard .mc-main { cursor: pointer; } .mktCard .mc-main:hover .mc-name { color: var(--an-green); } - /* Skills / Workflows segmented tabs */ + /* Skills / Workflows / Plugins segmented tabs */ .mktTabs { display: inline-flex; gap: 2px; padding: 2px; margin-bottom: 12px; background: var(--an-bg); border: 1px solid var(--an-line); border-radius: 999px; } .mktTab { background: transparent; border: none; color: var(--vscode-foreground); opacity: 0.6; @@ -1490,6 +1490,7 @@ export function chatHtml(): string {
+
@@ -3564,7 +3565,7 @@ export function chatHtml(): string { const mktDetailEl = document.getElementById('mktDetail'); const mktDetailBody = document.getElementById('mktDetailBody'); let lastMarketResults = []; // last search results, kept to re-render on owned-list change - let currentKind = 'skill'; // active tab: Skills | Workflows + let currentKind = 'skill'; // active tab: Skills | Workflows | Plugins let currentDetail = null; // { id, type } of the open detail — for comments refresh function runMarketSearch() { mktResults.innerHTML = '
Searching…
'; @@ -3586,7 +3587,7 @@ export function chatHtml(): string { if (e.isComposing || e.keyCode === 229) return; if (e.key === 'Enter') { e.preventDefault(); runMarketSearch(); } }); - // Skills / Workflows tabs — switching re-runs the search filtered to that kind. + // Skills / Workflows / Plugins tabs — switching re-runs the search filtered to that kind. for (const tab of document.querySelectorAll('.mktTab')) { tab.addEventListener('click', () => { currentKind = tab.getAttribute('data-kind'); @@ -3642,11 +3643,23 @@ export function chatHtml(): string { const addTag = (t) => { const s = document.createElement('span'); s.className = 'dt-tag'; s.textContent = t; meta.appendChild(s); }; if (c.category) addTag(c.category); for (const h of (c.hashtags || [])) addTag('#' + h); + for (const e of (c.engines || [])) addTag(e); + if (c.version) addTag('v' + c.version); if (typeof c.supply === 'number') addTag(c.supply + '\\u00d7 owned'); const price = fmtPrice(c.price); if (price) addTag(price); if (meta.childElementCount) skillModalBody.appendChild(meta); + if (c.type === 'plugin') { + const sec = document.createElement('div'); sec.className = 'dt-sec'; sec.textContent = 'Plugin package'; + const bd = document.createElement('div'); bd.className = 'dt-body'; + const lines = []; + if (c.iqGitPda) lines.push('IQ Git PDA: ' + c.iqGitPda); + if (c.capabilities && c.capabilities.length) lines.push('Capabilities: ' + c.capabilities.join(', ')); + if (c.permissions && c.permissions.length) lines.push('Permissions: ' + c.permissions.join(', ')); + bd.textContent = lines.length ? lines.join('\\n') : 'Plugin metadata is available. Install/equip support is not wired yet.'; + skillModalBody.appendChild(sec); skillModalBody.appendChild(bd); + } // buy (hidden on your own skills — you can't buy what you authored) - if (!owned || c.type) { + if (c.type !== 'plugin' && (!owned || c.type)) { const buy = document.createElement('button'); buy.className = 'dt-buy'; const buyLabel = price && price !== 'Free' ? ('Buy · ' + price) : 'Buy'; buy.textContent = owned ? 'Owned' : buyLabel; buy.disabled = owned; @@ -3655,8 +3668,12 @@ export function chatHtml(): string { vscode.postMessage({ type: 'buySkill', skillId: c.id, creatorWallet: c.creator }); }); skillModalBody.appendChild(buy); skillModalBuyBtn = buy; + } else if (c.type === 'plugin') { + const buy = document.createElement('button'); buy.className = 'dt-buy'; + buy.textContent = 'Plugin install/equip coming next'; buy.disabled = true; + skillModalBody.appendChild(buy); skillModalBuyBtn = buy; } - if (detail && detail.skillText) { + if (detail && detail.skillText && c.type !== 'plugin') { const sec = document.createElement('div'); sec.className = 'dt-sec'; sec.textContent = (c.type === 'workflow' ? 'Workflow' : 'Skill') + ' text'; const bd = document.createElement('div'); bd.className = 'dt-body'; bd.textContent = detail.skillText; skillModalBody.appendChild(sec); skillModalBody.appendChild(bd); @@ -3758,7 +3775,7 @@ export function chatHtml(): string { inputWrap.appendChild(ta); inputWrap.appendChild(errEl); inputWrap.appendChild(submit); } else { const gate = document.createElement('div'); gate.className = 'dt-note-gate'; - gate.textContent = 'Buy this skill to leave a comment.'; + gate.textContent = skillType === 'plugin' ? 'Own this plugin to leave a comment.' : 'Buy this skill to leave a comment.'; inputWrap.appendChild(gate); } wrap.appendChild(inputWrap); @@ -3795,10 +3812,28 @@ export function chatHtml(): string { const addTag = (t) => { const s = document.createElement('span'); s.className = 'dt-tag'; s.textContent = t; meta.appendChild(s); }; if (c.category) addTag(c.category); for (const h of (c.hashtags || [])) addTag('#' + h); + for (const e of (c.engines || [])) addTag(e); + if (c.version) addTag('v' + c.version); if (typeof c.supply === 'number') addTag(c.supply + '\\u00d7 owned'); const detailPrice = fmtPrice(c.price); if (detailPrice) addTag(detailPrice); // "Free" / "0.1 SOL" if (meta.childElementCount) mktDetailBody.appendChild(meta); + if (c.type === 'plugin') { + const sec = document.createElement('div'); sec.className = 'dt-sec'; sec.textContent = 'Plugin package'; + const body = document.createElement('div'); body.className = 'dt-body'; + const lines = []; + if (c.iqGitPda) lines.push('IQ Git PDA: ' + c.iqGitPda); + if (c.capabilities && c.capabilities.length) lines.push('Capabilities: ' + c.capabilities.join(', ')); + if (c.permissions && c.permissions.length) lines.push('Permissions: ' + c.permissions.join(', ')); + body.textContent = lines.length ? lines.join('\\n') : 'Plugin metadata is available. Install/equip support is not wired yet.'; + mktDetailBody.appendChild(sec); mktDetailBody.appendChild(body); + const install = document.createElement('button'); install.className = 'dt-buy'; + install.textContent = 'Plugin install/equip coming next'; install.disabled = true; + mktDetailBody.appendChild(install); + detailBuyBtn = install; currentDetailName = c.name || null; + renderComments(c.id, c.type, detail && detail.notes, owned); + return; + } // buy / re-equip / remove. A disposed skill (owned on-chain, un-equipped) shows a free // Re-equip; an owned skill shows "Owned" + a Remove (dispose); else the priced Buy. const buy = document.createElement('button'); buy.className = 'dt-buy'; @@ -3858,9 +3893,10 @@ export function chatHtml(): string { mktResults.innerHTML = ''; if (!results.length) { // empty can mean "no match" OR "no DAS RPC so reads return nothing" — say which. + const kindLabel = currentKind === 'workflow' ? 'workflows' : currentKind === 'plugin' ? 'plugins' : 'skills'; mktResults.innerHTML = dasReady - ? '
No skills found.
' - : '
No skills found. The default RPC can\\'t read the marketplace. Add a Helius key (free devnet tier) in the wallet menu \\u2192 RPC.
'; + ? '
No ' + kindLabel + ' found.
' + : '
No ' + kindLabel + ' found. The default RPC can\\'t read the marketplace. Add a Helius key (free devnet tier) in the wallet menu \\u2192 RPC.
'; return; } for (const r of results) { @@ -3875,13 +3911,17 @@ export function chatHtml(): string { main.addEventListener('click', () => openDetail(r.id)); // card body → detail view const sup = document.createElement('span'); sup.className = 'mc-sup'; const priceTxt = fmtPrice(r.price); - sup.textContent = (typeof r.supply === 'number') ? (r.supply + '\\u00d7') : ''; + const engineTxt = r.type === 'plugin' && r.engines && r.engines.length ? r.engines.join(' + ') : ''; + const supplyTxt = (typeof r.supply === 'number') ? (r.supply + '\\u00d7') : ''; + sup.textContent = engineTxt ? (engineTxt + (supplyTxt ? ' · ' + supplyTxt : '')) : supplyTxt; const pr = document.createElement('span'); pr.className = 'mc-price'; if (priceTxt) pr.textContent = priceTxt; // "Free" / "0.1 SOL"; empty when unknown const buy = document.createElement('button'); buy.className = 'mc-buy'; - buy.textContent = owned ? 'Owned' : 'Buy'; buy.disabled = owned; + const isPlugin = r.type === 'plugin'; + buy.textContent = isPlugin ? 'Install later' : owned ? 'Owned' : 'Buy'; buy.disabled = owned || isPlugin; buy.addEventListener('click', (e) => { e.stopPropagation(); // don't trigger the card-body detail open + if (isPlugin) return; buy.disabled = true; buy.textContent = 'Buying…'; vscode.postMessage({ type: 'buySkill', skillId: r.id, creatorWallet: r.creator }); }); diff --git a/surfaces/cli/src/views/SkillMarket.tsx b/surfaces/cli/src/views/SkillMarket.tsx index 3b5bde8..74fefe5 100644 --- a/surfaces/cli/src/views/SkillMarket.tsx +++ b/surfaces/cli/src/views/SkillMarket.tsx @@ -33,6 +33,10 @@ const SOL = 1_000_000_000; function sol(lamports: number | null): string { return lamports == null ? "—" : `${(lamports / SOL).toFixed(3)} SOL`; } +const MARKET_KINDS: MarketItemType[] = ["skill", "workflow", "plugin"]; +function nextMarketKind(kind: MarketItemType): MarketItemType { + return MARKET_KINDS[(MARKET_KINDS.indexOf(kind) + 1) % MARKET_KINDS.length]; +} export function SkillMarket({ api, @@ -48,7 +52,7 @@ export function SkillMarket({ onClose: () => void; }) { const [stage, setStage] = useState("list"); - const [kind, setKind] = useState<"skill" | "workflow">("skill"); + const [kind, setKind] = useState("skill"); const [query, setQuery] = useState(""); const [typing, setTyping] = useState(true); const [results, setResults] = useState([]); @@ -85,7 +89,7 @@ export function SkillMarket({ const clamped = Math.min(idx, Math.max(0, results.length - 1)); const selected = results[clamped]; - async function search(q: string, k: "skill" | "workflow") { + async function search(q: string, k: MarketItemType) { setLoading(true); setError(null); try { @@ -295,7 +299,7 @@ export function SkillMarket({ // ── detail ───────────────────────────────────────────────────────────── if (stage === "detail") { if (key.escape) { setStage("list"); setDetail(null); return; } - if (input === "b" && detail && !owned.has(detail.card.name)) { setStage("confirm"); return; } + if (input === "b" && detail && detail.card.type !== "plugin" && !owned.has(detail.card.name)) { setStage("confirm"); return; } if (input === "c" && detail) { setCommentText(""); setCommentGitLink(""); setCommentField("text"); setStage("comment"); @@ -309,7 +313,7 @@ export function SkillMarket({ if (typing) { if (key.return) { setTyping(false); void search(query, kind); return; } if (key.tab) { - const next = kind === "skill" ? "workflow" : "skill"; + const next = nextMarketKind(kind); setKind(next); void search(query, next); return; } if (key.backspace || key.delete) return setQuery((q) => q.slice(0, -1)); @@ -321,13 +325,13 @@ export function SkillMarket({ if (input === "a") { setStage("agents"); void loadAgents(); return; } if (input === "p") { setPubResult(null); setPubField("name"); setStage("publish"); return; } if (key.tab) { - const next = kind === "skill" ? "workflow" : "skill"; + const next = nextMarketKind(kind); setKind(next); void search(query, next); return; } if (key.upArrow) { if (clamped === 0) return setTyping(true); return setIdx((i) => Math.max(0, i - 1)); } if (key.downArrow) return setIdx((i) => Math.min(results.length - 1, i + 1)); if (key.return && selected) return void openDetail(selected.id); - if (input === "b" && selected && !owned.has(selected.name)) { setDetail(null); setStage("confirm"); } + if (input === "b" && selected && selected.type !== "plugin" && !owned.has(selected.name)) { setDetail(null); setStage("confirm"); } }); // ── agent profile ───────────────────────────────────────────────────────── @@ -501,6 +505,7 @@ export function SkillMarket({ if (stage === "detail" && detail) { const c = detail.card; const isOwned = owned.has(c.name); + const isPlugin = c.type === "plugin"; return ( @@ -508,6 +513,9 @@ export function SkillMarket({ {c.type ?? "skill"} · ×{c.supply ?? 0}{isOwned ? " · owned" : ""} {c.description ? {c.description} : null} + {isPlugin && c.engines?.length ? ( + engines: {c.engines.join(", ")} + ) : null} {c.category || (c.hashtags && c.hashtags.length) ? ( {c.category ? {c.category} : null} @@ -516,6 +524,16 @@ export function SkillMarket({ ))} ) : null} + {isPlugin ? ( + + ── plugin package ── + {c.version ? version {c.version} : null} + {c.iqGitPda ? IQ Git PDA {c.iqGitPda} : null} + {c.capabilities?.length ? capabilities {c.capabilities.join(", ")} : null} + {c.permissions?.length ? permissions {c.permissions.join(", ")} : null} + install/equip is not wired yet + + ) : null} {detail.requiredCards.length ? ( requires: @@ -524,7 +542,7 @@ export function SkillMarket({ ))} ) : null} - {detail.skillText ? ( + {detail.skillText && !isPlugin ? ( ── SKILL.md ── {detail.skillText.slice(0, 1200)} @@ -544,7 +562,7 @@ export function SkillMarket({ {flash ? {glyph.sparkle} {flash} : null} - {isOwned ? "owned · " : "[b] buy · "}[c] comment · [esc] back + {isPlugin ? "plugin install/equip later · " : isOwned ? "owned · " : "[b] buy · "}[c] comment · [esc] back @@ -564,6 +582,8 @@ export function SkillMarket({ · workflows · + plugins + · [a] agents [p] publish {/* search box */} @@ -580,7 +600,7 @@ export function SkillMarket({ ) : error ? ( {error} ) : results.length === 0 ? ( - no {kind === "skill" ? "skills" : "workflows"} found + no {kind === "workflow" ? "workflows" : kind === "plugin" ? "plugins" : "skills"} found ) : ( results.slice(0, 12).map((c, i) => { const on = !typing && i === clamped; @@ -594,6 +614,7 @@ export function SkillMarket({ ×{c.supply ?? 0} + {c.type === "plugin" && c.engines?.length ? {c.engines.join("+")} : null} {isOwned ? owned : null} {(c.description ?? "").slice(0, 40)} @@ -607,8 +628,8 @@ export function SkillMarket({ {typing - ? "type to search · ↵ run · [tab] skills/workflows · ↓ results · esc close" - : "↑/↓ move · ↵ open · [b] buy · [tab] switch · [/] search · [a] agents · [p] publish · esc close"} + ? "type to search · ↵ run · [tab] skills/workflows/plugins · ↓ results · esc close" + : "↑/↓ move · ↵ open · [b] buy non-plugin · [tab] switch · [/] search · [a] agents · [p] publish · esc close"} diff --git a/surfaces/webview/src/market/MarketScreen.tsx b/surfaces/webview/src/market/MarketScreen.tsx index 8a31e9a..5c43dc8 100644 --- a/surfaces/webview/src/market/MarketScreen.tsx +++ b/surfaces/webview/src/market/MarketScreen.tsx @@ -7,6 +7,7 @@ import { AgentDirectory } from "./AgentDirectory"; import { AgentProfileView } from "./AgentProfileView"; import { BuyCelebration } from "./BuyCelebration"; import type { SkillCard } from "../transport/protocol"; +import type { MarketItemType } from "@iqlabs-official/agent-sdk"; type MarketView = "browse" | "publish" | "helius" | "agents"; @@ -27,12 +28,12 @@ export function MarketScreen() { if (state.agentProfile) setView("agents"); }, [state.agentProfile]); - function runSearch(q: string, tab?: "skill" | "workflow") { + function runSearch(q: string, tab?: MarketItemType) { marketSearching(); send({ type: "searchSkills", query: q, kind: tab ?? state.marketTab }); } - function handleTabChange(tab: "skill" | "workflow") { + function handleTabChange(tab: MarketItemType) { setMarketTab(tab); runSearch(state.marketQuery, tab); } @@ -124,7 +125,7 @@ export function MarketScreen() { {/* Tabs */}
- {(["skill", "workflow"] as const).map((tab) => ( + {(["skill", "workflow", "plugin"] as const).map((tab) => ( ))}
- {skillText && ( + {isPlugin && ( +
+

Plugin package

+ {card.version &&

Version {card.version}

} + {card.iqGitPda &&

IQ Git PDA {card.iqGitPda}

} + {card.capabilities?.length ?

Capabilities: {card.capabilities.join(", ")}

: null} + {card.permissions?.length ?

Permissions: {card.permissions.join(", ")}

: null} +
+ )} + + {skillText && !isPlugin && (

SKILL.md

{skillText}
@@ -107,7 +121,7 @@ export function SkillDetailView({ detail, owned, onBack }: Props) { )}
- {!owned && ( + {!owned && !isPlugin && (
)} + {!owned && isPlugin && ( +
+ +
+ )} ); } diff --git a/surfaces/webview/src/state/store.tsx b/surfaces/webview/src/state/store.tsx index 53b20f3..1195582 100644 --- a/surfaces/webview/src/state/store.tsx +++ b/surfaces/webview/src/state/store.tsx @@ -28,6 +28,7 @@ import type { RpcStatus, Reputation, } from "../transport/protocol"; +import type { MarketItemType } from "@iqlabs-official/agent-sdk"; // A rendered log entry. We keep messages as-is and stream into the last assistant/ // thinking bubble when `partial` is set, matching the HTML webview's bubble model. @@ -44,7 +45,7 @@ export interface State { | "chat"; // market overlay (accessible from chat phase via "Markets" button) marketOpen: boolean; - marketTab: "skill" | "workflow"; + marketTab: MarketItemType; marketQuery: string; marketResults: SkillCard[] | null; marketSearching: boolean; @@ -190,7 +191,7 @@ type LocalAction = | { type: "__savePlan"; text: string } | { type: "__openMarket" } | { type: "__closeMarket" } - | { type: "__setMarketTab"; tab: "skill" | "workflow" } + | { type: "__setMarketTab"; tab: MarketItemType } | { type: "__setMarketQuery"; query: string } | { type: "__marketSearching" } | { type: "__clearMarketDetail" } @@ -409,7 +410,7 @@ interface Store { savePlan: (text: string) => void; openMarket: () => void; closeMarket: () => void; - setMarketTab: (tab: "skill" | "workflow") => void; + setMarketTab: (tab: MarketItemType) => void; setMarketQuery: (q: string) => void; marketSearching: () => void; clearMarketDetail: () => void; From 4cc99614acfbb8995777d8910e15f42a1046a5c2 Mon Sep 17 00:00:00 2001 From: mega123-art Date: Sat, 20 Jun 2026 02:41:07 +0530 Subject: [PATCH 4/7] Add marketplace commands and functionality for skills, workflows, and plugins --- packages/core/src/chat/ui/webview.ts | 33 +++++++++++++++++++++++--- surfaces/cli/src/commands.ts | 4 ++++ surfaces/cli/src/views/Chat.tsx | 23 ++++++++++++++---- surfaces/cli/src/views/SkillMarket.tsx | 9 ++++--- surfaces/webview/src/chat/Composer.tsx | 18 +++++++++++++- surfaces/webview/src/state/store.tsx | 7 ++++-- 6 files changed, 81 insertions(+), 13 deletions(-) diff --git a/packages/core/src/chat/ui/webview.ts b/packages/core/src/chat/ui/webview.ts index e47fe1e..d6e5a0e 100644 --- a/packages/core/src/chat/ui/webview.ts +++ b/packages/core/src/chat/ui/webview.ts @@ -1654,6 +1654,10 @@ export function chatHtml(): string { { name: 'model', desc: 'change model', insert: '/model ' }, { name: 'mode', desc: 'change permission mode', insert: '/mode ' }, { name: 'effort', desc: 'set reasoning effort', insert: '/effort ' }, + { name: 'market', desc: 'open marketplace', insert: '/market' }, + { name: 'skills', desc: 'open skills market', insert: '/skills' }, + { name: 'workflows', desc: 'open workflows market', insert: '/workflows' }, + { name: 'plugins', desc: 'open plugins market', insert: '/plugins' }, { name: 'help', desc: 'show help text', insert: '/help' }, ]; let slashIdx = 0; @@ -2836,12 +2840,29 @@ export function chatHtml(): string { case 'effort': if (arg) { effortByCli[cli] = arg; fillEfforts(); vscode.postMessage({ type: 'effort', effort: arg === 'default' ? undefined : arg }); } input.value = ''; return; + case 'market': + openMarket(); + showView('market'); + input.value = ''; return; + case 'skills': + openMarket('skill'); + showView('market'); + input.value = ''; return; + case 'workflows': + openMarket('workflow'); + showView('market'); + input.value = ''; return; + case 'plugins': + openMarket('plugin'); + showView('market'); + input.value = ''; return; case 'help': { const helpText = [ '/new — start fresh session', '/clear — clear on-screen log', '/copy — copy last reply', '/engine claude|codex — switch engine', '/model — change model', '/mode — change permission mode', '/effort low|medium|high|xhigh|max — set reasoning effort', + '/market — open marketplace', '/skills /workflows /plugins — open market tab', ].join('\\n'); const pre = document.createElement('pre'); pre.style.cssText = 'margin:8px 0;padding:8px 12px;background:var(--an-bg-1);border-radius:6px;font-size:0.82em;opacity:0.8'; @@ -3571,7 +3592,14 @@ export function chatHtml(): string { mktResults.innerHTML = '
Searching…
'; vscode.postMessage({ type: 'searchSkills', query: mktSearch.value.trim(), kind: currentKind }); } - function openMarket() { + function selectMarketKind(kind) { + currentKind = kind || 'skill'; + for (const t of document.querySelectorAll('.mktTab')) { + t.classList.toggle('on', t.getAttribute('data-kind') === currentKind); + } + } + function openMarket(kind) { + if (kind) selectMarketKind(kind); showMktList(); // first open (and re-open) loads the popular list (empty query = supply-sorted) mktResults.innerHTML = '
Loading…
'; @@ -3590,8 +3618,7 @@ export function chatHtml(): string { // Skills / Workflows / Plugins tabs — switching re-runs the search filtered to that kind. for (const tab of document.querySelectorAll('.mktTab')) { tab.addEventListener('click', () => { - currentKind = tab.getAttribute('data-kind'); - for (const t of document.querySelectorAll('.mktTab')) t.classList.toggle('on', t === tab); + selectMarketKind(tab.getAttribute('data-kind')); runMarketSearch(); }); } diff --git a/surfaces/cli/src/commands.ts b/surfaces/cli/src/commands.ts index 3bdedff..c655376 100644 --- a/surfaces/cli/src/commands.ts +++ b/surfaces/cli/src/commands.ts @@ -23,6 +23,10 @@ export const SLASH_COMMANDS: SlashCmd[] = [ { name: "btw", desc: "side-channel question without interrupting session", args: "" }, { name: "wallet", desc: "show wallet address" }, { name: "storage", desc: "show where sessions save" }, + { name: "market", desc: "open marketplace" }, + { name: "skills", desc: "open skills market" }, + { name: "workflows", desc: "open workflows market" }, + { name: "plugins", desc: "open plugins market" }, { name: "iq", desc: "a random IQ fact" }, { name: "dance", desc: "Iggy dances" }, { name: "help", desc: "list commands" }, diff --git a/surfaces/cli/src/views/Chat.tsx b/surfaces/cli/src/views/Chat.tsx index fbac5fb..8ede82b 100644 --- a/surfaces/cli/src/views/Chat.tsx +++ b/surfaces/cli/src/views/Chat.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from "react"; import { Box, Text, Static, useApp, useInput } from "ink"; import type { AgentRuntime, Wallet, ChatMessage } from "@iqlabs-official/agent-sdk/runtime/contract"; -import type { CliReport } from "@iqlabs-official/agent-sdk"; +import type { CliReport, MarketItemType } from "@iqlabs-official/agent-sdk"; import type { ApprovalRequest } from "@iqlabs-official/agent-sdk/runtime/approval/channel"; import type { AppOptions } from "../app.js"; import type { InkApprovalChannel } from "../InkApprovalChannel.js"; @@ -177,6 +177,7 @@ export function Chat({ const [replyText, setReplyText] = useState(""); const [showSessions, setShowSessions] = useState(false); const [showMarket, setShowMarket] = useState(false); + const [marketKind, setMarketKind] = useState("skill"); const [showModels, setShowModels] = useState(false); const [showEfforts, setShowEfforts] = useState(false); const [showAccount, setShowAccount] = useState(false); @@ -408,12 +409,13 @@ export function Chat({ setPanelFocused(false); } - function openMarket() { + function openMarket(kind: MarketItemType = "skill") { setPanelFocused(false); if (!market) { - setNotice("skill market still loading, try again in a moment"); + setNotice("marketplace still loading, try again in a moment"); return; } + setMarketKind(kind); setShowMarket(true); } @@ -541,6 +543,18 @@ export function Chat({ setNotice(info ? `storage: ${info.kind}${info.account ? ` (${info.account})` : ""}` : "storage: local only"), ); return; + case "market": + openMarket(); + return; + case "skills": + openMarket("skill"); + return; + case "workflows": + openMarket("workflow"); + return; + case "plugins": + openMarket("plugin"); + return; case "account": void (async () => { const lines: string[] = []; @@ -571,7 +585,7 @@ export function Chat({ startBtwQuery(arg.trim()); return; case "help": - setNotice("/new /sessions /resume /more /compact /clear /copy /models /engine /effort /account /settings /wallet /storage /btw /iq /quit · !cmd shell · Esc cancels · Ctrl+A/E/W/U edit"); + setNotice("/new /sessions /resume /more /compact /clear /copy /models /engine /effort /account /settings /wallet /storage /market /skills /workflows /plugins /btw /iq /quit · !cmd shell · Esc cancels · Ctrl+A/E/W/U edit"); return; default: setNotice(`unknown command: /${cmd} (try /help)`); @@ -708,6 +722,7 @@ export function Chat({ return ( { diff --git a/surfaces/cli/src/views/SkillMarket.tsx b/surfaces/cli/src/views/SkillMarket.tsx index 74fefe5..36654ed 100644 --- a/surfaces/cli/src/views/SkillMarket.tsx +++ b/surfaces/cli/src/views/SkillMarket.tsx @@ -40,19 +40,21 @@ function nextMarketKind(kind: MarketItemType): MarketItemType { export function SkillMarket({ api, + initialKind = "skill", walletAddr, ownedNames, onBought, onClose, }: { api: MarketApi; + initialKind?: MarketItemType; walletAddr: string; ownedNames: string[]; onBought: () => void; onClose: () => void; }) { const [stage, setStage] = useState("list"); - const [kind, setKind] = useState("skill"); + const [kind, setKind] = useState(initialKind); const [query, setQuery] = useState(""); const [typing, setTyping] = useState(true); const [results, setResults] = useState([]); @@ -104,10 +106,11 @@ export function SkillMarket({ } useEffect(() => { - void search("", kind); + setKind(initialKind); + void search("", initialKind); void api.solBalance().then(setBalance).catch(() => setBalance(null)); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [initialKind]); async function openDetail(mint: string) { setLoading(true); diff --git a/surfaces/webview/src/chat/Composer.tsx b/surfaces/webview/src/chat/Composer.tsx index 058ddcc..5d98bce 100644 --- a/surfaces/webview/src/chat/Composer.tsx +++ b/surfaces/webview/src/chat/Composer.tsx @@ -44,12 +44,16 @@ const SLASH_CMDS = [ { name: "model", desc: "change model" }, { name: "mode", desc: "change permission mode" }, { name: "effort", desc: "set reasoning effort" }, + { name: "market", desc: "open marketplace" }, + { name: "skills", desc: "open skills market" }, + { name: "workflows", desc: "open workflows market" }, + { name: "plugins", desc: "open plugins market" }, { name: "help", desc: "list commands" }, ]; // Input + engine tabs + model/effort pickers. FROZEN while an approval is pending. export function Composer() { - const { state, send, selectEngine, queueCount } = useStore(); + const { state, send, selectEngine, openMarket, queueCount } = useStore(); const [text, setText] = useState(""); const [effort, setEffort] = useState("default"); const mode = state.modeByCli[state.cli] ?? MODES[state.cli][0].value; @@ -222,6 +226,18 @@ export function Composer() { case "effort": if (arg) { setEffort(arg); send({ type: "effort", effort: arg === "default" ? undefined : arg }); } setText(""); return; + case "market": + openMarket(); + setText(""); return; + case "skills": + openMarket("skill"); + setText(""); return; + case "workflows": + openMarket("workflow"); + setText(""); return; + case "plugins": + openMarket("plugin"); + setText(""); return; case "help": { const lines = SLASH_CMDS .map((c) => `/${c.name} — ${c.desc}`).join("\n"); diff --git a/surfaces/webview/src/state/store.tsx b/surfaces/webview/src/state/store.tsx index 1195582..e334ae4 100644 --- a/surfaces/webview/src/state/store.tsx +++ b/surfaces/webview/src/state/store.tsx @@ -408,7 +408,7 @@ interface Store { selectEngine: (cli: Cli) => void; finishStorage: () => void; savePlan: (text: string) => void; - openMarket: () => void; + openMarket: (tab?: MarketItemType) => void; closeMarket: () => void; setMarketTab: (tab: MarketItemType) => void; setMarketQuery: (q: string) => void; @@ -526,7 +526,10 @@ export function StoreProvider({ children }: { children: ReactNode }) { selectEngine, finishStorage: () => raw({ type: "__finishStorage" }), savePlan: (text) => raw({ type: "__savePlan", text }), - openMarket: () => raw({ type: "__openMarket" }), + openMarket: (tab) => { + if (tab) raw({ type: "__setMarketTab", tab }); + raw({ type: "__openMarket" }); + }, closeMarket: () => raw({ type: "__closeMarket" }), setMarketTab: (tab) => raw({ type: "__setMarketTab", tab }), setMarketQuery: (query) => raw({ type: "__setMarketQuery", query }), From 103a51d15435da80364afed2eaac4f81889c157e Mon Sep 17 00:00:00 2001 From: mega123-art Date: Sat, 20 Jun 2026 02:42:33 +0530 Subject: [PATCH 5/7] Update marketplace terminology in webview and SkillMarket components --- packages/core/src/chat/ui/webview.ts | 8 ++++---- surfaces/cli/src/views/SkillMarket.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/chat/ui/webview.ts b/packages/core/src/chat/ui/webview.ts index d6e5a0e..b96de50 100644 --- a/packages/core/src/chat/ui/webview.ts +++ b/packages/core/src/chat/ui/webview.ts @@ -1472,20 +1472,20 @@ export function chatHtml(): string { -