From 5494e0c7c84180608f9aca59965b9aaa9cba0389 Mon Sep 17 00:00:00 2001 From: Ashish Soni Date: Wed, 17 Jun 2026 01:17:15 +0530 Subject: [PATCH] feat: add support for adding agent skills from private github repos --- .opencode/package-lock.json | 86 +++++++++++++++++++ apps/app/src/app/lib/openwork-server.ts | 5 +- apps/app/src/app/types.ts | 1 + .../domains/settings/pages/skills-view.tsx | 22 ++++- .../settings/state/extensions-store.ts | 7 +- apps/server/src/server.ts | 3 + apps/server/src/skill-hub.ts | 46 +++++----- 7 files changed, 144 insertions(+), 26 deletions(-) diff --git a/.opencode/package-lock.json b/.opencode/package-lock.json index 17e074d6b7..02364517f5 100644 --- a/.opencode/package-lock.json +++ b/.opencode/package-lock.json @@ -5,9 +5,95 @@ "packages": { "": { "dependencies": { + "@kilocode/plugin": "7.3.46", "@opencode-ai/plugin": "1.17.3" } }, + "node_modules/@kilocode/plugin": { + "version": "7.3.46", + "resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.3.46.tgz", + "integrity": "sha512-iVFgtwtw0gSNYt+h1WPydE84R+SbOvlwoUnyVenUUbXimbl/Afz3QvrUCOqMdr7RPZsdSU5+WyoUyt6ozQw20w==", + "license": "MIT", + "dependencies": { + "@kilocode/sdk": "7.3.46", + "effect": "4.0.0-beta.65", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.2.11", + "@opentui/keymap": ">=0.2.11", + "@opentui/solid": ">=0.2.11" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/keymap": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@kilocode/plugin/node_modules/effect": { + "version": "4.0.0-beta.65", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.65.tgz", + "integrity": "sha512-QYKvQPAj3CmtsvWkHQww15wX4KG2gNsszDWEcOO5sZCMknp66u6Si/Opmt3wwWCwsyvRmDAdIg+JIz5qzbbFIw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.6.0", + "find-my-way-ts": "^0.1.6", + "ini": "^6.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^1.11.9", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^13.0.0", + "yaml": "^2.8.3" + } + }, + "node_modules/@kilocode/plugin/node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@kilocode/plugin/node_modules/msgpackr": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/@kilocode/plugin/node_modules/uuid": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", + "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/@kilocode/sdk": { + "version": "7.3.46", + "resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.3.46.tgz", + "integrity": "sha512-9ok4KXOugz0b8xpHmxDdrRXJl404BpYdZQTULR82vN3/QNWOaHX/yG9xBDCc+3K2lScT6TCEZjGykQrTn/CuXQ==", + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", diff --git a/apps/app/src/app/lib/openwork-server.ts b/apps/app/src/app/lib/openwork-server.ts index 5ab36f7d40..30ce40aa99 100644 --- a/apps/app/src/app/lib/openwork-server.ts +++ b/apps/app/src/app/lib/openwork-server.ts @@ -155,6 +155,7 @@ export type OpenworkHubRepo = { owner?: string; repo?: string; ref?: string; + token?: string; }; export type OpenworkWorkspaceFileContent = { @@ -1420,9 +1421,11 @@ export function createOpenworkServerClient(options: { baseUrl: string; token?: s const owner = options?.repo?.owner?.trim(); const repo = options?.repo?.repo?.trim(); const ref = options?.repo?.ref?.trim(); + const repoToken = options?.repo?.token?.trim(); if (owner) params.set("owner", owner); if (repo) params.set("repo", repo); if (ref) params.set("ref", ref); + if (repoToken) params.set("token", repoToken); const query = params.size ? `?${params.toString()}` : ""; return requestJson<{ items: OpenworkHubSkillItem[] }>(baseUrl, `/hub/skills${query}`, { token, @@ -1432,7 +1435,7 @@ export function createOpenworkServerClient(options: { baseUrl: string; token?: s installHubSkill: ( workspaceId: string, name: string, - options?: { overwrite?: boolean; repo?: { owner?: string; repo?: string; ref?: string } }, + options?: { overwrite?: boolean; repo?: { owner?: string; repo?: string; ref?: string; token?: string } }, ) => requestJson<{ ok: boolean; name: string; path: string; action: "added" | "updated"; written: number; skipped: number }>( baseUrl, diff --git a/apps/app/src/app/types.ts b/apps/app/src/app/types.ts index d89b8a2eef..4cbaedba87 100644 --- a/apps/app/src/app/types.ts +++ b/apps/app/src/app/types.ts @@ -287,6 +287,7 @@ export type HubSkillRepo = { owner: string; repo: string; ref: string; + accessToken?: string; }; export type HubSkillCard = { diff --git a/apps/app/src/react-app/domains/settings/pages/skills-view.tsx b/apps/app/src/react-app/domains/settings/pages/skills-view.tsx index 148bdceb8f..28b415aae5 100644 --- a/apps/app/src/react-app/domains/settings/pages/skills-view.tsx +++ b/apps/app/src/react-app/domains/settings/pages/skills-view.tsx @@ -136,6 +136,7 @@ type SkillsViewLocalState = { customRepoOwner: string; customRepoName: string; customRepoRef: string; + customRepoToken: string; customRepoError: string | null; shareTarget: SkillCard | null; cloudSessionNonce: number; @@ -168,6 +169,7 @@ const initialSkillsViewLocalState: SkillsViewLocalState = { customRepoOwner: "", customRepoName: "", customRepoRef: "main", + customRepoToken: "", customRepoError: null, shareTarget: null, cloudSessionNonce: 0, @@ -242,6 +244,7 @@ export function SkillsView(props: SkillsViewProps) { customRepoOwner, customRepoName, customRepoRef, + customRepoToken, customRepoError, shareTarget, cloudSessionNonce, @@ -270,6 +273,7 @@ export function SkillsView(props: SkillsViewProps) { const setCustomRepoOwner = (value: SetStateAction) => setLocal("customRepoOwner", value); const setCustomRepoName = (value: SetStateAction) => setLocal("customRepoName", value); const setCustomRepoRef = (value: SetStateAction) => setLocal("customRepoRef", value); + const setCustomRepoToken = (value: SetStateAction) => setLocal("customRepoToken", value); const setCustomRepoError = (value: SetStateAction) => setLocal("customRepoError", value); const setShareTeamBusy = (value: SetStateAction) => setLocal("shareTeamBusy", value); const setShareTeamError = (value: SetStateAction) => setLocal("shareTeamError", value); @@ -644,6 +648,7 @@ export function SkillsView(props: SkillsViewProps) { setCustomRepoOwner(hubRepo?.owner ?? ""); setCustomRepoName(hubRepo?.repo ?? ""); setCustomRepoRef(hubRepo?.ref || "main"); + setCustomRepoToken(hubRepo?.accessToken ?? ""); setCustomRepoError(null); }, [hubRepo, props.busy]); @@ -656,15 +661,16 @@ export function SkillsView(props: SkillsViewProps) { const owner = customRepoOwner.trim(); const repo = customRepoName.trim(); const ref = customRepoRef.trim() || "main"; + const accessToken = customRepoToken.trim(); if (!owner || !repo) { setCustomRepoError(t("skills.owner_repo_required")); return; } - void Promise.resolve(extensions.addHubRepo({ owner, repo, ref })).then(() => { + void Promise.resolve(extensions.addHubRepo({ owner, repo, ref, accessToken })).then(() => { void extensions.refreshHubSkills({ force: true }); }); closeCustomRepoModal(); - }, [closeCustomRepoModal, customRepoName, customRepoOwner, customRepoRef, extensions]); + }, [closeCustomRepoModal, customRepoName, customRepoOwner, customRepoRef, customRepoToken, extensions]); const isOpenworkInjectedSkill = (skill: SkillCard) => { const normalizedName = skill.name.trim().toLowerCase(); @@ -1307,6 +1313,18 @@ export function SkillsView(props: SkillsViewProps) { /> + + {customRepoError ?
{customRepoError}
: null} diff --git a/apps/app/src/react-app/domains/settings/state/extensions-store.ts b/apps/app/src/react-app/domains/settings/state/extensions-store.ts index 489164951f..1133f16920 100644 --- a/apps/app/src/react-app/domains/settings/state/extensions-store.ts +++ b/apps/app/src/react-app/domains/settings/state/extensions-store.ts @@ -1292,6 +1292,7 @@ export function createExtensionsStore(options: { owner: repo.owner, repo: repo.repo, ref: repo.ref, + token: repo.accessToken, }, }); if (refreshHubSkillsAborted) return; @@ -1314,9 +1315,11 @@ export function createExtensionsStore(options: { return; } + const headers: Record = { Accept: "application/vnd.github+json" }; + if (repo.accessToken) headers["Authorization"] = `Bearer ${repo.accessToken}`; const listingRes = await fetch( `https://api.github.com/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}/contents/skills?ref=${encodeURIComponent(repo.ref)}`, - { headers: { Accept: "application/vnd.github+json" } }, + { headers }, ); if (!listingRes.ok) { throw new Error(`Failed to fetch hub catalog (${listingRes.status})`); @@ -1667,7 +1670,7 @@ export function createExtensionsStore(options: { setStateField("skillsStatus", null); try { - const repoOverride: OpenworkHubRepo = { owner: repo.owner, repo: repo.repo, ref: repo.ref }; + const repoOverride: OpenworkHubRepo = { owner: repo.owner, repo: repo.repo, ref: repo.ref, token: repo.accessToken }; if (!openworkClient || !openworkWorkspaceId) return { ok: false, message: "Hub install requires OpenWork server." }; const result = await openworkClient.installHubSkill(openworkWorkspaceId, trimmed, { repo: repoOverride }); await Promise.all([refreshSkills({ force: true }), refreshHubSkills({ force: true })]); diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 8c5c77b671..f221665967 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -1880,10 +1880,12 @@ function createRoutes( const owner = ctx.url.searchParams.get("owner")?.trim(); const repo = ctx.url.searchParams.get("repo")?.trim(); const ref = ctx.url.searchParams.get("ref")?.trim(); + const token = ctx.url.searchParams.get("token")?.trim(); const items = await listHubSkills({ owner: owner || "different-ai", repo: repo || "openwork-hub", ref: ref || "main", + token, }); return jsonResponse({ items }); }); @@ -1911,6 +1913,7 @@ function createRoutes( owner: typeof repoPayload.owner === "string" ? repoPayload.owner : undefined, repo: typeof repoPayload.repo === "string" ? repoPayload.repo : undefined, ref: typeof repoPayload.ref === "string" ? repoPayload.ref : undefined, + token: typeof repoPayload.token === "string" ? repoPayload.token : undefined, } : undefined; diff --git a/apps/server/src/skill-hub.ts b/apps/server/src/skill-hub.ts index 9f3c3be104..c7cb2a93f8 100644 --- a/apps/server/src/skill-hub.ts +++ b/apps/server/src/skill-hub.ts @@ -8,7 +8,7 @@ import { exists } from "./utils.js"; import { validateSkillName } from "./validators.js"; import { projectSkillsDir } from "./workspace-files.js"; -type HubRepo = { owner: string; repo: string; ref: string }; +type HubRepo = { owner: string; repo: string; ref: string; token?: string }; const DEFAULT_HUB_REPO: HubRepo = { owner: "different-ai", @@ -31,13 +31,14 @@ function hubRawBase(repo: HubRepo) { return `https://raw.githubusercontent.com/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}/${encodeURIComponent(repo.ref)}`; } -async function fetchJson(url: string): Promise { - const res = await fetch(url, { - headers: { - Accept: "application/vnd.github+json", - "User-Agent": "openwork-server", - }, - }); +async function fetchJson(url: string, token?: string): Promise { + const headers: Record = { + Accept: "application/vnd.github+json", + "User-Agent": "openwork-server", + }; + if (token) headers["Authorization"] = `Bearer ${token}`; + + const res = await fetch(url, { headers }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new ApiError(502, "hub_fetch_failed", `Failed to fetch hub data (${res.status}): ${text || url}`); @@ -45,13 +46,14 @@ async function fetchJson(url: string): Promise { return res.json(); } -async function fetchText(url: string): Promise { - const res = await fetch(url, { - headers: { - Accept: "text/plain", - "User-Agent": "openwork-server", - }, - }); +async function fetchText(url: string, token?: string): Promise { + const headers: Record = { + Accept: "text/plain", + "User-Agent": "openwork-server", + }; + if (token) headers["Authorization"] = `Bearer ${token}`; + + const res = await fetch(url, { headers }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new ApiError(502, "hub_fetch_failed", `Failed to fetch hub data (${res.status}): ${text || url}`); @@ -108,7 +110,7 @@ export async function listHubSkills(repo: HubRepo = DEFAULT_HUB_REPO): Promise entry && typeof entry === "object" && entry.type === "dir" && typeof entry.name === "string") @@ -120,7 +122,7 @@ export async function listHubSkills(repo: HubRepo = DEFAULT_HUB_REPO): Promise entry && entry.type === "blob" && typeof entry.path === "string" && entry.path.startsWith(prefix)) @@ -230,9 +233,10 @@ export async function installHubSkill( } await mkdir(dirname(destPath), { recursive: true }); - const res = await fetch(`${rawBase}/${file.path}`, { - headers: { "User-Agent": "openwork-server" }, - }); + const headers: Record = { "User-Agent": "openwork-server" }; + if (repo.token) headers["Authorization"] = `Bearer ${repo.token}`; + + const res = await fetch(`${rawBase}/${file.path}`, { headers }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new ApiError(502, "hub_fetch_failed", `Failed to fetch hub file (${res.status}): ${text || file.path}`);