@juspay/neurolink 9.84.2 → 9.85.1

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +410 -375
  3. package/dist/cli/factories/commandFactory.d.ts +17 -0
  4. package/dist/cli/factories/commandFactory.js +254 -0
  5. package/dist/cli/parser.js +2 -0
  6. package/dist/cli/utils/skillsFlags.d.ts +10 -0
  7. package/dist/cli/utils/skillsFlags.js +22 -0
  8. package/dist/index.d.ts +5 -0
  9. package/dist/index.js +6 -0
  10. package/dist/lib/index.d.ts +5 -0
  11. package/dist/lib/index.js +6 -0
  12. package/dist/lib/neurolink.d.ts +28 -0
  13. package/dist/lib/neurolink.js +117 -0
  14. package/dist/lib/server/routes/agentRoutes.js +156 -1
  15. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +39 -1
  16. package/dist/lib/server/routes/claudeProxyRoutes.js +300 -41
  17. package/dist/lib/server/utils/validation.d.ts +32 -0
  18. package/dist/lib/server/utils/validation.js +18 -0
  19. package/dist/lib/session/globalSessionState.d.ts +10 -1
  20. package/dist/lib/session/globalSessionState.js +18 -0
  21. package/dist/lib/skills/skillMatcher.d.ts +20 -0
  22. package/dist/lib/skills/skillMatcher.js +80 -0
  23. package/dist/lib/skills/skillStoreRedis.d.ts +22 -0
  24. package/dist/lib/skills/skillStoreRedis.js +98 -0
  25. package/dist/lib/skills/skillStoreS3.d.ts +41 -0
  26. package/dist/lib/skills/skillStoreS3.js +233 -0
  27. package/dist/lib/skills/skillStores.d.ts +44 -0
  28. package/dist/lib/skills/skillStores.js +252 -0
  29. package/dist/lib/skills/skillTools.d.ts +19 -0
  30. package/dist/lib/skills/skillTools.js +340 -0
  31. package/dist/lib/skills/skillsManager.d.ts +54 -0
  32. package/dist/lib/skills/skillsManager.js +220 -0
  33. package/dist/lib/types/config.d.ts +10 -0
  34. package/dist/lib/types/generate.d.ts +8 -0
  35. package/dist/lib/types/index.d.ts +1 -0
  36. package/dist/lib/types/index.js +1 -0
  37. package/dist/lib/types/proxy.d.ts +30 -2
  38. package/dist/lib/types/skills.d.ts +296 -0
  39. package/dist/lib/types/skills.js +17 -0
  40. package/dist/lib/types/stream.d.ts +8 -0
  41. package/dist/neurolink.d.ts +28 -0
  42. package/dist/neurolink.js +117 -0
  43. package/dist/server/routes/agentRoutes.js +156 -1
  44. package/dist/server/routes/claudeProxyRoutes.d.ts +39 -1
  45. package/dist/server/routes/claudeProxyRoutes.js +300 -41
  46. package/dist/server/utils/validation.d.ts +32 -0
  47. package/dist/server/utils/validation.js +18 -0
  48. package/dist/session/globalSessionState.d.ts +10 -1
  49. package/dist/session/globalSessionState.js +18 -0
  50. package/dist/skills/skillMatcher.d.ts +20 -0
  51. package/dist/skills/skillMatcher.js +79 -0
  52. package/dist/skills/skillStoreRedis.d.ts +22 -0
  53. package/dist/skills/skillStoreRedis.js +97 -0
  54. package/dist/skills/skillStoreS3.d.ts +41 -0
  55. package/dist/skills/skillStoreS3.js +232 -0
  56. package/dist/skills/skillStores.d.ts +44 -0
  57. package/dist/skills/skillStores.js +251 -0
  58. package/dist/skills/skillTools.d.ts +19 -0
  59. package/dist/skills/skillTools.js +339 -0
  60. package/dist/skills/skillsManager.d.ts +54 -0
  61. package/dist/skills/skillsManager.js +219 -0
  62. package/dist/types/config.d.ts +10 -0
  63. package/dist/types/generate.d.ts +8 -0
  64. package/dist/types/index.d.ts +1 -0
  65. package/dist/types/index.js +1 -0
  66. package/dist/types/proxy.d.ts +30 -2
  67. package/dist/types/skills.d.ts +296 -0
  68. package/dist/types/skills.js +16 -0
  69. package/dist/types/stream.d.ts +8 -0
  70. package/package.json +2 -1
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Built-in skill stores + the storage-config factory.
3
+ *
4
+ * First-party backends: in-process memory (tests, embedded), filesystem
5
+ * (curator-style JSON alongside Claude-skills-style markdown — `<name>.md`
6
+ * or `<name>/SKILL.md` with YAML frontmatter), S3 (skillStoreS3.ts, optional
7
+ * @aws-sdk/client-s3 peer), and Redis (skillStoreRedis.ts, pooled core
8
+ * client). Anything else plugs in via `{ type: "custom", store }`.
9
+ */
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ import yaml from "js-yaml";
13
+ import { logger } from "../utils/logger.js";
14
+ import { toSkillIndexItem } from "./skillMatcher.js";
15
+ import { S3SkillStore } from "./skillStoreS3.js";
16
+ import { RedisSkillStore } from "./skillStoreRedis.js";
17
+ /** Fill defaulted fields so downstream logic never branches on undefined. */
18
+ function normalizeSkill(skill) {
19
+ return {
20
+ ...skill,
21
+ scope: skill.scope ?? "global",
22
+ status: skill.status ?? "active",
23
+ version: skill.version ?? 1,
24
+ tags: skill.tags ?? [],
25
+ };
26
+ }
27
+ /** Minimal structural validation for skills loaded from external sources. */
28
+ function isValidSkill(candidate) {
29
+ if (typeof candidate !== "object" || candidate === null) {
30
+ return false;
31
+ }
32
+ const record = candidate;
33
+ return (typeof record.id === "string" &&
34
+ record.id.length > 0 &&
35
+ typeof record.name === "string" &&
36
+ record.name.length > 0 &&
37
+ typeof record.description === "string" &&
38
+ typeof record.instructions === "string");
39
+ }
40
+ /** In-process store backed by a Map. */
41
+ export class InMemorySkillStore {
42
+ skills = new Map();
43
+ constructor(seed) {
44
+ for (const skill of seed ?? []) {
45
+ this.skills.set(skill.id, normalizeSkill(skill));
46
+ }
47
+ }
48
+ async get(id) {
49
+ return this.skills.get(id) ?? null;
50
+ }
51
+ async put(skill) {
52
+ this.skills.set(skill.id, normalizeSkill(skill));
53
+ }
54
+ async delete(id) {
55
+ this.skills.delete(id);
56
+ }
57
+ async index() {
58
+ return Array.from(this.skills.values()).map(toSkillIndexItem);
59
+ }
60
+ }
61
+ /**
62
+ * Parse a markdown document with optional YAML frontmatter into a skill.
63
+ * The body becomes `instructions`; frontmatter supplies index metadata.
64
+ * Returns null when required fields are missing (logged, not thrown —
65
+ * one malformed file must not take down the whole store).
66
+ */
67
+ function parseSkillMarkdown(raw, fallbackId) {
68
+ const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
69
+ let meta = {};
70
+ let body = raw;
71
+ if (frontmatterMatch) {
72
+ try {
73
+ const parsed = yaml.load(frontmatterMatch[1]);
74
+ if (typeof parsed === "object" && parsed !== null) {
75
+ meta = parsed;
76
+ }
77
+ }
78
+ catch (error) {
79
+ logger.warn("[SkillStore] Invalid YAML frontmatter in skill markdown", {
80
+ fallbackId,
81
+ error: error instanceof Error ? error.message : String(error),
82
+ });
83
+ return null;
84
+ }
85
+ body = raw.slice(frontmatterMatch[0].length);
86
+ }
87
+ const name = typeof meta.name === "string" ? meta.name : fallbackId;
88
+ const description = typeof meta.description === "string" ? meta.description : "";
89
+ const instructions = body.trim();
90
+ if (!name || !description || !instructions) {
91
+ logger.warn("[SkillStore] Skipping skill markdown missing name/description/body", { fallbackId });
92
+ return null;
93
+ }
94
+ const candidate = normalizeSkill({
95
+ id: typeof meta.id === "string" ? meta.id : name,
96
+ name,
97
+ description,
98
+ instructions,
99
+ ...(typeof meta.displayName === "string"
100
+ ? { displayName: meta.displayName }
101
+ : {}),
102
+ ...(Array.isArray(meta.tags)
103
+ ? { tags: meta.tags.map((t) => String(t)) }
104
+ : {}),
105
+ ...(meta.scope === "scoped" || meta.scope === "global"
106
+ ? { scope: meta.scope }
107
+ : {}),
108
+ ...(Array.isArray(meta.scopeIds)
109
+ ? { scopeIds: meta.scopeIds.map((s) => String(s)) }
110
+ : {}),
111
+ ...(typeof meta.version === "number" ? { version: meta.version } : {}),
112
+ });
113
+ return candidate;
114
+ }
115
+ /**
116
+ * Directory-backed store. Read layouts:
117
+ * - `<dir>/<id>.json` — JSON SkillDefinition (mutable)
118
+ * - `<dir>/<name>.md` — frontmatter markdown (read-only source)
119
+ * - `<dir>/<name>/SKILL.md` — Claude-skills directory layout (read-only source)
120
+ * Mutations always write `<id>.json`; a JSON file shadows a markdown skill
121
+ * with the same id, so updating a markdown-sourced skill "copies up" to JSON.
122
+ */
123
+ export class FileSystemSkillStore {
124
+ baseDir;
125
+ cache = null;
126
+ constructor(baseDir) {
127
+ this.baseDir = baseDir;
128
+ }
129
+ invalidate() {
130
+ this.cache = null;
131
+ }
132
+ async get(id) {
133
+ const all = this.load();
134
+ return all.get(id) ?? null;
135
+ }
136
+ async put(skill) {
137
+ fs.mkdirSync(this.baseDir, { recursive: true });
138
+ const filePath = path.join(this.baseDir, `${skill.id}.json`);
139
+ fs.writeFileSync(filePath, JSON.stringify(normalizeSkill(skill), null, 2), "utf-8");
140
+ this.invalidate();
141
+ }
142
+ async delete(id) {
143
+ const filePath = path.join(this.baseDir, `${id}.json`);
144
+ try {
145
+ fs.unlinkSync(filePath);
146
+ }
147
+ catch (error) {
148
+ const code = error.code;
149
+ if (code !== "ENOENT") {
150
+ throw error;
151
+ }
152
+ }
153
+ this.invalidate();
154
+ }
155
+ async index() {
156
+ return Array.from(this.load().values()).map(toSkillIndexItem);
157
+ }
158
+ /**
159
+ * Scan the directory into an id→skill map. Markdown sources load first
160
+ * so JSON files (the mutable layer) shadow them on id collision.
161
+ */
162
+ load() {
163
+ if (this.cache) {
164
+ return this.cache;
165
+ }
166
+ const skills = new Map();
167
+ let entries;
168
+ try {
169
+ entries = fs.readdirSync(this.baseDir, { withFileTypes: true });
170
+ }
171
+ catch (error) {
172
+ const code = error.code;
173
+ if (code !== "ENOENT") {
174
+ logger.warn("[SkillStore] Failed to read skills directory", {
175
+ baseDir: this.baseDir,
176
+ error: error instanceof Error ? error.message : String(error),
177
+ });
178
+ }
179
+ this.cache = skills;
180
+ return skills;
181
+ }
182
+ const jsonFiles = [];
183
+ for (const entry of entries) {
184
+ const fullPath = path.join(this.baseDir, entry.name);
185
+ try {
186
+ if (entry.isDirectory()) {
187
+ const skillMd = path.join(fullPath, "SKILL.md");
188
+ if (fs.existsSync(skillMd)) {
189
+ const parsed = parseSkillMarkdown(fs.readFileSync(skillMd, "utf-8"), entry.name);
190
+ if (parsed) {
191
+ skills.set(parsed.id, parsed);
192
+ }
193
+ }
194
+ }
195
+ else if (entry.name.endsWith(".md")) {
196
+ const parsed = parseSkillMarkdown(fs.readFileSync(fullPath, "utf-8"), entry.name.replace(/\.md$/, ""));
197
+ if (parsed) {
198
+ skills.set(parsed.id, parsed);
199
+ }
200
+ }
201
+ else if (entry.name.endsWith(".json")) {
202
+ jsonFiles.push(fullPath);
203
+ }
204
+ }
205
+ catch (error) {
206
+ logger.warn("[SkillStore] Failed to load skill file", {
207
+ file: fullPath,
208
+ error: error instanceof Error ? error.message : String(error),
209
+ });
210
+ }
211
+ }
212
+ // JSON layer last — shadows markdown-sourced skills with the same id.
213
+ for (const filePath of jsonFiles) {
214
+ try {
215
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
216
+ if (isValidSkill(parsed)) {
217
+ skills.set(parsed.id, normalizeSkill(parsed));
218
+ }
219
+ else {
220
+ logger.warn("[SkillStore] Skipping invalid skill JSON", {
221
+ file: filePath,
222
+ });
223
+ }
224
+ }
225
+ catch (error) {
226
+ logger.warn("[SkillStore] Failed to parse skill JSON", {
227
+ file: filePath,
228
+ error: error instanceof Error ? error.message : String(error),
229
+ });
230
+ }
231
+ }
232
+ this.cache = skills;
233
+ return skills;
234
+ }
235
+ }
236
+ /** Resolve a storage config to a concrete store. Defaults to memory. */
237
+ export function createSkillStore(config) {
238
+ if (!config || config.type === "memory") {
239
+ return new InMemorySkillStore(config?.type === "memory" ? config.skills : undefined);
240
+ }
241
+ if (config.type === "filesystem") {
242
+ return new FileSystemSkillStore(config.path);
243
+ }
244
+ if (config.type === "s3") {
245
+ return new S3SkillStore(config);
246
+ }
247
+ if (config.type === "redis") {
248
+ return new RedisSkillStore(config);
249
+ }
250
+ return config.store;
251
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Built-in skill tools, following the createMemoryRetrievalTools /
3
+ * createFileTools / createTaskTools factory pattern.
4
+ *
5
+ * The manager is resolved lazily via a resolver callback so tools can be
6
+ * registered at construction time while the store initializes on first
7
+ * use (mirrors how retrieve_context resolves conversationMemory lazily).
8
+ *
9
+ * Tool descriptions are ported from curator's production skills tools —
10
+ * the "always check skills first / no_match is expected, not an error /
11
+ * pick the best of 2-5 or ask" prompt engineering is proven in prod.
12
+ */
13
+ import type { SkillsManagerLike, SkillToolsOptions, Tool } from "../types/index.js";
14
+ /**
15
+ * Create the built-in skill tools bound to a lazily-resolved manager.
16
+ * Returns Vercel AI SDK tool() objects (description + Zod inputSchema +
17
+ * execute) keyed by tool name.
18
+ */
19
+ export declare function createSkillTools(resolveManager: () => SkillsManagerLike | null, options?: SkillToolsOptions): Record<string, Tool>;
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Built-in skill tools, following the createMemoryRetrievalTools /
3
+ * createFileTools / createTaskTools factory pattern.
4
+ *
5
+ * The manager is resolved lazily via a resolver callback so tools can be
6
+ * registered at construction time while the store initializes on first
7
+ * use (mirrors how retrieve_context resolves conversationMemory lazily).
8
+ *
9
+ * Tool descriptions are ported from curator's production skills tools —
10
+ * the "always check skills first / no_match is expected, not an error /
11
+ * pick the best of 2-5 or ask" prompt engineering is proven in prod.
12
+ */
13
+ import { z } from "zod";
14
+ import { logger } from "../utils/logger.js";
15
+ import { tool } from "../utils/tool.js";
16
+ /** Trim a hydrated skill to the fields the model needs. */
17
+ function toToolSkill(skill) {
18
+ return {
19
+ id: skill.id,
20
+ name: skill.name,
21
+ ...(skill.displayName ? { displayName: skill.displayName } : {}),
22
+ description: skill.description,
23
+ instructions: skill.instructions,
24
+ tags: skill.tags ?? [],
25
+ scope: skill.scope ?? "global",
26
+ version: skill.version ?? 1,
27
+ };
28
+ }
29
+ /**
30
+ * Create the built-in skill tools bound to a lazily-resolved manager.
31
+ * Returns Vercel AI SDK tool() objects (description + Zod inputSchema +
32
+ * execute) keyed by tool name.
33
+ */
34
+ export function createSkillTools(resolveManager, options) {
35
+ const notConfigured = {
36
+ success: false,
37
+ error: "Skills are not available — the skills store failed to initialize. " +
38
+ "Answer from general knowledge.",
39
+ data: { skills: [] },
40
+ };
41
+ const tools = {
42
+ search_skills: tool({
43
+ description: "ALWAYS call this at the start of every user request before answering from general knowledge. " +
44
+ "Searches team-defined skills (SOPs, playbooks, workflows) using an index-first approach — " +
45
+ "fast, low-cost, and does not load skills that do not match. " +
46
+ "You MUST provide at least one of: query (keyword from the user message) or tag (domain category). " +
47
+ "If 1 match is found, follow its instructions exactly. " +
48
+ "If 2-5 matches are found, use your judgment based on the user message to pick the best one, " +
49
+ "or ask the user to clarify if the instructions would lead to meaningfully different actions. " +
50
+ "Only skip this call for purely conversational messages like greetings. " +
51
+ 'Returns `{ skills: [], reason: "no_match" }` when nothing matches — this is expected, NOT an ' +
52
+ "error. In that case, answer from general knowledge.",
53
+ inputSchema: z.object({
54
+ query: z
55
+ .string()
56
+ .optional()
57
+ .describe("Keyword from the user message, matched against skill name, display name, and description. " +
58
+ 'E.g. "deployment", "refund process", "on-call escalation".'),
59
+ tag: z
60
+ .string()
61
+ .optional()
62
+ .describe('Domain category tag to narrow results (e.g. "payments", "devops"). ' +
63
+ "Applied on top of the query filter when both are provided."),
64
+ scopeId: z
65
+ .string()
66
+ .optional()
67
+ .describe("Scope identifier (channel/team/tenant id) to include scoped skills alongside global ones. " +
68
+ "Usually omit — the host default applies."),
69
+ }),
70
+ execute: async (args) => {
71
+ if (!args.query && !args.tag) {
72
+ return {
73
+ success: false,
74
+ error: 'At least one of "query" or "tag" must be provided to search_skills.',
75
+ data: { skills: [] },
76
+ };
77
+ }
78
+ const manager = resolveManager();
79
+ if (!manager) {
80
+ return notConfigured;
81
+ }
82
+ try {
83
+ const skills = await manager.search(args);
84
+ if (skills.length === 0) {
85
+ return {
86
+ success: true,
87
+ data: {
88
+ skills: [],
89
+ reason: "no_match",
90
+ message: `No skills found${args.query ? ` matching "${args.query}"` : ""}${args.tag ? ` with tag "${args.tag}"` : ""}. Answer from general knowledge.`,
91
+ },
92
+ };
93
+ }
94
+ return {
95
+ success: true,
96
+ data: {
97
+ skills: skills.map(toToolSkill),
98
+ count: skills.length,
99
+ ...(skills.length > 1
100
+ ? {
101
+ hint: "Multiple skills matched. Pick the most relevant one for the user message, or ask the user to clarify.",
102
+ }
103
+ : {}),
104
+ },
105
+ };
106
+ }
107
+ catch (error) {
108
+ logger.warn("[SkillTools] search_skills failed", {
109
+ error: error instanceof Error ? error.message : String(error),
110
+ });
111
+ return {
112
+ success: false,
113
+ error: error instanceof Error ? error.message : String(error),
114
+ data: { skills: [] },
115
+ };
116
+ }
117
+ },
118
+ }),
119
+ list_skills: tool({
120
+ description: "Returns a lightweight list of all available skills — name, display name, description, and " +
121
+ "tags only. No instructions are returned, keeping context cost minimal. " +
122
+ 'Use this only when a user explicitly asks "what skills do you have?" or "what can you help ' +
123
+ 'me with?". Do NOT use this for skill lookup before answering — use search_skills instead.',
124
+ inputSchema: z.object({
125
+ scopeId: z
126
+ .string()
127
+ .optional()
128
+ .describe("Scope identifier to include scoped skills alongside global ones. Usually omit."),
129
+ }),
130
+ execute: async (args) => {
131
+ const manager = resolveManager();
132
+ if (!manager) {
133
+ return notConfigured;
134
+ }
135
+ try {
136
+ const items = await manager.list(args.scopeId);
137
+ return {
138
+ success: true,
139
+ data: {
140
+ skills: items.map((item) => ({
141
+ name: item.name,
142
+ ...(item.displayName ? { displayName: item.displayName } : {}),
143
+ description: item.description,
144
+ tags: item.tags ?? [],
145
+ scope: item.scope ?? "global",
146
+ })),
147
+ count: items.length,
148
+ },
149
+ };
150
+ }
151
+ catch (error) {
152
+ logger.warn("[SkillTools] list_skills failed", {
153
+ error: error instanceof Error ? error.message : String(error),
154
+ });
155
+ return {
156
+ success: false,
157
+ error: error instanceof Error ? error.message : String(error),
158
+ data: { skills: [] },
159
+ };
160
+ }
161
+ },
162
+ }),
163
+ };
164
+ if (options?.allowMutations) {
165
+ Object.assign(tools, createSkillMutationTools(resolveManager));
166
+ }
167
+ return tools;
168
+ }
169
+ /** Shared executor for the three mutation tools. */
170
+ async function runMutation(resolveManager, action) {
171
+ const manager = resolveManager();
172
+ if (!manager) {
173
+ return {
174
+ success: false,
175
+ error: "Skills are not available — the skills store failed to initialize.",
176
+ };
177
+ }
178
+ try {
179
+ const result = await manager.requestMutation(action);
180
+ if (result.decision.outcome === "rejected") {
181
+ return {
182
+ success: false,
183
+ error: `Skill ${action.type} was rejected${result.decision.reason ? `: ${result.decision.reason}` : "."}`,
184
+ };
185
+ }
186
+ if (result.decision.outcome === "pending") {
187
+ return {
188
+ success: true,
189
+ data: {
190
+ status: "pending_approval",
191
+ ...(result.decision.reference
192
+ ? { reference: result.decision.reference }
193
+ : {}),
194
+ message: `Skill ${action.type} was submitted for approval. It takes effect once an approver accepts it.`,
195
+ },
196
+ };
197
+ }
198
+ return {
199
+ success: true,
200
+ data: {
201
+ status: "applied",
202
+ ...(result.skill
203
+ ? {
204
+ skillId: result.skill.id,
205
+ name: result.skill.name,
206
+ version: result.skill.version,
207
+ skillStatus: result.skill.status,
208
+ }
209
+ : {}),
210
+ },
211
+ };
212
+ }
213
+ catch (error) {
214
+ logger.warn(`[SkillTools] skill_${action.type} failed`, {
215
+ error: error instanceof Error ? error.message : String(error),
216
+ });
217
+ return {
218
+ success: false,
219
+ error: error instanceof Error ? error.message : String(error),
220
+ };
221
+ }
222
+ }
223
+ function createSkillMutationTools(resolveManager) {
224
+ const scopeFields = {
225
+ scope: z
226
+ .enum(["global", "scoped"])
227
+ .optional()
228
+ .describe('"global" = available everywhere; "scoped" = only for specific scope ids.'),
229
+ scopeIds: z
230
+ .array(z.string())
231
+ .optional()
232
+ .describe('Required when scope="scoped": the scope ids (channel/team/tenant) the skill applies to.'),
233
+ };
234
+ return {
235
+ skill_create: tool({
236
+ description: "Propose a NEW skill (SOP/playbook). Depending on host configuration the skill is either " +
237
+ "applied immediately or queued for human approval. " +
238
+ "INVOKE ONLY WHEN the user explicitly asks to create a new skill and has supplied the name, " +
239
+ "description, and full instructions text. DO NOT INVENT CONTENT — the instructions must come " +
240
+ "from the user verbatim or nearly so. If the user has not given the full text, ask. " +
241
+ "Do not use this to modify an existing skill (use skill_update).",
242
+ inputSchema: z.object({
243
+ name: z
244
+ .string()
245
+ .min(1)
246
+ .describe('Short machine-friendly skill name (snake_case recommended), unique. E.g. "refund_dispute_escalation".'),
247
+ displayName: z
248
+ .string()
249
+ .optional()
250
+ .describe('Human-readable display name. E.g. "Refund Dispute Escalation".'),
251
+ description: z
252
+ .string()
253
+ .min(1)
254
+ .describe("One or two sentences explaining when this skill applies. Used for matching."),
255
+ instructions: z
256
+ .string()
257
+ .min(1)
258
+ .describe("The full step-by-step instructions to follow when this skill matches. " +
259
+ "Must come from the user — do not invent steps."),
260
+ tags: z
261
+ .array(z.string())
262
+ .optional()
263
+ .describe('Domain tags for filtering (e.g. ["payments", "escalation"]).'),
264
+ ...scopeFields,
265
+ requestedBy: z
266
+ .string()
267
+ .optional()
268
+ .describe("Identifier of the requesting user, when known."),
269
+ }),
270
+ execute: async (args) => {
271
+ if (args.scope === "scoped" && (args.scopeIds ?? []).length === 0) {
272
+ return {
273
+ success: false,
274
+ error: '"scopeIds" must contain at least one scope id when scope="scoped".',
275
+ };
276
+ }
277
+ const { requestedBy, ...skill } = args;
278
+ return runMutation(resolveManager, {
279
+ type: "create",
280
+ skill,
281
+ ...(requestedBy ? { requestedBy } : {}),
282
+ });
283
+ },
284
+ }),
285
+ skill_update: tool({
286
+ description: "Propose an update to an EXISTING skill. Only the provided fields change; the version is " +
287
+ "bumped. Depending on host configuration the change is applied immediately or queued for " +
288
+ "human approval. Look the skill up with search_skills or list_skills first to get its id or " +
289
+ "exact name. Do not paraphrase or expand the user's instructions.",
290
+ inputSchema: z.object({
291
+ skillId: z
292
+ .string()
293
+ .min(1)
294
+ .describe("Id (or exact unique name) of the skill to update."),
295
+ displayName: z.string().optional(),
296
+ description: z.string().optional(),
297
+ instructions: z
298
+ .string()
299
+ .optional()
300
+ .describe("Replacement instructions text, verbatim from the user."),
301
+ tags: z.array(z.string()).optional(),
302
+ ...scopeFields,
303
+ requestedBy: z.string().optional(),
304
+ }),
305
+ execute: async (args) => {
306
+ const { skillId, requestedBy, ...patch } = args;
307
+ if (Object.values(patch).every((v) => v === undefined)) {
308
+ return {
309
+ success: false,
310
+ error: "Provide at least one field to update.",
311
+ };
312
+ }
313
+ return runMutation(resolveManager, {
314
+ type: "update",
315
+ skillId,
316
+ patch,
317
+ ...(requestedBy ? { requestedBy } : {}),
318
+ });
319
+ },
320
+ }),
321
+ skill_delete: tool({
322
+ description: "Propose deleting (deprecating) an EXISTING skill. The skill stops matching but stays in " +
323
+ "storage for audit. Depending on host configuration this is applied immediately or queued " +
324
+ "for human approval. INVOKE ONLY WHEN the user explicitly asks to delete a skill.",
325
+ inputSchema: z.object({
326
+ skillId: z
327
+ .string()
328
+ .min(1)
329
+ .describe("Id (or exact unique name) of the skill to delete."),
330
+ requestedBy: z.string().optional(),
331
+ }),
332
+ execute: async (args) => runMutation(resolveManager, {
333
+ type: "delete",
334
+ skillId: args.skillId,
335
+ ...(args.requestedBy ? { requestedBy: args.requestedBy } : {}),
336
+ }),
337
+ }),
338
+ };
339
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * SkillsManager — orchestrates the skill store, cached index, index-first
3
+ * search (hydrating instructions only for matches), prompt-index rendering,
4
+ * and the mutation gate.
5
+ *
6
+ * Read paths fail open (errors surface as empty results + a warn log);
7
+ * write paths fail closed (errors reject the mutation).
8
+ */
9
+ import type { SkillDefinition, SkillIndexItem, SkillMutationAction, SkillMutationResult, SkillSearchQuery, SkillsConfig } from "../types/index.js";
10
+ export declare class SkillsManager {
11
+ private readonly config;
12
+ private readonly store;
13
+ private cachedIndex;
14
+ private cachedIndexAt;
15
+ /** Serializes writes within this process — see requestMutation(). */
16
+ private mutationQueue;
17
+ constructor(config: SkillsConfig);
18
+ /** Cached index read. TTL 0 disables caching. */
19
+ getIndex(forceRefresh?: boolean): Promise<SkillIndexItem[]>;
20
+ private invalidateIndex;
21
+ /**
22
+ * Index-first search: filter the cached index, hydrate only the matched
23
+ * entries (max `limit`) with instructions. Cost: one cached index read +
24
+ * N_matched store gets.
25
+ */
26
+ search(query: SkillSearchQuery): Promise<SkillDefinition[]>;
27
+ /** Index entries only — no instructions. For discovery/listing. */
28
+ list(scopeId?: string): Promise<SkillIndexItem[]>;
29
+ /** Fetch one skill by id, falling back to name lookup. */
30
+ get(idOrName: string): Promise<SkillDefinition | null>;
31
+ /**
32
+ * Render the system-prompt skills index for one call, or null when
33
+ * nothing is visible. Never includes instructions.
34
+ */
35
+ buildPromptIndex(options?: {
36
+ scopeId?: string;
37
+ tags?: string[];
38
+ }): Promise<string | null>;
39
+ /**
40
+ * Whether skill create/update/delete is enabled on this instance. Gates the
41
+ * LLM-facing `skill_*` tools (registration) and the server REST mutation
42
+ * routes. Direct programmatic `requestMutation` calls are intentionally not
43
+ * gated, so a host can still seed skills at startup.
44
+ */
45
+ get mutationsAllowed(): boolean;
46
+ /**
47
+ * Gate a proposed mutation through the host's onMutationRequest hook,
48
+ * then apply it when approved. No hook configured means direct apply
49
+ * (the tools themselves are already gated by allowMutations).
50
+ */
51
+ requestMutation(action: SkillMutationAction): Promise<SkillMutationResult>;
52
+ private applyMutation;
53
+ private assertNameAvailable;
54
+ }