pi-harness-runtime 1.1.411 → 1.1.413
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/harness/notification-settings.ts +130 -0
- package/harness/skill-remote-source.ts +157 -0
- package/harness/skill-sync.ts +74 -22
- package/harness/skills-startup.ts +229 -0
- package/harness/startup-report.ts +98 -56
- package/harness-startup-status.ts +31 -2
- package/index.ts +125 -31
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/dist/client.d.ts +10 -0
- package/packages/qdrant-skills/dist/client.d.ts.map +1 -1
- package/packages/qdrant-skills/dist/client.js +41 -9
- package/packages/qdrant-skills/dist/client.js.map +1 -1
- package/packages/qdrant-skills/dist/embedder.d.ts +10 -0
- package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -1
- package/packages/qdrant-skills/dist/embedder.js +12 -2
- package/packages/qdrant-skills/dist/embedder.js.map +1 -1
- package/packages/qdrant-skills/dist/index.d.ts +2 -2
- package/packages/qdrant-skills/dist/index.d.ts.map +1 -1
- package/packages/qdrant-skills/dist/index.js +2 -2
- package/packages/qdrant-skills/dist/index.js.map +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/qdrant-skills/src/client.ts +38 -9
- package/packages/qdrant-skills/src/embedder.ts +14 -2
- package/packages/qdrant-skills/src/index.ts +3 -0
- package/packages/qdrant-skills/test/collection-info.test.ts +165 -0
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/dist/index.d.ts +14 -1
- package/packages/skills/dist/index.d.ts.map +1 -1
- package/packages/skills/dist/index.js +3 -1
- package/packages/skills/dist/index.js.map +1 -1
- package/packages/skills/dist/loader.d.ts +209 -0
- package/packages/skills/dist/loader.d.ts.map +1 -1
- package/packages/skills/dist/loader.js +386 -15
- package/packages/skills/dist/loader.js.map +1 -1
- package/packages/skills/dist/remote-source.d.ts +107 -0
- package/packages/skills/dist/remote-source.d.ts.map +1 -0
- package/packages/skills/dist/remote-source.js +79 -0
- package/packages/skills/dist/remote-source.js.map +1 -0
- package/packages/skills/dist/vector-schema.d.ts +58 -0
- package/packages/skills/dist/vector-schema.d.ts.map +1 -0
- package/packages/skills/dist/vector-schema.js +55 -0
- package/packages/skills/dist/vector-schema.js.map +1 -0
- package/packages/skills/package.json +1 -1
- package/packages/skills/src/index.ts +16 -2
- package/packages/skills/src/loader.ts +532 -39
- package/packages/skills/src/remote-source.ts +148 -0
- package/packages/skills/src/vector-schema.ts +72 -0
- package/packages/skills/test/remote-skills.test.ts +292 -0
- package/packages/skills/test/remote-source.test.ts +248 -0
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/sync-to-qdrant.ts +79 -14
|
@@ -10,14 +10,29 @@ import { join } from "node:path";
|
|
|
10
10
|
import type {
|
|
11
11
|
Skill,
|
|
12
12
|
SkillIndex,
|
|
13
|
-
SkillIndexEntry,
|
|
14
13
|
MatchOptions,
|
|
15
14
|
MatchResult,
|
|
16
15
|
LoadOptions,
|
|
17
16
|
LoadedSkill,
|
|
18
|
-
DEFAULT_SKILLS_CONFIG,
|
|
19
17
|
} from "./types.js";
|
|
20
|
-
import { parseSkillFile
|
|
18
|
+
import { parseSkillFile } from "./parser.js";
|
|
19
|
+
import {
|
|
20
|
+
LEGACY_SKILL_COLLECTION,
|
|
21
|
+
LEGACY_SKILL_COLLECTION_DIMENSIONS,
|
|
22
|
+
LEGACY_SKILL_COLLECTION_MODEL,
|
|
23
|
+
SKILL_COLLECTION,
|
|
24
|
+
describeSkillSchemaMismatch,
|
|
25
|
+
type SkillVectorSchema,
|
|
26
|
+
} from "./vector-schema.js";
|
|
27
|
+
import {
|
|
28
|
+
describeRemoteFailure,
|
|
29
|
+
isStale,
|
|
30
|
+
skillFromRemote,
|
|
31
|
+
type RemoteSkill,
|
|
32
|
+
type RemoteSkillSource,
|
|
33
|
+
type RemoteSkillState,
|
|
34
|
+
type RemoteSkillStats,
|
|
35
|
+
} from "./remote-source.js";
|
|
21
36
|
|
|
22
37
|
// --- Qdrant integration (optional) -----------------------------------------
|
|
23
38
|
// Loaded dynamically so skills work without Qdrant configured.
|
|
@@ -31,7 +46,7 @@ export interface QdrantSkillPoint {
|
|
|
31
46
|
text: string;
|
|
32
47
|
}
|
|
33
48
|
|
|
34
|
-
interface QdrantClientLike {
|
|
49
|
+
export interface QdrantClientLike {
|
|
35
50
|
upsert(collection: string, opts: { wait?: boolean; points: unknown[] }): Promise<void>;
|
|
36
51
|
query(collection: string, opts: {
|
|
37
52
|
query?: { text?: string; model?: string };
|
|
@@ -39,14 +54,95 @@ interface QdrantClientLike {
|
|
|
39
54
|
with_payload?: boolean;
|
|
40
55
|
filter?: Record<string, unknown>;
|
|
41
56
|
}): Promise<unknown>;
|
|
57
|
+
/** Read the description of a collection. Optional. */
|
|
58
|
+
getCollection?(name: string): Promise<unknown>;
|
|
42
59
|
}
|
|
43
60
|
|
|
44
|
-
interface QdrantEmbedderLike {
|
|
61
|
+
export interface QdrantEmbedderLike {
|
|
45
62
|
createEmbedding(text: string, apiKey?: string): Promise<{ embedding: number[]; tokens: number }>;
|
|
46
63
|
/** One request for many texts. Absent in an older package build. */
|
|
47
64
|
createEmbeddingsBatch?: (texts: string[], apiKey?: string) => Promise<{ embeddings: number[][]; tokens: number }>;
|
|
48
65
|
}
|
|
49
66
|
|
|
67
|
+
/** What the vector store is in this run. */
|
|
68
|
+
export type QdrantSkillState =
|
|
69
|
+
/** No Qdrant credentials on this machine. */
|
|
70
|
+
| "not-configured"
|
|
71
|
+
/** The cluster is configured but cannot be reached. */
|
|
72
|
+
| "unavailable"
|
|
73
|
+
/** The cluster is reachable but the collection does not exist yet. */
|
|
74
|
+
| "missing"
|
|
75
|
+
/** The collection exists but holds no skill. */
|
|
76
|
+
| "empty"
|
|
77
|
+
/** The collection holds skills. */
|
|
78
|
+
| "populated";
|
|
79
|
+
|
|
80
|
+
/** The state of the skill vector store. */
|
|
81
|
+
export interface QdrantSkillStats {
|
|
82
|
+
/** What the vector store is in this run. */
|
|
83
|
+
state: QdrantSkillState;
|
|
84
|
+
/** The number of skill vectors in the collection. */
|
|
85
|
+
count: number;
|
|
86
|
+
/** The collection the numbers belong to. */
|
|
87
|
+
collection?: string;
|
|
88
|
+
/** The vector size the collection was created with. */
|
|
89
|
+
dimensions?: number;
|
|
90
|
+
/** The distance the collection was created with. */
|
|
91
|
+
distance?: string;
|
|
92
|
+
/** Why the collection could not be read. Never a credential. */
|
|
93
|
+
error?: string;
|
|
94
|
+
/**
|
|
95
|
+
* The collection does not match the schema of the runtime.
|
|
96
|
+
*
|
|
97
|
+
* The message names the real cause, so a user whose vector search finds
|
|
98
|
+
* nothing does not have to guess why.
|
|
99
|
+
*/
|
|
100
|
+
schemaMismatch?: string;
|
|
101
|
+
/**
|
|
102
|
+
* The collection of an earlier installation that still holds the skills.
|
|
103
|
+
*
|
|
104
|
+
* The runtime collection is empty (or does not exist yet) while the
|
|
105
|
+
* legacy collection holds the skills of the previous version. They are
|
|
106
|
+
* not lost - they live in a collection with another name, and one with
|
|
107
|
+
* another vector size.
|
|
108
|
+
*/
|
|
109
|
+
legacy?: LegacyCollectionInfo;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The skills of the legacy collection.
|
|
114
|
+
*/
|
|
115
|
+
export interface LegacyCollectionInfo {
|
|
116
|
+
/** The name of the collection. */
|
|
117
|
+
collection: string;
|
|
118
|
+
/** The number of skill vectors it holds. */
|
|
119
|
+
count: number;
|
|
120
|
+
/** The vector size it was created with. */
|
|
121
|
+
dimensions?: number;
|
|
122
|
+
/** The embedding model it was created with. */
|
|
123
|
+
model?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The information the Qdrant package can report about a collection. */
|
|
127
|
+
interface QdrantCollectionInfo {
|
|
128
|
+
exists: boolean;
|
|
129
|
+
vectorsSize?: number;
|
|
130
|
+
vectorsDistance?: string;
|
|
131
|
+
pointsCount?: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The vector size of the runtime, used when the installed Qdrant package
|
|
136
|
+
* does not report the size of its model.
|
|
137
|
+
*
|
|
138
|
+
* The embedder owns the real value; this is the fallback for an older
|
|
139
|
+
* package build.
|
|
140
|
+
*/
|
|
141
|
+
const RUNTIME_VECTOR_DIMENSIONS = 1536;
|
|
142
|
+
|
|
143
|
+
/** The distance of the runtime skill collection. */
|
|
144
|
+
const RUNTIME_VECTOR_DISTANCE = "Cosine";
|
|
145
|
+
|
|
50
146
|
/**
|
|
51
147
|
* A point of the vector index: the vector and the payload of one skill.
|
|
52
148
|
*/
|
|
@@ -86,6 +182,26 @@ export interface SkillRegistryOptions {
|
|
|
86
182
|
* requests but holds more of them in memory.
|
|
87
183
|
*/
|
|
88
184
|
batchSize?: number;
|
|
185
|
+
/**
|
|
186
|
+
* The Qdrant client.
|
|
187
|
+
*
|
|
188
|
+
* `undefined` (the default) connects to the configured cluster. An object
|
|
189
|
+
* is used as the client - a test uses a double, so the search path can be
|
|
190
|
+
* exercised without a network.
|
|
191
|
+
*/
|
|
192
|
+
qdrantClient?: QdrantClientLike;
|
|
193
|
+
/**
|
|
194
|
+
* The embedder that turns a query into a vector.
|
|
195
|
+
*
|
|
196
|
+
* Only used together with an injected client.
|
|
197
|
+
*/
|
|
198
|
+
embedder?: QdrantEmbedderLike;
|
|
199
|
+
/**
|
|
200
|
+
* The collection of the injected client.
|
|
201
|
+
*
|
|
202
|
+
* Only used together with an injected client.
|
|
203
|
+
*/
|
|
204
|
+
collection?: string;
|
|
89
205
|
}
|
|
90
206
|
|
|
91
207
|
/**
|
|
@@ -93,25 +209,44 @@ export interface SkillRegistryOptions {
|
|
|
93
209
|
*/
|
|
94
210
|
const DEFAULT_INDEX_BATCH_SIZE = 32;
|
|
95
211
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
createQdrantClient: (cfg: unknown) =>
|
|
212
|
+
/** The Qdrant package, as far as the registry needs it. */
|
|
213
|
+
interface QdrantModules {
|
|
214
|
+
createQdrantClient: (cfg: unknown) => QdrantClientLike;
|
|
99
215
|
createCollection: (client: unknown, name: string, dims: number) => Promise<void>;
|
|
100
216
|
createEmbedding: (text: string, apiKey?: string) => Promise<{ embedding: number[]; tokens: number }>;
|
|
217
|
+
/** One request for many texts. Absent in an older package build. */
|
|
101
218
|
createEmbeddingsBatch?: (texts: string[], apiKey?: string) => Promise<{ embeddings: number[][]; tokens: number }>;
|
|
102
|
-
|
|
219
|
+
/** Read the schema and the point count of a collection. */
|
|
220
|
+
getCollectionInfo?: (client: unknown, name: string) => Promise<QdrantCollectionInfo | null>;
|
|
221
|
+
/** The embedding model of the installed package. */
|
|
222
|
+
embeddingModel?: string;
|
|
223
|
+
/** The vector size of the embedding model. */
|
|
224
|
+
embeddingDimensions?: number;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Resolve the Qdrant skill module path (monorepo sibling or npm package) */
|
|
228
|
+
async function resolveQdrantModules(): Promise<QdrantModules | null> {
|
|
103
229
|
// Try the monorepo sibling first (the compiled package, so the module
|
|
104
230
|
// comes from the same build the app ships).
|
|
105
231
|
for (const base of ["../../../packages/qdrant-skills/dist", "../../packages/qdrant-skills/dist"]) {
|
|
106
232
|
try {
|
|
107
233
|
// A dynamic import: this module is ESM, where `require` does not exist.
|
|
108
|
-
const mod = (await import(/* @vite-ignore */ base + "/index.js")) as
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
234
|
+
const mod = (await import(/* @vite-ignore */ base + "/index.js")) as Record<string, unknown>;
|
|
235
|
+
return {
|
|
236
|
+
createQdrantClient: mod.createQdrantClient as QdrantModules["createQdrantClient"],
|
|
237
|
+
createCollection: mod.createCollection as QdrantModules["createCollection"],
|
|
238
|
+
createEmbedding: mod.createEmbedding as QdrantModules["createEmbedding"],
|
|
239
|
+
// Absent in an older package build: the registry falls back to one
|
|
240
|
+
// request per text, still with a bounded concurrency.
|
|
241
|
+
createEmbeddingsBatch: typeof mod.createEmbeddingsBatch === "function"
|
|
242
|
+
? (mod.createEmbeddingsBatch as QdrantModules["createEmbeddingsBatch"])
|
|
243
|
+
: undefined,
|
|
244
|
+
getCollectionInfo: typeof mod.getCollectionInfo === "function"
|
|
245
|
+
? (mod.getCollectionInfo as QdrantModules["getCollectionInfo"])
|
|
246
|
+
: undefined,
|
|
247
|
+
embeddingModel: typeof mod.EMBEDDING_MODEL === "string" ? mod.EMBEDDING_MODEL : undefined,
|
|
248
|
+
embeddingDimensions: typeof mod.EMBEDDING_DIMENSIONS === "number" ? mod.EMBEDDING_DIMENSIONS : undefined,
|
|
113
249
|
};
|
|
114
|
-
return mod as Awaited<ReturnType<typeof resolveQdrantModules>>;
|
|
115
250
|
} catch {
|
|
116
251
|
// Try next path
|
|
117
252
|
}
|
|
@@ -121,21 +256,19 @@ async function resolveQdrantModules(): Promise<{
|
|
|
121
256
|
// The specifier is a variable on purpose: the package is optional and
|
|
122
257
|
// must not be resolved by the type checker.
|
|
123
258
|
const specifier = "@pi-harness/qdrant-skills";
|
|
124
|
-
const mod = (await import(/* @vite-ignore */ specifier)) as
|
|
125
|
-
createQdrantClient: unknown;
|
|
126
|
-
createCollection: unknown;
|
|
127
|
-
createEmbedding: unknown;
|
|
128
|
-
createEmbeddingsBatch?: unknown;
|
|
129
|
-
};
|
|
259
|
+
const mod = (await import(/* @vite-ignore */ specifier)) as Record<string, unknown>;
|
|
130
260
|
return {
|
|
131
|
-
createQdrantClient:
|
|
132
|
-
createCollection: mod.createCollection as
|
|
133
|
-
createEmbedding: mod.createEmbedding as
|
|
134
|
-
// Absent in an older package build: the registry falls back to one
|
|
135
|
-
// request per text, still with a bounded concurrency.
|
|
261
|
+
createQdrantClient: mod.createQdrantClient as QdrantModules["createQdrantClient"],
|
|
262
|
+
createCollection: mod.createCollection as QdrantModules["createCollection"],
|
|
263
|
+
createEmbedding: mod.createEmbedding as QdrantModules["createEmbedding"],
|
|
136
264
|
createEmbeddingsBatch: typeof mod.createEmbeddingsBatch === "function"
|
|
137
|
-
? (mod.createEmbeddingsBatch as
|
|
265
|
+
? (mod.createEmbeddingsBatch as QdrantModules["createEmbeddingsBatch"])
|
|
138
266
|
: undefined,
|
|
267
|
+
getCollectionInfo: typeof mod.getCollectionInfo === "function"
|
|
268
|
+
? (mod.getCollectionInfo as QdrantModules["getCollectionInfo"])
|
|
269
|
+
: undefined,
|
|
270
|
+
embeddingModel: typeof mod.EMBEDDING_MODEL === "string" ? mod.EMBEDDING_MODEL : undefined,
|
|
271
|
+
embeddingDimensions: typeof mod.EMBEDDING_DIMENSIONS === "number" ? mod.EMBEDDING_DIMENSIONS : undefined,
|
|
139
272
|
};
|
|
140
273
|
} catch {
|
|
141
274
|
return null;
|
|
@@ -176,7 +309,10 @@ export function getQdrantConfig(): {
|
|
|
176
309
|
return {
|
|
177
310
|
url,
|
|
178
311
|
apiKey,
|
|
179
|
-
collection:
|
|
312
|
+
// The collection carries the model in its name: a collection built with
|
|
313
|
+
// a different embedding model holds vectors of a different size and
|
|
314
|
+
// cannot be searched with these vectors.
|
|
315
|
+
collection: process.env.QDRANT_COLLECTION || SKILL_COLLECTION,
|
|
180
316
|
};
|
|
181
317
|
}
|
|
182
318
|
|
|
@@ -185,6 +321,29 @@ export function getQdrantConfig(): {
|
|
|
185
321
|
*/
|
|
186
322
|
export class SkillRegistry {
|
|
187
323
|
private skills: Map<string, Skill> = new Map();
|
|
324
|
+
/** The options of the registry. */
|
|
325
|
+
private _options: SkillRegistryOptions;
|
|
326
|
+
/**
|
|
327
|
+
* The skills that only exist in the vector store.
|
|
328
|
+
*
|
|
329
|
+
* They are kept apart from the local skills on purpose: `list()` must
|
|
330
|
+
* report the LOCAL skills, so a client can say how many skills it has on
|
|
331
|
+
* disk and how many it can reach through the vector store.
|
|
332
|
+
*/
|
|
333
|
+
private remoteSkills: Map<string, Skill> = new Map();
|
|
334
|
+
/**
|
|
335
|
+
* The remote skills whose content is COMPLETE.
|
|
336
|
+
*
|
|
337
|
+
* A skill that only carries the truncated body of a vector store payload
|
|
338
|
+
* is not in this set - it is completed on the first load.
|
|
339
|
+
*/
|
|
340
|
+
private _completeRemote: Set<string> = new Set();
|
|
341
|
+
/**
|
|
342
|
+
* The service that holds the server-side skills.
|
|
343
|
+
*
|
|
344
|
+
* `undefined` means "no service is configured".
|
|
345
|
+
*/
|
|
346
|
+
private _remoteSource: RemoteSkillSource | undefined;
|
|
188
347
|
private index: SkillIndex = { entries: [], loadedAt: 0 };
|
|
189
348
|
private loading: Map<string, Promise<Skill>> = new Map();
|
|
190
349
|
|
|
@@ -210,6 +369,7 @@ export class SkillRegistry {
|
|
|
210
369
|
private _qdrantIndexer: SkillVectorIndexer | null = null;
|
|
211
370
|
|
|
212
371
|
constructor(options: SkillRegistryOptions = {}) {
|
|
372
|
+
this._options = options;
|
|
213
373
|
this._indexer = options.indexer;
|
|
214
374
|
this._batchSize = options.batchSize ?? DEFAULT_INDEX_BATCH_SIZE;
|
|
215
375
|
}
|
|
@@ -243,6 +403,21 @@ export class SkillRegistry {
|
|
|
243
403
|
}
|
|
244
404
|
|
|
245
405
|
private async _doQdrantInit(): Promise<void> {
|
|
406
|
+
// An injected client replaces the whole connection: a test exercises the
|
|
407
|
+
// search path without a network and without credentials.
|
|
408
|
+
const injected = this._options.qdrantClient;
|
|
409
|
+
if (injected) {
|
|
410
|
+
this._qdrant = {
|
|
411
|
+
client: injected,
|
|
412
|
+
embedder: this._options.embedder ?? {
|
|
413
|
+
createEmbedding: async () => ({ embedding: [], tokens: 0 }),
|
|
414
|
+
},
|
|
415
|
+
collection: this._options.collection ?? "pi-harness-skills-test",
|
|
416
|
+
ready: true,
|
|
417
|
+
};
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
246
421
|
const config = getQdrantConfig();
|
|
247
422
|
if (!config) {
|
|
248
423
|
this._qdrantInitFailed = true;
|
|
@@ -261,7 +436,11 @@ export class SkillRegistry {
|
|
|
261
436
|
url: config.url,
|
|
262
437
|
apiKey: config.apiKey,
|
|
263
438
|
});
|
|
264
|
-
|
|
439
|
+
// The collection must hold vectors of the size the embedder produces.
|
|
440
|
+
// An existing collection with a different size is NOT recreated here:
|
|
441
|
+
// that would destroy the skills of another model. The mismatch is
|
|
442
|
+
// reported instead (see getQdrantStats).
|
|
443
|
+
await mods.createCollection(client, config.collection, this._expectedVectorSchema(mods).dimensions);
|
|
265
444
|
this._qdrant = {
|
|
266
445
|
client,
|
|
267
446
|
embedder: {
|
|
@@ -277,6 +456,161 @@ export class SkillRegistry {
|
|
|
277
456
|
}
|
|
278
457
|
}
|
|
279
458
|
|
|
459
|
+
/**
|
|
460
|
+
* The skills of the legacy collection.
|
|
461
|
+
*
|
|
462
|
+
* The legacy collection holds 384-dimensional vectors, so it cannot be
|
|
463
|
+
* searched with the vectors of the runtime. Its skills are not lost -
|
|
464
|
+
* they live in a collection with another name.
|
|
465
|
+
*/
|
|
466
|
+
private async _readLegacyCollection(
|
|
467
|
+
client: QdrantClientLike,
|
|
468
|
+
mods: QdrantModules,
|
|
469
|
+
): Promise<LegacyCollectionInfo | undefined> {
|
|
470
|
+
// The legacy collection IS the runtime collection when the user
|
|
471
|
+
// configured it explicitly - then there is no second collection.
|
|
472
|
+
if (this._options.collection === LEGACY_SKILL_COLLECTION) return undefined;
|
|
473
|
+
if (!mods.getCollectionInfo) return undefined;
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
const info = await mods.getCollectionInfo(client, LEGACY_SKILL_COLLECTION);
|
|
477
|
+
if (!info?.exists) return undefined;
|
|
478
|
+
const count = info.pointsCount ?? 0;
|
|
479
|
+
if (count === 0) return undefined;
|
|
480
|
+
return {
|
|
481
|
+
collection: LEGACY_SKILL_COLLECTION,
|
|
482
|
+
count,
|
|
483
|
+
dimensions: info.vectorsSize,
|
|
484
|
+
model:
|
|
485
|
+
info.vectorsSize === LEGACY_SKILL_COLLECTION_DIMENSIONS
|
|
486
|
+
? LEGACY_SKILL_COLLECTION_MODEL
|
|
487
|
+
: undefined,
|
|
488
|
+
};
|
|
489
|
+
} catch {
|
|
490
|
+
// The legacy collection is a bonus - a failure to read it must not
|
|
491
|
+
// hide the state of the runtime collection.
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The schema the runtime expects of the skill collection.
|
|
498
|
+
*
|
|
499
|
+
* The installed Qdrant package knows the size of its own model; an older
|
|
500
|
+
* build does not, and the fallback is used then.
|
|
501
|
+
*/
|
|
502
|
+
private _expectedVectorSchema(mods: QdrantModules): SkillVectorSchema {
|
|
503
|
+
return {
|
|
504
|
+
dimensions: mods.embeddingDimensions ?? RUNTIME_VECTOR_DIMENSIONS,
|
|
505
|
+
distance: RUNTIME_VECTOR_DISTANCE,
|
|
506
|
+
model: mods.embeddingModel,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* The state of the skill vector store.
|
|
512
|
+
*
|
|
513
|
+
* The numbers are what the startup summary shows next to the local skill
|
|
514
|
+
* count, and the state says WHY there is no number: no credentials, an
|
|
515
|
+
* unreachable cluster, a collection that does not exist yet.
|
|
516
|
+
*
|
|
517
|
+
* The collection schema is checked here as well: a collection built with a
|
|
518
|
+
* different embedding model holds vectors the runtime cannot search with,
|
|
519
|
+
* and the reason has to be named - otherwise the vector search simply
|
|
520
|
+
* finds nothing.
|
|
521
|
+
*/
|
|
522
|
+
async getQdrantStats(): Promise<QdrantSkillStats> {
|
|
523
|
+
const injected = this._options.qdrantClient;
|
|
524
|
+
const config = injected
|
|
525
|
+
? {
|
|
526
|
+
url: "",
|
|
527
|
+
apiKey: "",
|
|
528
|
+
collection: this._options.collection ?? "pi-harness-skills-test",
|
|
529
|
+
}
|
|
530
|
+
: getQdrantConfig();
|
|
531
|
+
if (!config) return { state: "not-configured", count: 0 };
|
|
532
|
+
|
|
533
|
+
const mods = await resolveQdrantModules();
|
|
534
|
+
if (!mods) {
|
|
535
|
+
return {
|
|
536
|
+
state: "unavailable",
|
|
537
|
+
count: 0,
|
|
538
|
+
collection: config.collection,
|
|
539
|
+
error: "the Qdrant package is not available",
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
if (!mods.getCollectionInfo) {
|
|
543
|
+
return {
|
|
544
|
+
state: "unavailable",
|
|
545
|
+
count: 0,
|
|
546
|
+
collection: config.collection,
|
|
547
|
+
error: "the installed Qdrant package cannot read a collection",
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
const client = injected ?? mods.createQdrantClient({ url: config.url, apiKey: config.apiKey });
|
|
553
|
+
const info = await mods.getCollectionInfo(client, config.collection);
|
|
554
|
+
if (!info) {
|
|
555
|
+
return {
|
|
556
|
+
state: "unavailable",
|
|
557
|
+
count: 0,
|
|
558
|
+
collection: config.collection,
|
|
559
|
+
error: "the collection could not be read",
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
if (!info.exists) {
|
|
563
|
+
// The runtime collection does not exist (yet). An earlier
|
|
564
|
+
// installation may have left the skills in the legacy collection -
|
|
565
|
+
// naming them is more useful than an empty count.
|
|
566
|
+
return {
|
|
567
|
+
state: "missing",
|
|
568
|
+
count: 0,
|
|
569
|
+
collection: config.collection,
|
|
570
|
+
legacy: await this._readLegacyCollection(client, mods),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const count = info.pointsCount ?? 0;
|
|
575
|
+
const stats: QdrantSkillStats = {
|
|
576
|
+
state: count > 0 ? "populated" : "empty",
|
|
577
|
+
count,
|
|
578
|
+
collection: config.collection,
|
|
579
|
+
dimensions: info.vectorsSize,
|
|
580
|
+
distance: info.vectorsDistance,
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// An empty runtime collection can still have its skills in the
|
|
584
|
+
// legacy one - the same information, one collection further down.
|
|
585
|
+
if (count === 0) {
|
|
586
|
+
stats.legacy = await this._readLegacyCollection(client, mods);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const mismatch = describeSkillSchemaMismatch(
|
|
590
|
+
{ dimensions: info.vectorsSize, distance: info.vectorsDistance },
|
|
591
|
+
this._expectedVectorSchema(mods),
|
|
592
|
+
);
|
|
593
|
+
if (mismatch) {
|
|
594
|
+
stats.schemaMismatch = mismatch;
|
|
595
|
+
// The legacy collection is a known cause - name it, so the reader
|
|
596
|
+
// does not have to know the history of the collection names.
|
|
597
|
+
if (config.collection === LEGACY_SKILL_COLLECTION) {
|
|
598
|
+
stats.schemaMismatch +=
|
|
599
|
+
` - the collection ${LEGACY_SKILL_COLLECTION} was created by scripts/sync-to-qdrant.ts` +
|
|
600
|
+
` with ${LEGACY_SKILL_COLLECTION_MODEL} (${LEGACY_SKILL_COLLECTION_DIMENSIONS} dimensions)`;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return stats;
|
|
604
|
+
} catch (err) {
|
|
605
|
+
return {
|
|
606
|
+
state: "unavailable",
|
|
607
|
+
count: 0,
|
|
608
|
+
collection: config.collection,
|
|
609
|
+
error: err instanceof Error ? err.message : String(err),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
280
614
|
/** The point of a skill in the vector index. */
|
|
281
615
|
private _toQdrantPoint(skill: Skill): QdrantSkillPoint {
|
|
282
616
|
return {
|
|
@@ -453,7 +787,97 @@ export class SkillRegistry {
|
|
|
453
787
|
* Get a skill by ID
|
|
454
788
|
*/
|
|
455
789
|
get(skillId: string): Skill | undefined {
|
|
456
|
-
|
|
790
|
+
// A server skill is only known after a vector search found it, so it is
|
|
791
|
+
// looked up second - the local skill always wins.
|
|
792
|
+
return this.skills.get(skillId) ?? this.remoteSkills.get(skillId);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* The skills that only exist in the vector store.
|
|
797
|
+
*
|
|
798
|
+
* They are not part of `list()`: the local count must stay local.
|
|
799
|
+
*/
|
|
800
|
+
listRemote(): Skill[] {
|
|
801
|
+
return Array.from(this.remoteSkills.values());
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* The service that holds the server-side skills.
|
|
806
|
+
*
|
|
807
|
+
* `undefined` means "no service is configured". Setting a source clears
|
|
808
|
+
* the cached remote skills: a new service is a new authority, so the old
|
|
809
|
+
* content must not survive it.
|
|
810
|
+
*/
|
|
811
|
+
setRemoteSource(source: RemoteSkillSource | undefined): void {
|
|
812
|
+
this._remoteSource = source;
|
|
813
|
+
this.remoteSkills.clear();
|
|
814
|
+
this._completeRemote.clear();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Ask the service for the skills it holds.
|
|
819
|
+
*
|
|
820
|
+
* A skill whose version changed is replaced by the new one. A record
|
|
821
|
+
* without a version never replaces a cached skill - an unknown version is
|
|
822
|
+
* not evidence of a change.
|
|
823
|
+
*/
|
|
824
|
+
async refreshRemoteSkills(options?: {
|
|
825
|
+
search?: string;
|
|
826
|
+
limit?: number;
|
|
827
|
+
}): Promise<RemoteSkillStats> {
|
|
828
|
+
if (!this._remoteSource) {
|
|
829
|
+
return { state: "not-configured", count: 0 };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
let records: RemoteSkill[];
|
|
833
|
+
try {
|
|
834
|
+
records = await this._remoteSource.list(options);
|
|
835
|
+
} catch (error) {
|
|
836
|
+
return {
|
|
837
|
+
state: this._remoteFailureState(error),
|
|
838
|
+
count: 0,
|
|
839
|
+
error: describeRemoteFailure(error),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
for (const record of records) {
|
|
844
|
+
const cached = this.remoteSkills.get(record.id);
|
|
845
|
+
if (!isStale(cached, record)) continue;
|
|
846
|
+
const skill = skillFromRemote(record);
|
|
847
|
+
this.remoteSkills.set(skill.id, skill);
|
|
848
|
+
if (record.contentComplete) this._completeRemote.add(skill.id);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
state: records.length > 0 ? "populated" : "empty",
|
|
853
|
+
count: records.length,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* The state of the service that holds the server-side skills.
|
|
859
|
+
*
|
|
860
|
+
* The states stay apart: a refused call is not an unreachable service and
|
|
861
|
+
* not an empty service.
|
|
862
|
+
*/
|
|
863
|
+
async getRemoteStats(): Promise<RemoteSkillStats> {
|
|
864
|
+
if (!this._remoteSource) {
|
|
865
|
+
return { state: "not-configured", count: 0 };
|
|
866
|
+
}
|
|
867
|
+
return this.refreshRemoteSkills();
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* The state a failed call stands for.
|
|
872
|
+
*
|
|
873
|
+
* A refusal of the service is "denied" - the service answered, it just
|
|
874
|
+
* did not allow this call. Everything else is a service that could not be
|
|
875
|
+
* reached.
|
|
876
|
+
*/
|
|
877
|
+
private _remoteFailureState(error: unknown): RemoteSkillState {
|
|
878
|
+
const code = (error as { code?: unknown } | null)?.code;
|
|
879
|
+
if (code === "denied" || code === -32001 || code === -32002) return "denied";
|
|
880
|
+
return "unavailable";
|
|
457
881
|
}
|
|
458
882
|
|
|
459
883
|
/**
|
|
@@ -567,10 +991,38 @@ export class SkillRegistry {
|
|
|
567
991
|
|
|
568
992
|
// --- Qdrant vector search --------------------------------------------------
|
|
569
993
|
|
|
994
|
+
/**
|
|
995
|
+
* A skill that only exists in the vector store.
|
|
996
|
+
*
|
|
997
|
+
* The payload carries the name, the description and the body - enough to
|
|
998
|
+
* USE the skill on a machine that has no local copy of it.
|
|
999
|
+
*/
|
|
1000
|
+
private static skillFromPayload(payload: Record<string, unknown>, name: string): Skill | null {
|
|
1001
|
+
const description = typeof payload.description === "string" ? payload.description : "";
|
|
1002
|
+
const body = typeof payload.body === "string" ? payload.body : "";
|
|
1003
|
+
if (!description && !body) return null;
|
|
1004
|
+
return {
|
|
1005
|
+
id: name,
|
|
1006
|
+
// A server skill has no directory on this machine.
|
|
1007
|
+
path: "",
|
|
1008
|
+
frontmatter: { name, description },
|
|
1009
|
+
body,
|
|
1010
|
+
references: [],
|
|
1011
|
+
examples: [],
|
|
1012
|
+
metadata: { name, description },
|
|
1013
|
+
loaded: true,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
570
1017
|
/**
|
|
571
1018
|
* Search skills by semantic similarity using Qdrant.
|
|
572
1019
|
* Falls back to an empty result if Qdrant is not available.
|
|
573
1020
|
*
|
|
1021
|
+
* A hit that belongs to no local skill is a SERVER skill: it is returned
|
|
1022
|
+
* with its payload instead of being dropped. Dropping it would make a
|
|
1023
|
+
* shared skill invisible on every machine that has no local copy of it -
|
|
1024
|
+
* which is exactly the case the vector store exists for.
|
|
1025
|
+
*
|
|
574
1026
|
* @param query Natural-language query
|
|
575
1027
|
* @param limit Max results (default 5)
|
|
576
1028
|
* @param threshold Minimum cosine score (default 0.5)
|
|
@@ -579,7 +1031,7 @@ export class SkillRegistry {
|
|
|
579
1031
|
query: string,
|
|
580
1032
|
limit = 5,
|
|
581
1033
|
threshold = 0.5,
|
|
582
|
-
): Promise<Array<{ skill: Skill; score: number }>> {
|
|
1034
|
+
): Promise<Array<{ skill: Skill; score: number; source: "local" | "remote" }>> {
|
|
583
1035
|
await this._ensureQdrant();
|
|
584
1036
|
if (!this._qdrant?.ready) return [];
|
|
585
1037
|
|
|
@@ -593,12 +1045,21 @@ export class SkillRegistry {
|
|
|
593
1045
|
const raw = await this._qdrant.client.query(this._qdrant.collection, queryOpts) as {
|
|
594
1046
|
points?: Array<{ id: number; score: number; payload?: Record<string, unknown> }> };
|
|
595
1047
|
|
|
596
|
-
const results: Array<{ skill: Skill; score: number }> = [];
|
|
1048
|
+
const results: Array<{ skill: Skill; score: number; source: "local" | "remote" }> = [];
|
|
597
1049
|
for (const pt of raw.points ?? []) {
|
|
598
1050
|
if (pt.score < threshold) continue;
|
|
599
|
-
const
|
|
600
|
-
const
|
|
601
|
-
if (
|
|
1051
|
+
const payload = pt.payload ?? {};
|
|
1052
|
+
const name = typeof payload.name === "string" ? payload.name : undefined;
|
|
1053
|
+
if (!name) continue;
|
|
1054
|
+
const local = this.skills.get(name);
|
|
1055
|
+
if (local) {
|
|
1056
|
+
results.push({ skill: local, score: pt.score, source: "local" });
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
const remote = SkillRegistry.skillFromPayload(payload, name);
|
|
1060
|
+
if (!remote) continue;
|
|
1061
|
+
this.remoteSkills.set(remote.id, remote);
|
|
1062
|
+
results.push({ skill: remote, score: pt.score, source: "remote" });
|
|
602
1063
|
}
|
|
603
1064
|
return results;
|
|
604
1065
|
} catch (err) {
|
|
@@ -648,7 +1109,36 @@ export class SkillRegistry {
|
|
|
648
1109
|
* Load a skill's full content (progressive disclosure)
|
|
649
1110
|
*/
|
|
650
1111
|
async load(skillId: string, options?: LoadOptions): Promise<LoadedSkill | null> {
|
|
651
|
-
|
|
1112
|
+
let skill = this.skills.get(skillId) ?? this.remoteSkills.get(skillId);
|
|
1113
|
+
|
|
1114
|
+
// A skill that is not known yet may still live on the server. The
|
|
1115
|
+
// service decides whether this user may see it - a refusal is not an
|
|
1116
|
+
// error, it is an answer.
|
|
1117
|
+
if (!skill && this._remoteSource) {
|
|
1118
|
+
const record = await this._remoteSource.get(skillId);
|
|
1119
|
+
if (!record) return null;
|
|
1120
|
+
skill = skillFromRemote(record);
|
|
1121
|
+
this.remoteSkills.set(skill.id, skill);
|
|
1122
|
+
if (record.contentComplete) this._completeRemote.add(skill.id);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// A cached remote skill may carry only a truncated body. The service
|
|
1126
|
+
// holds the complete content, so it is fetched once.
|
|
1127
|
+
if (
|
|
1128
|
+
skill &&
|
|
1129
|
+
!this.skills.has(skillId) &&
|
|
1130
|
+
this._remoteSource &&
|
|
1131
|
+
!this._completeRemote.has(skillId)
|
|
1132
|
+
) {
|
|
1133
|
+
const record = await this._remoteSource.get(skillId);
|
|
1134
|
+
if (record) {
|
|
1135
|
+
const complete = skillFromRemote(record);
|
|
1136
|
+
this.remoteSkills.set(complete.id, complete);
|
|
1137
|
+
if (record.contentComplete) this._completeRemote.add(complete.id);
|
|
1138
|
+
skill = complete;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
652
1142
|
if (!skill) {
|
|
653
1143
|
return null;
|
|
654
1144
|
}
|
|
@@ -685,10 +1175,12 @@ export class SkillRegistry {
|
|
|
685
1175
|
options?: LoadOptions
|
|
686
1176
|
): Promise<LoadedSkill> {
|
|
687
1177
|
const loadedSkill: LoadedSkill = { ...skill };
|
|
688
|
-
|
|
1178
|
+
// A server skill has no directory on this machine - its body comes from
|
|
1179
|
+
// the vector store, so nothing is read from the disk.
|
|
1180
|
+
const skillMdPath = skill.path ? join(skill.path, "SKILL.md") : "";
|
|
689
1181
|
|
|
690
1182
|
// Re-parse to get fresh content
|
|
691
|
-
if (existsSync(skillMdPath)) {
|
|
1183
|
+
if (skillMdPath && existsSync(skillMdPath)) {
|
|
692
1184
|
const parsed = parseSkillFile(skillMdPath);
|
|
693
1185
|
loadedSkill.body = parsed.body;
|
|
694
1186
|
loadedSkill.frontmatter = parsed.frontmatter;
|
|
@@ -770,6 +1262,7 @@ export class SkillRegistry {
|
|
|
770
1262
|
*/
|
|
771
1263
|
clear(): void {
|
|
772
1264
|
this.skills.clear();
|
|
1265
|
+
this.remoteSkills.clear();
|
|
773
1266
|
this.index = { entries: [], loadedAt: 0 };
|
|
774
1267
|
this.loading.clear();
|
|
775
1268
|
}
|
|
@@ -785,7 +1278,7 @@ export class SkillRegistry {
|
|
|
785
1278
|
* Check if skill exists
|
|
786
1279
|
*/
|
|
787
1280
|
has(skillId: string): boolean {
|
|
788
|
-
return this.skills.has(skillId);
|
|
1281
|
+
return this.skills.has(skillId) || this.remoteSkills.has(skillId);
|
|
789
1282
|
}
|
|
790
1283
|
}
|
|
791
1284
|
|