@juspay/neurolink 9.84.2 → 9.85.0
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/CHANGELOG.md +6 -0
- package/dist/browser/neurolink.min.js +436 -401
- package/dist/cli/factories/commandFactory.d.ts +17 -0
- package/dist/cli/factories/commandFactory.js +254 -0
- package/dist/cli/parser.js +2 -0
- package/dist/cli/utils/skillsFlags.d.ts +10 -0
- package/dist/cli/utils/skillsFlags.js +22 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/lib/index.d.ts +5 -0
- package/dist/lib/index.js +6 -0
- package/dist/lib/neurolink.d.ts +28 -0
- package/dist/lib/neurolink.js +117 -0
- package/dist/lib/server/routes/agentRoutes.js +156 -1
- package/dist/lib/server/utils/validation.d.ts +32 -0
- package/dist/lib/server/utils/validation.js +18 -0
- package/dist/lib/session/globalSessionState.d.ts +10 -1
- package/dist/lib/session/globalSessionState.js +18 -0
- package/dist/lib/skills/skillMatcher.d.ts +20 -0
- package/dist/lib/skills/skillMatcher.js +80 -0
- package/dist/lib/skills/skillStoreRedis.d.ts +22 -0
- package/dist/lib/skills/skillStoreRedis.js +98 -0
- package/dist/lib/skills/skillStoreS3.d.ts +41 -0
- package/dist/lib/skills/skillStoreS3.js +233 -0
- package/dist/lib/skills/skillStores.d.ts +44 -0
- package/dist/lib/skills/skillStores.js +252 -0
- package/dist/lib/skills/skillTools.d.ts +19 -0
- package/dist/lib/skills/skillTools.js +340 -0
- package/dist/lib/skills/skillsManager.d.ts +54 -0
- package/dist/lib/skills/skillsManager.js +220 -0
- package/dist/lib/types/config.d.ts +10 -0
- package/dist/lib/types/generate.d.ts +8 -0
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/skills.d.ts +296 -0
- package/dist/lib/types/skills.js +17 -0
- package/dist/lib/types/stream.d.ts +8 -0
- package/dist/neurolink.d.ts +28 -0
- package/dist/neurolink.js +117 -0
- package/dist/server/routes/agentRoutes.js +156 -1
- package/dist/server/utils/validation.d.ts +32 -0
- package/dist/server/utils/validation.js +18 -0
- package/dist/session/globalSessionState.d.ts +10 -1
- package/dist/session/globalSessionState.js +18 -0
- package/dist/skills/skillMatcher.d.ts +20 -0
- package/dist/skills/skillMatcher.js +79 -0
- package/dist/skills/skillStoreRedis.d.ts +22 -0
- package/dist/skills/skillStoreRedis.js +97 -0
- package/dist/skills/skillStoreS3.d.ts +41 -0
- package/dist/skills/skillStoreS3.js +232 -0
- package/dist/skills/skillStores.d.ts +44 -0
- package/dist/skills/skillStores.js +251 -0
- package/dist/skills/skillTools.d.ts +19 -0
- package/dist/skills/skillTools.js +339 -0
- package/dist/skills/skillsManager.d.ts +54 -0
- package/dist/skills/skillsManager.js +219 -0
- package/dist/types/config.d.ts +10 -0
- package/dist/types/generate.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/skills.d.ts +296 -0
- package/dist/types/skills.js +16 -0
- package/dist/types/stream.d.ts +8 -0
- package/package.json +2 -1
|
@@ -7,7 +7,34 @@ import { ProviderFactory } from "../../factories/providerFactory.js";
|
|
|
7
7
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
8
8
|
import { tracers } from "../../telemetry/tracers.js";
|
|
9
9
|
import { createStreamRedactor } from "../utils/redaction.js";
|
|
10
|
-
import { AgentExecuteRequestSchema, createErrorResponse as createError, EmbedManyRequestSchema, EmbedRequestSchema, validateRequest, } from "../utils/validation.js";
|
|
10
|
+
import { AgentExecuteRequestSchema, createErrorResponse as createError, EmbedManyRequestSchema, EmbedRequestSchema, SkillCreateRequestSchema, SkillUpdateRequestSchema, validateRequest, } from "../utils/validation.js";
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the skills manager from the server's NeuroLink instance, or a
|
|
13
|
+
* 503 error response when skills are not configured on this server.
|
|
14
|
+
*/
|
|
15
|
+
function resolveSkillsManager(ctx) {
|
|
16
|
+
const manager = ctx.neurolink.getSkillsManager();
|
|
17
|
+
if (!manager) {
|
|
18
|
+
return createError("SKILLS_UNAVAILABLE", "Skills are not enabled on this server — construct NeuroLink with a `skills` config.", undefined, ctx.requestId);
|
|
19
|
+
}
|
|
20
|
+
return manager;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the skills manager for a *mutating* route, or an error response when
|
|
24
|
+
* skills aren't configured (503) or mutations are disabled (allowMutations is
|
|
25
|
+
* not true). The LLM tools are already registration-gated; this applies the
|
|
26
|
+
* same switch to the REST create/update/delete endpoints.
|
|
27
|
+
*/
|
|
28
|
+
function resolveMutableSkillsManager(ctx) {
|
|
29
|
+
const manager = resolveSkillsManager(ctx);
|
|
30
|
+
if ("error" in manager) {
|
|
31
|
+
return manager;
|
|
32
|
+
}
|
|
33
|
+
if (!manager.mutationsAllowed) {
|
|
34
|
+
return createError("SKILLS_MUTATIONS_DISABLED", "Skill mutations are disabled on this server \u2014 construct NeuroLink with `skills.allowMutations: true` to enable create/update/delete.", undefined, ctx.requestId);
|
|
35
|
+
}
|
|
36
|
+
return manager;
|
|
37
|
+
}
|
|
11
38
|
/**
|
|
12
39
|
* Create agent routes
|
|
13
40
|
*/
|
|
@@ -239,6 +266,134 @@ export function createAgentRoutes(basePath = "/api") {
|
|
|
239
266
|
description: "Generate embeddings for multiple texts in a batch",
|
|
240
267
|
tags: ["agent", "embeddings"],
|
|
241
268
|
},
|
|
269
|
+
{
|
|
270
|
+
method: "GET",
|
|
271
|
+
path: `${basePath}/agent/skills`,
|
|
272
|
+
handler: async (ctx) => {
|
|
273
|
+
const manager = resolveSkillsManager(ctx);
|
|
274
|
+
if ("error" in manager) {
|
|
275
|
+
return manager;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const skills = await manager.list(ctx.query.scopeId);
|
|
279
|
+
return { skills, count: skills.length };
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
return createError("EXECUTION_FAILED", error instanceof Error ? error.message : "Skills listing failed", undefined, ctx.requestId);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
description: "List active skills (index only — no instructions). Optional ?scopeId= filter.",
|
|
286
|
+
tags: ["agent", "skills"],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
method: "GET",
|
|
290
|
+
path: `${basePath}/agent/skills/:id`,
|
|
291
|
+
handler: async (ctx) => {
|
|
292
|
+
const manager = resolveSkillsManager(ctx);
|
|
293
|
+
if ("error" in manager) {
|
|
294
|
+
return manager;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const skill = await manager.get(ctx.params.id);
|
|
298
|
+
if (!skill) {
|
|
299
|
+
return createError("NOT_FOUND", `Skill "${ctx.params.id}" not found`, undefined, ctx.requestId);
|
|
300
|
+
}
|
|
301
|
+
return skill;
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
return createError("EXECUTION_FAILED", error instanceof Error ? error.message : "Skill lookup failed", undefined, ctx.requestId);
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
description: "Fetch one skill (by id or exact name) with instructions",
|
|
308
|
+
tags: ["agent", "skills"],
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
method: "POST",
|
|
312
|
+
path: `${basePath}/agent/skills`,
|
|
313
|
+
handler: async (ctx) => {
|
|
314
|
+
const manager = resolveMutableSkillsManager(ctx);
|
|
315
|
+
if ("error" in manager) {
|
|
316
|
+
return manager;
|
|
317
|
+
}
|
|
318
|
+
const validation = validateRequest(SkillCreateRequestSchema, ctx.body, ctx.requestId);
|
|
319
|
+
if (!validation.success) {
|
|
320
|
+
return validation.error;
|
|
321
|
+
}
|
|
322
|
+
const { requestedBy, ...skill } = validation.data;
|
|
323
|
+
try {
|
|
324
|
+
const result = await manager.requestMutation({
|
|
325
|
+
type: "create",
|
|
326
|
+
skill,
|
|
327
|
+
// Authenticated identity wins over caller-supplied attribution.
|
|
328
|
+
...(ctx.user?.id || requestedBy
|
|
329
|
+
? { requestedBy: ctx.user?.id ?? requestedBy }
|
|
330
|
+
: {}),
|
|
331
|
+
});
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
return createError("EXECUTION_FAILED", error instanceof Error ? error.message : "Skill create failed", undefined, ctx.requestId);
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
description: "Create a skill (routed through the host's onMutationRequest gate when configured)",
|
|
339
|
+
tags: ["agent", "skills"],
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
method: "PATCH",
|
|
343
|
+
path: `${basePath}/agent/skills/:id`,
|
|
344
|
+
handler: async (ctx) => {
|
|
345
|
+
const manager = resolveMutableSkillsManager(ctx);
|
|
346
|
+
if ("error" in manager) {
|
|
347
|
+
return manager;
|
|
348
|
+
}
|
|
349
|
+
const validation = validateRequest(SkillUpdateRequestSchema, ctx.body, ctx.requestId);
|
|
350
|
+
if (!validation.success) {
|
|
351
|
+
return validation.error;
|
|
352
|
+
}
|
|
353
|
+
const { requestedBy, ...patch } = validation.data;
|
|
354
|
+
try {
|
|
355
|
+
const result = await manager.requestMutation({
|
|
356
|
+
type: "update",
|
|
357
|
+
skillId: ctx.params.id,
|
|
358
|
+
patch,
|
|
359
|
+
...(ctx.user?.id || requestedBy
|
|
360
|
+
? { requestedBy: ctx.user?.id ?? requestedBy }
|
|
361
|
+
: {}),
|
|
362
|
+
});
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
const message = error instanceof Error ? error.message : "Skill update failed";
|
|
367
|
+
return createError(message.includes("not found") ? "NOT_FOUND" : "EXECUTION_FAILED", message, undefined, ctx.requestId);
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
description: "Update a skill (patch semantics; version is bumped)",
|
|
371
|
+
tags: ["agent", "skills"],
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
method: "DELETE",
|
|
375
|
+
path: `${basePath}/agent/skills/:id`,
|
|
376
|
+
handler: async (ctx) => {
|
|
377
|
+
const manager = resolveMutableSkillsManager(ctx);
|
|
378
|
+
if ("error" in manager) {
|
|
379
|
+
return manager;
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const result = await manager.requestMutation({
|
|
383
|
+
type: "delete",
|
|
384
|
+
skillId: ctx.params.id,
|
|
385
|
+
...(ctx.user?.id ? { requestedBy: ctx.user.id } : {}),
|
|
386
|
+
});
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
const message = error instanceof Error ? error.message : "Skill delete failed";
|
|
391
|
+
return createError(message.includes("not found") ? "NOT_FOUND" : "EXECUTION_FAILED", message, undefined, ctx.requestId);
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
description: "Soft-delete (deprecate) a skill",
|
|
395
|
+
tags: ["agent", "skills"],
|
|
396
|
+
},
|
|
242
397
|
],
|
|
243
398
|
};
|
|
244
399
|
}
|
|
@@ -99,6 +99,38 @@ export declare const EmbedManyRequestSchema: z.ZodObject<{
|
|
|
99
99
|
provider: z.ZodOptional<z.ZodString>;
|
|
100
100
|
model: z.ZodOptional<z.ZodString>;
|
|
101
101
|
}, z.core.$strip>;
|
|
102
|
+
/**
|
|
103
|
+
* Skill create request schema
|
|
104
|
+
*/
|
|
105
|
+
export declare const SkillCreateRequestSchema: z.ZodObject<{
|
|
106
|
+
name: z.ZodString;
|
|
107
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
108
|
+
description: z.ZodString;
|
|
109
|
+
instructions: z.ZodString;
|
|
110
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
111
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
112
|
+
global: "global";
|
|
113
|
+
scoped: "scoped";
|
|
114
|
+
}>>;
|
|
115
|
+
scopeIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
116
|
+
requestedBy: z.ZodOptional<z.ZodString>;
|
|
117
|
+
}, z.core.$strip>;
|
|
118
|
+
/**
|
|
119
|
+
* Skill update request schema — all fields optional (patch semantics)
|
|
120
|
+
*/
|
|
121
|
+
export declare const SkillUpdateRequestSchema: z.ZodObject<{
|
|
122
|
+
name: z.ZodOptional<z.ZodString>;
|
|
123
|
+
displayName: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
124
|
+
description: z.ZodOptional<z.ZodString>;
|
|
125
|
+
instructions: z.ZodOptional<z.ZodString>;
|
|
126
|
+
tags: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
|
|
127
|
+
scope: z.ZodOptional<z.ZodOptional<z.ZodEnum<{
|
|
128
|
+
global: "global";
|
|
129
|
+
scoped: "scoped";
|
|
130
|
+
}>>>;
|
|
131
|
+
scopeIds: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
|
|
132
|
+
requestedBy: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
133
|
+
}, z.core.$strip>;
|
|
102
134
|
/**
|
|
103
135
|
* Type guard to check if a value is an ErrorResponse
|
|
104
136
|
*/
|
|
@@ -127,6 +127,23 @@ export const EmbedManyRequestSchema = z.object({
|
|
|
127
127
|
provider: z.string().optional(),
|
|
128
128
|
model: z.string().optional(),
|
|
129
129
|
});
|
|
130
|
+
/**
|
|
131
|
+
* Skill create request schema
|
|
132
|
+
*/
|
|
133
|
+
export const SkillCreateRequestSchema = z.object({
|
|
134
|
+
name: z.string().min(1, "Name is required"),
|
|
135
|
+
displayName: z.string().optional(),
|
|
136
|
+
description: z.string().min(1, "Description is required"),
|
|
137
|
+
instructions: z.string().min(1, "Instructions are required"),
|
|
138
|
+
tags: z.array(z.string()).optional(),
|
|
139
|
+
scope: z.enum(["global", "scoped"]).optional(),
|
|
140
|
+
scopeIds: z.array(z.string()).optional(),
|
|
141
|
+
requestedBy: z.string().optional(),
|
|
142
|
+
});
|
|
143
|
+
/**
|
|
144
|
+
* Skill update request schema — all fields optional (patch semantics)
|
|
145
|
+
*/
|
|
146
|
+
export const SkillUpdateRequestSchema = SkillCreateRequestSchema.partial();
|
|
130
147
|
// ============================================
|
|
131
148
|
// Error Response Type Guards / Helpers
|
|
132
149
|
// ============================================
|
|
@@ -159,6 +176,7 @@ function getDefaultHttpStatus(code) {
|
|
|
159
176
|
RATE_LIMIT_EXCEEDED: 429,
|
|
160
177
|
MCP_UNAVAILABLE: 503,
|
|
161
178
|
MEMORY_UNAVAILABLE: 503,
|
|
179
|
+
SKILLS_UNAVAILABLE: 503,
|
|
162
180
|
EXECUTION_FAILED: 500,
|
|
163
181
|
INTERNAL_ERROR: 500,
|
|
164
182
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NeuroLink } from "../neurolink.js";
|
|
2
|
-
import type { ClassifierRouterConfig, ConversationMemoryConfig, LoopSessionState, SessionVariableValue, ToolRoutingConfig } from "../types/index.js";
|
|
2
|
+
import type { ClassifierRouterConfig, ConversationMemoryConfig, LoopSessionState, SessionVariableValue, SkillsConfig, ToolRoutingConfig } from "../types/index.js";
|
|
3
3
|
export declare class GlobalSessionManager {
|
|
4
4
|
private static instance;
|
|
5
5
|
private loopSession;
|
|
@@ -7,6 +7,8 @@ export declare class GlobalSessionManager {
|
|
|
7
7
|
private _toolRoutingConfig;
|
|
8
8
|
/** Optional classifier-router config set by CLI handlers before SDK construction. */
|
|
9
9
|
private _classifierRouterConfig;
|
|
10
|
+
/** Optional skills config set by CLI handlers before SDK construction. */
|
|
11
|
+
private _skillsConfig;
|
|
10
12
|
static getInstance(): GlobalSessionManager;
|
|
11
13
|
setLoopSession(config?: ConversationMemoryConfig): string;
|
|
12
14
|
/**
|
|
@@ -51,6 +53,13 @@ export declare class GlobalSessionManager {
|
|
|
51
53
|
* already exists).
|
|
52
54
|
*/
|
|
53
55
|
setClassifierRouterConfig(config: ClassifierRouterConfig): void;
|
|
56
|
+
/**
|
|
57
|
+
* Store a skills config to be injected at SDK construction time.
|
|
58
|
+
* Call this BEFORE `getOrCreateNeuroLink()` inside a command handler.
|
|
59
|
+
* When a loop session is already active the config is ignored (the instance
|
|
60
|
+
* already exists).
|
|
61
|
+
*/
|
|
62
|
+
setSkillsConfig(config: SkillsConfig): void;
|
|
54
63
|
getOrCreateNeuroLink(): NeuroLink;
|
|
55
64
|
getCurrentSessionId(): string | undefined;
|
|
56
65
|
setSessionVariable(key: string, value: SessionVariableValue): void;
|
|
@@ -35,6 +35,8 @@ export class GlobalSessionManager {
|
|
|
35
35
|
_toolRoutingConfig = undefined;
|
|
36
36
|
/** Optional classifier-router config set by CLI handlers before SDK construction. */
|
|
37
37
|
_classifierRouterConfig = undefined;
|
|
38
|
+
/** Optional skills config set by CLI handlers before SDK construction. */
|
|
39
|
+
_skillsConfig = undefined;
|
|
38
40
|
static getInstance() {
|
|
39
41
|
if (!GlobalSessionManager.instance) {
|
|
40
42
|
GlobalSessionManager.instance = new GlobalSessionManager();
|
|
@@ -155,6 +157,18 @@ export class GlobalSessionManager {
|
|
|
155
157
|
}
|
|
156
158
|
this._classifierRouterConfig = config;
|
|
157
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Store a skills config to be injected at SDK construction time.
|
|
162
|
+
* Call this BEFORE `getOrCreateNeuroLink()` inside a command handler.
|
|
163
|
+
* When a loop session is already active the config is ignored (the instance
|
|
164
|
+
* already exists).
|
|
165
|
+
*/
|
|
166
|
+
setSkillsConfig(config) {
|
|
167
|
+
if (this.hasActiveSession()) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this._skillsConfig = config;
|
|
171
|
+
}
|
|
158
172
|
getOrCreateNeuroLink() {
|
|
159
173
|
const session = this.getLoopSession();
|
|
160
174
|
if (session) {
|
|
@@ -178,6 +192,10 @@ export class GlobalSessionManager {
|
|
|
178
192
|
options.classifierRouter = this._classifierRouterConfig;
|
|
179
193
|
this._classifierRouterConfig = undefined;
|
|
180
194
|
}
|
|
195
|
+
if (this._skillsConfig) {
|
|
196
|
+
options.skills = this._skillsConfig;
|
|
197
|
+
this._skillsConfig = undefined;
|
|
198
|
+
}
|
|
181
199
|
return new NeuroLink(Object.keys(options).length ? options : undefined);
|
|
182
200
|
}
|
|
183
201
|
getCurrentSessionId() {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure index-filtering + prompt-index formatting helpers.
|
|
3
|
+
*
|
|
4
|
+
* Matching semantics are ported from curator's battle-tested
|
|
5
|
+
* `searchSkillsFromIndex`: case-insensitive substring match on
|
|
6
|
+
* name/displayName/description, optional tag filter on top, scope filter
|
|
7
|
+
* that always admits global skills, and active-only visibility.
|
|
8
|
+
*/
|
|
9
|
+
import type { SkillDefinition, SkillIndexItem, SkillSearchQuery } from "../types/index.js";
|
|
10
|
+
/** Strip instructions from a full definition to form an index entry. */
|
|
11
|
+
export declare function toSkillIndexItem(skill: SkillDefinition): SkillIndexItem;
|
|
12
|
+
/** Filter index entries by query/tag/scope. Active skills only. */
|
|
13
|
+
export declare function filterSkillIndex(items: SkillIndexItem[], query: SkillSearchQuery): SkillIndexItem[];
|
|
14
|
+
/**
|
|
15
|
+
* Render the compact skills index injected into the system prompt.
|
|
16
|
+
* Names + descriptions only — instructions are never included; the model
|
|
17
|
+
* loads them via search_skills. Returns null when nothing is visible so
|
|
18
|
+
* callers can skip injection entirely.
|
|
19
|
+
*/
|
|
20
|
+
export declare function formatSkillsPromptIndex(items: SkillIndexItem[], maxItems: number): string | null;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure index-filtering + prompt-index formatting helpers.
|
|
3
|
+
*
|
|
4
|
+
* Matching semantics are ported from curator's battle-tested
|
|
5
|
+
* `searchSkillsFromIndex`: case-insensitive substring match on
|
|
6
|
+
* name/displayName/description, optional tag filter on top, scope filter
|
|
7
|
+
* that always admits global skills, and active-only visibility.
|
|
8
|
+
*/
|
|
9
|
+
/** Strip instructions from a full definition to form an index entry. */
|
|
10
|
+
export function toSkillIndexItem(skill) {
|
|
11
|
+
const { instructions: _instructions, ...indexItem } = skill;
|
|
12
|
+
return indexItem;
|
|
13
|
+
}
|
|
14
|
+
/** Filter index entries by query/tag/scope. Active skills only. */
|
|
15
|
+
export function filterSkillIndex(items, query) {
|
|
16
|
+
const lowerQuery = query.query?.toLowerCase();
|
|
17
|
+
const lowerTag = query.tag?.toLowerCase();
|
|
18
|
+
const matched = items.filter((item) => {
|
|
19
|
+
if ((item.status ?? "active") !== "active") {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
// Scope filter: global skills always pass; scoped skills require a
|
|
23
|
+
// matching scopeId. When the caller provides no scopeId, scoped skills
|
|
24
|
+
// still pass (curator semantics — unscoped callers see everything).
|
|
25
|
+
if (query.scopeId && item.scope === "scoped") {
|
|
26
|
+
if (!(item.scopeIds ?? []).includes(query.scopeId)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (lowerQuery) {
|
|
31
|
+
const nameMatch = item.name.toLowerCase().includes(lowerQuery) ||
|
|
32
|
+
(item.displayName ?? "").toLowerCase().includes(lowerQuery) ||
|
|
33
|
+
item.description.toLowerCase().includes(lowerQuery);
|
|
34
|
+
if (!nameMatch) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (lowerTag) {
|
|
39
|
+
const tagMatch = (item.tags ?? []).some((t) => t.toLowerCase().includes(lowerTag));
|
|
40
|
+
if (!tagMatch) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
});
|
|
46
|
+
return query.limit !== undefined ? matched.slice(0, query.limit) : matched;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Render the compact skills index injected into the system prompt.
|
|
50
|
+
* Names + descriptions only — instructions are never included; the model
|
|
51
|
+
* loads them via search_skills. Returns null when nothing is visible so
|
|
52
|
+
* callers can skip injection entirely.
|
|
53
|
+
*/
|
|
54
|
+
export function formatSkillsPromptIndex(items, maxItems) {
|
|
55
|
+
if (items.length === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const visible = items.slice(0, maxItems);
|
|
59
|
+
const lines = visible.map((item) => {
|
|
60
|
+
const label = item.displayName
|
|
61
|
+
? `${item.name} (${item.displayName})`
|
|
62
|
+
: item.name;
|
|
63
|
+
const tags = item.tags && item.tags.length > 0
|
|
64
|
+
? ` [tags: ${item.tags.join(", ")}]`
|
|
65
|
+
: "";
|
|
66
|
+
return `- ${label}: ${item.description}${tags}`;
|
|
67
|
+
});
|
|
68
|
+
const truncationNote = items.length > visible.length
|
|
69
|
+
? `\n(${items.length - visible.length} more skills exist — use search_skills or list_skills to discover them.)`
|
|
70
|
+
: "";
|
|
71
|
+
return ([
|
|
72
|
+
"## Available Skills",
|
|
73
|
+
"The following team-defined skills (SOPs, playbooks, workflows) are available.",
|
|
74
|
+
"Before answering from general knowledge, check whether one applies to the user's request.",
|
|
75
|
+
"To use a skill, call the search_skills tool to load its full instructions, then follow them exactly.",
|
|
76
|
+
"",
|
|
77
|
+
...lines,
|
|
78
|
+
].join("\n") + truncationNote);
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=skillMatcher.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis-backed skill store.
|
|
3
|
+
*
|
|
4
|
+
* Reuses NeuroLink's pooled Redis client (`redis` v5 is already a core
|
|
5
|
+
* dependency, shared with Redis conversation memory). One JSON value per
|
|
6
|
+
* skill under `<keyPrefix><id>`; the index is derived with SCAN + MGET —
|
|
7
|
+
* no separate index document to drift. Skills are persistent (no TTL).
|
|
8
|
+
*/
|
|
9
|
+
import type { SkillDefinition, SkillIndexItem, SkillRedisStorageConfig, SkillStore } from "../types/index.js";
|
|
10
|
+
export declare class RedisSkillStore implements SkillStore {
|
|
11
|
+
private readonly keyPrefix;
|
|
12
|
+
private readonly normalizedConfig;
|
|
13
|
+
private clientPromise;
|
|
14
|
+
constructor(config: SkillRedisStorageConfig);
|
|
15
|
+
private getClient;
|
|
16
|
+
private skillKey;
|
|
17
|
+
get(id: string): Promise<SkillDefinition | null>;
|
|
18
|
+
put(skill: SkillDefinition): Promise<void>;
|
|
19
|
+
delete(id: string): Promise<void>;
|
|
20
|
+
index(): Promise<SkillIndexItem[]>;
|
|
21
|
+
private parseSkill;
|
|
22
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis-backed skill store.
|
|
3
|
+
*
|
|
4
|
+
* Reuses NeuroLink's pooled Redis client (`redis` v5 is already a core
|
|
5
|
+
* dependency, shared with Redis conversation memory). One JSON value per
|
|
6
|
+
* skill under `<keyPrefix><id>`; the index is derived with SCAN + MGET —
|
|
7
|
+
* no separate index document to drift. Skills are persistent (no TTL).
|
|
8
|
+
*/
|
|
9
|
+
import { getNormalizedConfig, getPooledRedisClient, scanKeys, } from "../utils/redis.js";
|
|
10
|
+
import { logger } from "../utils/logger.js";
|
|
11
|
+
import { toSkillIndexItem } from "./skillMatcher.js";
|
|
12
|
+
const DEFAULT_KEY_PREFIX = "neurolink:skills:";
|
|
13
|
+
export class RedisSkillStore {
|
|
14
|
+
keyPrefix;
|
|
15
|
+
normalizedConfig;
|
|
16
|
+
clientPromise = null;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.keyPrefix = config.keyPrefix ?? DEFAULT_KEY_PREFIX;
|
|
19
|
+
this.normalizedConfig = getNormalizedConfig({
|
|
20
|
+
...(config.url ? { url: config.url } : {}),
|
|
21
|
+
...(config.host ? { host: config.host } : {}),
|
|
22
|
+
...(config.port !== undefined ? { port: config.port } : {}),
|
|
23
|
+
...(config.username ? { username: config.username } : {}),
|
|
24
|
+
...(config.password ? { password: config.password } : {}),
|
|
25
|
+
...(config.db !== undefined ? { db: config.db } : {}),
|
|
26
|
+
keyPrefix: this.keyPrefix,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
getClient() {
|
|
30
|
+
if (!this.clientPromise) {
|
|
31
|
+
this.clientPromise = getPooledRedisClient(this.normalizedConfig).catch((error) => {
|
|
32
|
+
// Reset so a later call can retry after a transient outage.
|
|
33
|
+
this.clientPromise = null;
|
|
34
|
+
throw error;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return this.clientPromise;
|
|
38
|
+
}
|
|
39
|
+
skillKey(id) {
|
|
40
|
+
return `${this.keyPrefix}${id}`;
|
|
41
|
+
}
|
|
42
|
+
async get(id) {
|
|
43
|
+
const client = await this.getClient();
|
|
44
|
+
const raw = await client.get(this.skillKey(id));
|
|
45
|
+
return raw ? this.parseSkill(String(raw), this.skillKey(id)) : null;
|
|
46
|
+
}
|
|
47
|
+
async put(skill) {
|
|
48
|
+
const client = await this.getClient();
|
|
49
|
+
// Persistent — deliberately no TTL: skills must not expire silently.
|
|
50
|
+
await client.set(this.skillKey(skill.id), JSON.stringify(skill));
|
|
51
|
+
}
|
|
52
|
+
async delete(id) {
|
|
53
|
+
const client = await this.getClient();
|
|
54
|
+
await client.del(this.skillKey(id));
|
|
55
|
+
}
|
|
56
|
+
async index() {
|
|
57
|
+
const client = await this.getClient();
|
|
58
|
+
const keys = await scanKeys(client, `${this.keyPrefix}*`);
|
|
59
|
+
if (keys.length === 0) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const values = await client.mGet(keys);
|
|
63
|
+
const items = [];
|
|
64
|
+
values.forEach((value, i) => {
|
|
65
|
+
if (!value) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const skill = this.parseSkill(String(value), keys[i]);
|
|
69
|
+
if (skill) {
|
|
70
|
+
items.push(toSkillIndexItem(skill));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return items;
|
|
74
|
+
}
|
|
75
|
+
parseSkill(raw, key) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(raw);
|
|
78
|
+
if (typeof parsed?.id === "string" &&
|
|
79
|
+
typeof parsed?.name === "string" &&
|
|
80
|
+
typeof parsed?.description === "string" &&
|
|
81
|
+
typeof parsed?.instructions === "string") {
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
logger.warn("[SkillStoreRedis] Value is not a valid skill — skipping", {
|
|
85
|
+
key,
|
|
86
|
+
});
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
logger.warn("[SkillStoreRedis] Failed to parse skill value", {
|
|
91
|
+
key,
|
|
92
|
+
error: error instanceof Error ? error.message : String(error),
|
|
93
|
+
});
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=skillStoreRedis.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3-backed skill store.
|
|
3
|
+
*
|
|
4
|
+
* Layout (curator-compatible):
|
|
5
|
+
* <prefix>skills/<id>.json — one JSON-serialized SkillDefinition per object
|
|
6
|
+
* <prefix>index.json — { lastUpdated, skills: SkillIndexItem[] }
|
|
7
|
+
*
|
|
8
|
+
* The index document is upserted on every write and rebuilt from a bucket
|
|
9
|
+
* listing when missing or unparsable (self-healing). Concurrent writers can
|
|
10
|
+
* race the index read-modify-write; the rebuild path recovers from any
|
|
11
|
+
* resulting drift, matching curator's production behavior.
|
|
12
|
+
*
|
|
13
|
+
* @aws-sdk/client-s3 is an optional peer — loaded lazily with the same
|
|
14
|
+
* createRequire pattern as memory's @juspay/hippocampus so importing
|
|
15
|
+
* NeuroLink core never requires the AWS SDK. Tests (and hosts with
|
|
16
|
+
* pre-configured clients) inject a SkillS3ObjectOps implementation instead.
|
|
17
|
+
*/
|
|
18
|
+
import type { SkillDefinition, SkillIndexItem, SkillS3ObjectOps, SkillS3StorageConfig, SkillStore } from "../types/index.js";
|
|
19
|
+
export declare class S3SkillStore implements SkillStore {
|
|
20
|
+
private readonly config;
|
|
21
|
+
/** Test/host seam — omit to build ops from @aws-sdk/client-s3 lazily. */
|
|
22
|
+
private readonly injectedOps?;
|
|
23
|
+
private readonly prefix;
|
|
24
|
+
private ops;
|
|
25
|
+
constructor(config: SkillS3StorageConfig,
|
|
26
|
+
/** Test/host seam — omit to build ops from @aws-sdk/client-s3 lazily. */
|
|
27
|
+
injectedOps?: SkillS3ObjectOps | undefined);
|
|
28
|
+
private getOps;
|
|
29
|
+
private skillKey;
|
|
30
|
+
private indexKey;
|
|
31
|
+
get(id: string): Promise<SkillDefinition | null>;
|
|
32
|
+
put(skill: SkillDefinition): Promise<void>;
|
|
33
|
+
delete(id: string): Promise<void>;
|
|
34
|
+
index(): Promise<SkillIndexItem[]>;
|
|
35
|
+
private writeIndex;
|
|
36
|
+
/**
|
|
37
|
+
* Read index.json; when missing or unparsable, rebuild it from a listing
|
|
38
|
+
* of the skills/ prefix and persist the rebuilt document (self-heal).
|
|
39
|
+
*/
|
|
40
|
+
private readOrRebuildIndex;
|
|
41
|
+
}
|