@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "2.9.4-canary.0",
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.8",
62
- "@opengeni/codex": "^0.2.22-canary.8",
63
- "@opengeni/config": "^1.1.2-canary.0",
64
- "@opengeni/contracts": "^3.0.2-canary.0",
65
- "@opengeni/db": "^4.3.3-canary.0",
66
- "@opengeni/documents": "^0.8.27-canary.0",
67
- "@opengeni/events": "^0.4.25-canary.0",
68
- "@opengeni/network": "^0.3.1-canary.8",
69
- "@opengeni/observability": "^0.8.26-canary.0",
70
- "@opengeni/runtime": "^2.5.3-canary.0",
71
- "@opengeni/storage": "^0.2.127-canary.0",
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
- if (!cursor) {
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 Error(`${label} is temporarily unavailable because GitHub limited the request`);
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
- const sourceUrl =
107
- sourcePath === "."
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]!) ||