@opengeni/core 2.9.4-canary.0 → 2.9.4-canary.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/domain/skill-imports.d.ts +1 -0
- package/dist/index.js +322 -258
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/src/domain/capabilities.ts +4 -21
- package/src/domain/github-skill-source.ts +90 -1
- package/src/domain/skill-imports.ts +38 -2
- package/src/domain/skill-search.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "2.9.4-canary.
|
|
3
|
+
"version": "2.9.4-canary.2",
|
|
4
4
|
"description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -58,17 +58,17 @@
|
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
61
|
-
"@opengeni/capabilities": "^0.3.3-canary.
|
|
62
|
-
"@opengeni/codex": "^0.2.22-canary.
|
|
63
|
-
"@opengeni/config": "^1.1.2-canary.
|
|
64
|
-
"@opengeni/contracts": "^3.0.2-canary.
|
|
65
|
-
"@opengeni/db": "^4.3.3-canary.
|
|
66
|
-
"@opengeni/documents": "^0.8.27-canary.
|
|
67
|
-
"@opengeni/events": "^0.4.25-canary.
|
|
68
|
-
"@opengeni/network": "^0.3.1-canary.
|
|
69
|
-
"@opengeni/observability": "^0.8.26-canary.
|
|
70
|
-
"@opengeni/runtime": "^2.5.3-canary.
|
|
71
|
-
"@opengeni/storage": "^0.2.127-canary.
|
|
61
|
+
"@opengeni/capabilities": "^0.3.3-canary.10",
|
|
62
|
+
"@opengeni/codex": "^0.2.22-canary.10",
|
|
63
|
+
"@opengeni/config": "^1.1.2-canary.2",
|
|
64
|
+
"@opengeni/contracts": "^3.0.2-canary.2",
|
|
65
|
+
"@opengeni/db": "^4.3.3-canary.2",
|
|
66
|
+
"@opengeni/documents": "^0.8.27-canary.2",
|
|
67
|
+
"@opengeni/events": "^0.4.25-canary.2",
|
|
68
|
+
"@opengeni/network": "^0.3.1-canary.10",
|
|
69
|
+
"@opengeni/observability": "^0.8.26-canary.2",
|
|
70
|
+
"@opengeni/runtime": "^2.5.3-canary.2",
|
|
71
|
+
"@opengeni/storage": "^0.2.127-canary.2",
|
|
72
72
|
"hono": "^4.12.18",
|
|
73
73
|
"zod": "^4.2.1"
|
|
74
74
|
},
|
|
@@ -123,7 +123,7 @@ export async function buildCapabilityCatalog(input: {
|
|
|
123
123
|
...configuredMcpCatalogItems(input.settings),
|
|
124
124
|
...providerIntegrationCatalogItems(socialConnections),
|
|
125
125
|
fikenCatalogItem(workspaceConnections.filter(isFikenConnection)),
|
|
126
|
-
...curatedLibrarySkills,
|
|
126
|
+
...curatedLibrarySkills.filter((item) => installedSkillById.has(item.id)),
|
|
127
127
|
...installedSkills
|
|
128
128
|
.filter(
|
|
129
129
|
(skill) =>
|
|
@@ -980,9 +980,6 @@ export async function discoverMcpRegistryCapabilities(input: {
|
|
|
980
980
|
if (!item || seen.has(item.id)) {
|
|
981
981
|
continue;
|
|
982
982
|
}
|
|
983
|
-
if (query && !catalogSearchText(item).includes(query)) {
|
|
984
|
-
continue;
|
|
985
|
-
}
|
|
986
983
|
seen.add(item.id);
|
|
987
984
|
items.push(item);
|
|
988
985
|
if (items.length >= limit) {
|
|
@@ -990,7 +987,9 @@ export async function discoverMcpRegistryCapabilities(input: {
|
|
|
990
987
|
}
|
|
991
988
|
}
|
|
992
989
|
cursor = typeof page.metadata?.nextCursor === "string" ? page.metadata.nextCursor : undefined;
|
|
993
|
-
|
|
990
|
+
// Return usable results promptly rather than fetching more pages merely
|
|
991
|
+
// to fill the requested limit. The registry already applies the search.
|
|
992
|
+
if (items.length > 0 || !cursor) {
|
|
994
993
|
break;
|
|
995
994
|
}
|
|
996
995
|
}
|
|
@@ -1996,22 +1995,6 @@ function validUrl(value: string | undefined): string | null {
|
|
|
1996
1995
|
}
|
|
1997
1996
|
}
|
|
1998
1997
|
|
|
1999
|
-
function catalogSearchText(item: CapabilityCatalogItem): string {
|
|
2000
|
-
return [
|
|
2001
|
-
item.name,
|
|
2002
|
-
item.description,
|
|
2003
|
-
item.category,
|
|
2004
|
-
...item.tags,
|
|
2005
|
-
item.endpointUrl,
|
|
2006
|
-
item.homepageUrl,
|
|
2007
|
-
item.installUrl,
|
|
2008
|
-
JSON.stringify(item.metadata),
|
|
2009
|
-
]
|
|
2010
|
-
.filter(Boolean)
|
|
2011
|
-
.join(" ")
|
|
2012
|
-
.toLowerCase();
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
1998
|
function capabilityInstallationRuntimeReady(
|
|
2016
1999
|
item: CapabilityCatalogItem,
|
|
2017
2000
|
installation: CapabilityInstallation | undefined,
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import { HTTPException } from "hono/http-exception";
|
|
2
3
|
import type { GitHubSkillSourceClient, GitHubSkillTreeEntry } from "./skill-imports";
|
|
3
4
|
import { pinnedFetch, readResponseJsonBounded, readResponseTextBounded } from "@opengeni/network";
|
|
5
|
+
import {
|
|
6
|
+
PORTABLE_SKILL_MAX_FILES,
|
|
7
|
+
PORTABLE_SKILL_MAX_TOTAL_BYTES,
|
|
8
|
+
type SkillLibraryFile,
|
|
9
|
+
} from "@opengeni/runtime/skill-library";
|
|
4
10
|
|
|
5
11
|
const githubApiBase = "https://api.github.com";
|
|
6
12
|
const githubRequestTimeoutMs = 15_000;
|
|
@@ -14,7 +20,32 @@ export function createGitHubSkillSourceClient(
|
|
|
14
20
|
requestJson: GitHubJsonRequest = (path, maxBytes, label) =>
|
|
15
21
|
githubJson(settings, path, maxBytes, label),
|
|
16
22
|
): GitHubSkillSourceClient {
|
|
23
|
+
const readJson = requestJson;
|
|
24
|
+
const cache = new Map<string, { expires: number; payload: unknown }>();
|
|
25
|
+
const pending = new Map<string, Promise<unknown>>();
|
|
26
|
+
requestJson = async (path, maxBytes, label) => {
|
|
27
|
+
const cached = cache.get(path);
|
|
28
|
+
if (cached && cached.expires > Date.now()) return cached.payload;
|
|
29
|
+
const existing = pending.get(path);
|
|
30
|
+
if (existing) return existing;
|
|
31
|
+
const task = readJson(path, maxBytes, label).then((payload) => {
|
|
32
|
+
if (cache.size >= 128) cache.delete(cache.keys().next().value!);
|
|
33
|
+
cache.set(path, {
|
|
34
|
+
payload,
|
|
35
|
+
expires: Date.now() + (path.includes("/commits/") ? 60_000 : 3_600_000),
|
|
36
|
+
});
|
|
37
|
+
return payload;
|
|
38
|
+
});
|
|
39
|
+
pending.set(path, task);
|
|
40
|
+
try {
|
|
41
|
+
return await task;
|
|
42
|
+
} finally {
|
|
43
|
+
pending.delete(path);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
17
46
|
return {
|
|
47
|
+
downloadSnapshot: (owner, repository, slug) =>
|
|
48
|
+
downloadSkillSnapshot(settings, owner, repository, slug),
|
|
18
49
|
resolveCommit: async (owner, repository, ref) => {
|
|
19
50
|
const payload = recordValue(
|
|
20
51
|
await requestJson(
|
|
@@ -91,6 +122,61 @@ export function createGitHubSkillSourceClient(
|
|
|
91
122
|
};
|
|
92
123
|
}
|
|
93
124
|
|
|
125
|
+
async function downloadSkillSnapshot(
|
|
126
|
+
settings: Settings,
|
|
127
|
+
owner: string,
|
|
128
|
+
repository: string,
|
|
129
|
+
slug: string,
|
|
130
|
+
): Promise<readonly SkillLibraryFile[]> {
|
|
131
|
+
const signal = AbortSignal.timeout(githubRequestTimeoutMs);
|
|
132
|
+
const response = await pinnedFetch(
|
|
133
|
+
`https://skills.sh/api/download/${[owner, repository, slug].map(encodeURIComponent).join("/")}`,
|
|
134
|
+
{ headers: { accept: "application/json" }, credentials: "omit", redirect: "manual", signal },
|
|
135
|
+
settings,
|
|
136
|
+
{ label: "Skill download", requireHttpsOutsideLocalTest: true },
|
|
137
|
+
);
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
await response.body?.cancel();
|
|
140
|
+
throw new HTTPException(response.status === 429 ? 429 : 422, {
|
|
141
|
+
message:
|
|
142
|
+
response.status === 429
|
|
143
|
+
? "skills.sh is busy. Try again shortly."
|
|
144
|
+
: response.status === 404
|
|
145
|
+
? "This skill has no downloadable snapshot. Import its GitHub folder URL instead."
|
|
146
|
+
: "Could not download this skill from skills.sh. Try again shortly.",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// JSON escaping may expand the text payload. Artifact validation below applies
|
|
150
|
+
// the stricter limits to decoded files, paths, and total content size.
|
|
151
|
+
const payload = recordValue(
|
|
152
|
+
await readResponseJsonBounded(
|
|
153
|
+
response,
|
|
154
|
+
PORTABLE_SKILL_MAX_TOTAL_BYTES * 6 + 128_000,
|
|
155
|
+
"Skill download",
|
|
156
|
+
{ signal },
|
|
157
|
+
),
|
|
158
|
+
"Skill download",
|
|
159
|
+
);
|
|
160
|
+
if (
|
|
161
|
+
!Array.isArray(payload.files) ||
|
|
162
|
+
payload.files.length === 0 ||
|
|
163
|
+
payload.files.length > PORTABLE_SKILL_MAX_FILES
|
|
164
|
+
) {
|
|
165
|
+
throw new HTTPException(422, { message: "The downloaded skill has an invalid file list" });
|
|
166
|
+
}
|
|
167
|
+
return payload.files.map((file) => {
|
|
168
|
+
if (
|
|
169
|
+
!file ||
|
|
170
|
+
typeof file !== "object" ||
|
|
171
|
+
typeof file.path !== "string" ||
|
|
172
|
+
typeof file.contents !== "string"
|
|
173
|
+
) {
|
|
174
|
+
throw new HTTPException(422, { message: "The downloaded skill contains an invalid file" });
|
|
175
|
+
}
|
|
176
|
+
return { path: file.path, content: file.contents };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
94
180
|
async function githubJson(
|
|
95
181
|
settings: Settings,
|
|
96
182
|
path: string,
|
|
@@ -118,7 +204,10 @@ async function githubJson(
|
|
|
118
204
|
await readResponseTextBounded(response, 8_192, `${label} error`).catch(() => undefined);
|
|
119
205
|
if (response.status === 404) throw new Error(`${label} was not found or is not public`);
|
|
120
206
|
if (response.status === 403 || response.status === 429) {
|
|
121
|
-
throw new
|
|
207
|
+
throw new HTTPException(429, {
|
|
208
|
+
message:
|
|
209
|
+
"GitHub's public request limit has been reached. Skill search is still available; try previewing again after the limit resets.",
|
|
210
|
+
});
|
|
122
211
|
}
|
|
123
212
|
throw new Error(`${label} failed with HTTP ${response.status}`);
|
|
124
213
|
}
|
|
@@ -19,6 +19,11 @@ export type GitHubSkillTreeEntry = Readonly<{
|
|
|
19
19
|
}>;
|
|
20
20
|
|
|
21
21
|
export type GitHubSkillSourceClient = Readonly<{
|
|
22
|
+
downloadSnapshot?(
|
|
23
|
+
owner: string,
|
|
24
|
+
repository: string,
|
|
25
|
+
slug: string,
|
|
26
|
+
): Promise<readonly SkillLibraryFile[]>;
|
|
22
27
|
resolveCommit(owner: string, repository: string, ref: string): Promise<string>;
|
|
23
28
|
listTree(
|
|
24
29
|
owner: string,
|
|
@@ -52,6 +57,10 @@ export async function resolveSkillImport(
|
|
|
52
57
|
client: GitHubSkillSourceClient,
|
|
53
58
|
): Promise<ResolvedSkillImport> {
|
|
54
59
|
const parsed = parseSkillSource(rawUrl);
|
|
60
|
+
if (parsed.source === "skills_sh" && client.downloadSnapshot) {
|
|
61
|
+
const files = await client.downloadSnapshot(parsed.owner, parsed.repository, parsed.skillSlug!);
|
|
62
|
+
return buildResolvedImport(parsed, files, parsed.skillSlug!, null);
|
|
63
|
+
}
|
|
55
64
|
const sourceCommit = await client.resolveCommit(parsed.owner, parsed.repository, parsed.ref);
|
|
56
65
|
if (!gitCommit.test(sourceCommit)) {
|
|
57
66
|
throw new HTTPException(422, { message: "GitHub returned an invalid source commit" });
|
|
@@ -94,6 +103,15 @@ export async function resolveSkillImport(
|
|
|
94
103
|
}
|
|
95
104
|
return { path: relativeSkillPath(entry.path, sourcePath), content };
|
|
96
105
|
});
|
|
106
|
+
return buildResolvedImport(parsed, files, sourcePath, sourceCommit);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildResolvedImport(
|
|
110
|
+
parsed: ParsedSkillSource,
|
|
111
|
+
files: readonly SkillLibraryFile[],
|
|
112
|
+
sourcePath: string,
|
|
113
|
+
commit: string | null,
|
|
114
|
+
): ResolvedSkillImport {
|
|
97
115
|
let artifact;
|
|
98
116
|
try {
|
|
99
117
|
artifact = buildPortableSkillArtifact(files);
|
|
@@ -103,8 +121,25 @@ export async function resolveSkillImport(
|
|
|
103
121
|
});
|
|
104
122
|
}
|
|
105
123
|
const repositoryUrl = `https://github.com/${parsed.owner}/${parsed.repository}`;
|
|
106
|
-
|
|
107
|
-
|
|
124
|
+
// The existing revision field holds our content digest for registry snapshots;
|
|
125
|
+
// snapshots do not provide a Git commit or a repository-relative folder path.
|
|
126
|
+
const sourceCommit = commit ?? artifact.contentSha256;
|
|
127
|
+
if (
|
|
128
|
+
!commit &&
|
|
129
|
+
artifact.name
|
|
130
|
+
.toLowerCase()
|
|
131
|
+
.replace(/[\s_]+/g, "-")
|
|
132
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
133
|
+
.replace(/-+/g, "-")
|
|
134
|
+
.replace(/^-|-$/g, "") !== parsed.skillSlug!.toLowerCase()
|
|
135
|
+
) {
|
|
136
|
+
throw new HTTPException(422, {
|
|
137
|
+
message: "The downloaded skill does not match the requested name",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const sourceUrl = !commit
|
|
141
|
+
? `https://skills.sh/${parsed.owner}/${parsed.repository}/${parsed.skillSlug}`
|
|
142
|
+
: sourcePath === "."
|
|
108
143
|
? `${repositoryUrl}/tree/${sourceCommit}`
|
|
109
144
|
: `${repositoryUrl}/tree/${sourceCommit}/${encodeGitHubPath(sourcePath)}`;
|
|
110
145
|
const warnings: string[] = [];
|
|
@@ -119,6 +154,7 @@ export async function resolveSkillImport(
|
|
|
119
154
|
}
|
|
120
155
|
return {
|
|
121
156
|
preview: {
|
|
157
|
+
markdown: artifact.files.find((file) => file.path === "SKILL.md")?.content,
|
|
122
158
|
source: parsed.source,
|
|
123
159
|
sourceUrl,
|
|
124
160
|
repositoryUrl,
|
|
@@ -173,6 +173,9 @@ function normalizeResponse(
|
|
|
173
173
|
if (!isRecord(skill) || typeof skill.id !== "string" || typeof skill.source !== "string")
|
|
174
174
|
throw invalid();
|
|
175
175
|
const parts = skill.id.split("/");
|
|
176
|
+
// Website-hosted skills cannot use our GitHub import flow. Their presence
|
|
177
|
+
// must not discard the supported results in the same response.
|
|
178
|
+
if (parts.length === 2 && skill.source === parts[0]) continue;
|
|
176
179
|
if (
|
|
177
180
|
parts.length !== 3 ||
|
|
178
181
|
!ownerPattern.test(parts[0]!) ||
|