@juspay/neurolink 9.85.1 → 9.86.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 +370 -362
- package/dist/context/contextCompactor.js +16 -2
- package/dist/context/stages/slidingWindowTruncator.js +76 -30
- package/dist/core/conversationMemoryManager.js +13 -2
- package/dist/core/redisConversationMemoryManager.js +10 -1
- package/dist/lib/context/contextCompactor.js +16 -2
- package/dist/lib/context/stages/slidingWindowTruncator.js +76 -30
- package/dist/lib/core/conversationMemoryManager.js +13 -2
- package/dist/lib/core/redisConversationMemoryManager.js +10 -1
- package/dist/lib/neurolink.d.ts +31 -6
- package/dist/lib/neurolink.js +163 -33
- package/dist/lib/skills/skillMatcher.d.ts +33 -4
- package/dist/lib/skills/skillMatcher.js +81 -6
- package/dist/lib/skills/skillSessionTracker.d.ts +52 -0
- package/dist/lib/skills/skillSessionTracker.js +150 -0
- package/dist/lib/skills/skillStoreRedis.d.ts +8 -0
- package/dist/lib/skills/skillStoreRedis.js +18 -2
- package/dist/lib/skills/skillStoreS3.d.ts +17 -2
- package/dist/lib/skills/skillStoreS3.js +78 -6
- package/dist/lib/skills/skillStores.d.ts +16 -2
- package/dist/lib/skills/skillStores.js +94 -5
- package/dist/lib/skills/skillTools.d.ts +26 -10
- package/dist/lib/skills/skillTools.js +190 -79
- package/dist/lib/skills/skillsManager.d.ts +25 -1
- package/dist/lib/skills/skillsManager.js +46 -4
- package/dist/lib/types/config.d.ts +5 -5
- package/dist/lib/types/conversation.d.ts +27 -0
- package/dist/lib/types/skills.d.ts +145 -14
- package/dist/lib/types/skills.js +3 -3
- package/dist/lib/utils/conversationMemory.d.ts +1 -1
- package/dist/lib/utils/conversationMemory.js +15 -2
- package/dist/neurolink.d.ts +31 -6
- package/dist/neurolink.js +163 -33
- package/dist/skills/skillMatcher.d.ts +33 -4
- package/dist/skills/skillMatcher.js +81 -6
- package/dist/skills/skillSessionTracker.d.ts +52 -0
- package/dist/skills/skillSessionTracker.js +149 -0
- package/dist/skills/skillStoreRedis.d.ts +8 -0
- package/dist/skills/skillStoreRedis.js +18 -2
- package/dist/skills/skillStoreS3.d.ts +17 -2
- package/dist/skills/skillStoreS3.js +78 -6
- package/dist/skills/skillStores.d.ts +16 -2
- package/dist/skills/skillStores.js +94 -5
- package/dist/skills/skillTools.d.ts +26 -10
- package/dist/skills/skillTools.js +190 -79
- package/dist/skills/skillsManager.d.ts +25 -1
- package/dist/skills/skillsManager.js +46 -4
- package/dist/types/config.d.ts +5 -5
- package/dist/types/conversation.d.ts +27 -0
- package/dist/types/skills.d.ts +145 -14
- package/dist/types/skills.js +3 -3
- package/dist/utils/conversationMemory.d.ts +1 -1
- package/dist/utils/conversationMemory.js +15 -2
- package/package.json +1 -1
|
@@ -9,11 +9,19 @@
|
|
|
9
9
|
import type { SkillDefinition, SkillIndexItem, SkillRedisStorageConfig, SkillStore } from "../types/index.js";
|
|
10
10
|
export declare class RedisSkillStore implements SkillStore {
|
|
11
11
|
private readonly keyPrefix;
|
|
12
|
+
/**
|
|
13
|
+
* Resources live under `<keyPrefix>__resources__:` — a reserved segment
|
|
14
|
+
* inside the skill prefix, explicitly excluded from the index SCAN so
|
|
15
|
+
* resource values are never mistaken for skills, whatever the
|
|
16
|
+
* configured keyPrefix looks like.
|
|
17
|
+
*/
|
|
18
|
+
private readonly resourcePrefix;
|
|
12
19
|
private readonly normalizedConfig;
|
|
13
20
|
private clientPromise;
|
|
14
21
|
constructor(config: SkillRedisStorageConfig);
|
|
15
22
|
private getClient;
|
|
16
23
|
private skillKey;
|
|
24
|
+
getResource(id: string, resourcePath: string): Promise<string | null>;
|
|
17
25
|
get(id: string): Promise<SkillDefinition | null>;
|
|
18
26
|
put(skill: SkillDefinition): Promise<void>;
|
|
19
27
|
delete(id: string): Promise<void>;
|
|
@@ -8,14 +8,22 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { getNormalizedConfig, getPooledRedisClient, scanKeys, } from "../utils/redis.js";
|
|
10
10
|
import { logger } from "../utils/logger.js";
|
|
11
|
-
import { toSkillIndexItem } from "./skillMatcher.js";
|
|
11
|
+
import { isSafeSkillResourcePath, toSkillIndexItem } from "./skillMatcher.js";
|
|
12
12
|
const DEFAULT_KEY_PREFIX = "neurolink:skills:";
|
|
13
13
|
export class RedisSkillStore {
|
|
14
14
|
keyPrefix;
|
|
15
|
+
/**
|
|
16
|
+
* Resources live under `<keyPrefix>__resources__:` — a reserved segment
|
|
17
|
+
* inside the skill prefix, explicitly excluded from the index SCAN so
|
|
18
|
+
* resource values are never mistaken for skills, whatever the
|
|
19
|
+
* configured keyPrefix looks like.
|
|
20
|
+
*/
|
|
21
|
+
resourcePrefix;
|
|
15
22
|
normalizedConfig;
|
|
16
23
|
clientPromise = null;
|
|
17
24
|
constructor(config) {
|
|
18
25
|
this.keyPrefix = config.keyPrefix ?? DEFAULT_KEY_PREFIX;
|
|
26
|
+
this.resourcePrefix = `${this.keyPrefix}__resources__:`;
|
|
19
27
|
this.normalizedConfig = getNormalizedConfig({
|
|
20
28
|
...(config.url ? { url: config.url } : {}),
|
|
21
29
|
...(config.host ? { host: config.host } : {}),
|
|
@@ -39,6 +47,14 @@ export class RedisSkillStore {
|
|
|
39
47
|
skillKey(id) {
|
|
40
48
|
return `${this.keyPrefix}${id}`;
|
|
41
49
|
}
|
|
50
|
+
async getResource(id, resourcePath) {
|
|
51
|
+
if (!isSafeSkillResourcePath(resourcePath)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const client = await this.getClient();
|
|
55
|
+
const raw = await client.get(`${this.resourcePrefix}${id}:${resourcePath}`);
|
|
56
|
+
return raw ? String(raw) : null;
|
|
57
|
+
}
|
|
42
58
|
async get(id) {
|
|
43
59
|
const client = await this.getClient();
|
|
44
60
|
const raw = await client.get(this.skillKey(id));
|
|
@@ -55,7 +71,7 @@ export class RedisSkillStore {
|
|
|
55
71
|
}
|
|
56
72
|
async index() {
|
|
57
73
|
const client = await this.getClient();
|
|
58
|
-
const keys = await scanKeys(client, `${this.keyPrefix}*`);
|
|
74
|
+
const keys = (await scanKeys(client, `${this.keyPrefix}*`)).filter((key) => !key.startsWith(this.resourcePrefix));
|
|
59
75
|
if (keys.length === 0) {
|
|
60
76
|
return [];
|
|
61
77
|
}
|
|
@@ -22,20 +22,35 @@ export declare class S3SkillStore implements SkillStore {
|
|
|
22
22
|
private readonly injectedOps?;
|
|
23
23
|
private readonly prefix;
|
|
24
24
|
private ops;
|
|
25
|
+
/** Last parsed index + its ETag, revalidated with If-None-Match reads. */
|
|
26
|
+
private cachedIndexDoc;
|
|
27
|
+
private cachedIndexEtag;
|
|
25
28
|
constructor(config: SkillS3StorageConfig,
|
|
26
29
|
/** Test/host seam — omit to build ops from @aws-sdk/client-s3 lazily. */
|
|
27
30
|
injectedOps?: SkillS3ObjectOps | undefined);
|
|
31
|
+
invalidate(): void;
|
|
28
32
|
private getOps;
|
|
29
33
|
private skillKey;
|
|
30
34
|
private indexKey;
|
|
35
|
+
/** Resources live beside the skill object: <prefix>skills/<id>/<path>. */
|
|
36
|
+
private resourceKey;
|
|
37
|
+
getResource(id: string, resourcePath: string): Promise<string | null>;
|
|
31
38
|
get(id: string): Promise<SkillDefinition | null>;
|
|
32
39
|
put(skill: SkillDefinition): Promise<void>;
|
|
33
40
|
delete(id: string): Promise<void>;
|
|
34
41
|
index(): Promise<SkillIndexItem[]>;
|
|
35
42
|
private writeIndex;
|
|
36
43
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
44
|
+
* Raw index.json read. Returns the body (with its ETag when available),
|
|
45
|
+
* null body when the object is absent, or notModified when a
|
|
46
|
+
* conditional read reported the cached copy unchanged.
|
|
47
|
+
*/
|
|
48
|
+
private readIndexObject;
|
|
49
|
+
/**
|
|
50
|
+
* Read index.json (ETag-revalidated when the ops support conditional
|
|
51
|
+
* reads — an unchanged index costs a 304, not a download); when missing
|
|
52
|
+
* or unparsable, rebuild it from a listing of the skills/ prefix and
|
|
53
|
+
* persist the rebuilt document (self-heal).
|
|
39
54
|
*/
|
|
40
55
|
private readOrRebuildIndex;
|
|
41
56
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { createRequire } from "node:module";
|
|
19
19
|
import { logger } from "../utils/logger.js";
|
|
20
|
-
import { toSkillIndexItem } from "./skillMatcher.js";
|
|
20
|
+
import { isSafeSkillResourcePath, toSkillIndexItem } from "./skillMatcher.js";
|
|
21
21
|
const lazyRequire = createRequire(import.meta.url);
|
|
22
22
|
let cachedS3Module;
|
|
23
23
|
function loadS3Module() {
|
|
@@ -97,6 +97,32 @@ function createDefaultOps(config) {
|
|
|
97
97
|
} while (continuationToken);
|
|
98
98
|
return keys;
|
|
99
99
|
},
|
|
100
|
+
async getObjectConditional(key, etag) {
|
|
101
|
+
try {
|
|
102
|
+
const response = (await client.send(new mod.GetObjectCommand({
|
|
103
|
+
Bucket: bucket,
|
|
104
|
+
Key: key,
|
|
105
|
+
...(etag ? { IfNoneMatch: etag } : {}),
|
|
106
|
+
})));
|
|
107
|
+
const body = (await response.Body?.transformToString()) ?? null;
|
|
108
|
+
return {
|
|
109
|
+
body,
|
|
110
|
+
...(response.ETag ? { etag: response.ETag } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
const name = error.name;
|
|
115
|
+
const status = error
|
|
116
|
+
.$metadata?.httpStatusCode;
|
|
117
|
+
if (status === 304 || name === "304" || name === "NotModified") {
|
|
118
|
+
return { body: null, notModified: true };
|
|
119
|
+
}
|
|
120
|
+
if (name === "NoSuchKey" || name === "NotFound") {
|
|
121
|
+
return { body: null };
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
100
126
|
};
|
|
101
127
|
}
|
|
102
128
|
export class S3SkillStore {
|
|
@@ -104,6 +130,9 @@ export class S3SkillStore {
|
|
|
104
130
|
injectedOps;
|
|
105
131
|
prefix;
|
|
106
132
|
ops = null;
|
|
133
|
+
/** Last parsed index + its ETag, revalidated with If-None-Match reads. */
|
|
134
|
+
cachedIndexDoc = null;
|
|
135
|
+
cachedIndexEtag;
|
|
107
136
|
constructor(config,
|
|
108
137
|
/** Test/host seam — omit to build ops from @aws-sdk/client-s3 lazily. */
|
|
109
138
|
injectedOps) {
|
|
@@ -113,6 +142,10 @@ export class S3SkillStore {
|
|
|
113
142
|
this.prefix =
|
|
114
143
|
rawPrefix === "" || rawPrefix.endsWith("/") ? rawPrefix : `${rawPrefix}/`;
|
|
115
144
|
}
|
|
145
|
+
invalidate() {
|
|
146
|
+
this.cachedIndexDoc = null;
|
|
147
|
+
this.cachedIndexEtag = undefined;
|
|
148
|
+
}
|
|
116
149
|
getOps() {
|
|
117
150
|
if (!this.ops) {
|
|
118
151
|
this.ops = this.injectedOps ?? createDefaultOps(this.config);
|
|
@@ -125,6 +158,16 @@ export class S3SkillStore {
|
|
|
125
158
|
indexKey() {
|
|
126
159
|
return `${this.prefix}index.json`;
|
|
127
160
|
}
|
|
161
|
+
/** Resources live beside the skill object: <prefix>skills/<id>/<path>. */
|
|
162
|
+
resourceKey(id, resourcePath) {
|
|
163
|
+
return `${this.prefix}skills/${id}/${resourcePath}`;
|
|
164
|
+
}
|
|
165
|
+
async getResource(id, resourcePath) {
|
|
166
|
+
if (!isSafeSkillResourcePath(resourcePath)) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return this.getOps().getObject(this.resourceKey(id, resourcePath));
|
|
170
|
+
}
|
|
128
171
|
async get(id) {
|
|
129
172
|
const body = await this.getOps().getObject(this.skillKey(id));
|
|
130
173
|
if (!body) {
|
|
@@ -169,18 +212,42 @@ export class S3SkillStore {
|
|
|
169
212
|
async writeIndex(index) {
|
|
170
213
|
index.lastUpdated = new Date().toISOString();
|
|
171
214
|
await this.getOps().putObject(this.indexKey(), JSON.stringify(index, null, 2));
|
|
215
|
+
// The write changed the object's ETag; keep the doc, revalidate next read.
|
|
216
|
+
this.cachedIndexDoc = index;
|
|
217
|
+
this.cachedIndexEtag = undefined;
|
|
172
218
|
}
|
|
173
219
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
220
|
+
* Raw index.json read. Returns the body (with its ETag when available),
|
|
221
|
+
* null body when the object is absent, or notModified when a
|
|
222
|
+
* conditional read reported the cached copy unchanged.
|
|
223
|
+
*/
|
|
224
|
+
async readIndexObject(ops) {
|
|
225
|
+
if (ops.getObjectConditional) {
|
|
226
|
+
return ops.getObjectConditional(this.indexKey(), this.cachedIndexDoc ? this.cachedIndexEtag : undefined);
|
|
227
|
+
}
|
|
228
|
+
return { body: await ops.getObject(this.indexKey()) };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Read index.json (ETag-revalidated when the ops support conditional
|
|
232
|
+
* reads — an unchanged index costs a 304, not a download); when missing
|
|
233
|
+
* or unparsable, rebuild it from a listing of the skills/ prefix and
|
|
234
|
+
* persist the rebuilt document (self-heal).
|
|
176
235
|
*/
|
|
177
236
|
async readOrRebuildIndex() {
|
|
178
237
|
const ops = this.getOps();
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
238
|
+
const read = await this.readIndexObject(ops);
|
|
239
|
+
if (read.notModified && this.cachedIndexDoc) {
|
|
240
|
+
return this.cachedIndexDoc;
|
|
241
|
+
}
|
|
242
|
+
if (read.body) {
|
|
181
243
|
try {
|
|
182
|
-
const parsed = JSON.parse(
|
|
244
|
+
const parsed = JSON.parse(read.body);
|
|
183
245
|
if (Array.isArray(parsed.skills)) {
|
|
246
|
+
this.cachedIndexDoc = parsed;
|
|
247
|
+
// Cache the ETag only for a body that parsed: pairing a corrupt
|
|
248
|
+
// object's ETag with the previous good doc would make every
|
|
249
|
+
// later conditional read 304 into permanently stale data.
|
|
250
|
+
this.cachedIndexEtag = read.etag;
|
|
184
251
|
return parsed;
|
|
185
252
|
}
|
|
186
253
|
}
|
|
@@ -189,6 +256,7 @@ export class S3SkillStore {
|
|
|
189
256
|
error: error instanceof Error ? error.message : String(error),
|
|
190
257
|
});
|
|
191
258
|
}
|
|
259
|
+
this.cachedIndexEtag = undefined;
|
|
192
260
|
}
|
|
193
261
|
const skillsPrefix = `${this.prefix}skills/`;
|
|
194
262
|
const keys = await ops.listKeys(skillsPrefix);
|
|
@@ -214,6 +282,10 @@ export class S3SkillStore {
|
|
|
214
282
|
lastUpdated: new Date().toISOString(),
|
|
215
283
|
skills,
|
|
216
284
|
};
|
|
285
|
+
// Cache the rebuilt doc even when persistence below fails, so reads
|
|
286
|
+
// don't fall back to a stale pre-rebuild copy.
|
|
287
|
+
this.cachedIndexDoc = rebuilt;
|
|
288
|
+
this.cachedIndexEtag = undefined;
|
|
217
289
|
try {
|
|
218
290
|
await this.writeIndex(rebuilt);
|
|
219
291
|
logger.info("[SkillStoreS3] Rebuilt skills index", {
|
|
@@ -11,26 +11,40 @@ import type { SkillDefinition, SkillIndexItem, SkillStore, SkillsStorageConfig }
|
|
|
11
11
|
/** In-process store backed by a Map. */
|
|
12
12
|
export declare class InMemorySkillStore implements SkillStore {
|
|
13
13
|
private skills;
|
|
14
|
-
|
|
14
|
+
/** skill id → relative path → content. */
|
|
15
|
+
private resources;
|
|
16
|
+
constructor(seed?: SkillDefinition[], resources?: Record<string, Record<string, string>>);
|
|
15
17
|
get(id: string): Promise<SkillDefinition | null>;
|
|
16
18
|
put(skill: SkillDefinition): Promise<void>;
|
|
17
19
|
delete(id: string): Promise<void>;
|
|
18
20
|
index(): Promise<SkillIndexItem[]>;
|
|
21
|
+
getResource(id: string, resourcePath: string): Promise<string | null>;
|
|
19
22
|
}
|
|
20
23
|
/**
|
|
21
24
|
* Directory-backed store. Read layouts:
|
|
22
25
|
* - `<dir>/<id>.json` — JSON SkillDefinition (mutable)
|
|
23
26
|
* - `<dir>/<name>.md` — frontmatter markdown (read-only source)
|
|
24
|
-
* - `<dir>/<name>/SKILL.md` — Claude-skills directory layout (read-only source)
|
|
27
|
+
* - `<dir>/<name>/SKILL.md` — Claude-skills directory layout (read-only source);
|
|
28
|
+
* sibling files become on-demand resources (read_skill_resource)
|
|
25
29
|
* Mutations always write `<id>.json`; a JSON file shadows a markdown skill
|
|
26
30
|
* with the same id, so updating a markdown-sourced skill "copies up" to JSON.
|
|
27
31
|
*/
|
|
28
32
|
export declare class FileSystemSkillStore implements SkillStore {
|
|
29
33
|
private readonly baseDir;
|
|
30
34
|
private cache;
|
|
35
|
+
/** skill id → directory, for skills loaded from the SKILL.md layout. */
|
|
36
|
+
private skillDirs;
|
|
31
37
|
constructor(baseDir: string);
|
|
32
38
|
invalidate(): void;
|
|
33
39
|
get(id: string): Promise<SkillDefinition | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Resources exist only for directory-layout skills (`<name>/SKILL.md`) —
|
|
42
|
+
* every sibling file of SKILL.md is addressable by its relative path.
|
|
43
|
+
* The REAL path must stay inside the skill directory: lexical
|
|
44
|
+
* containment alone would follow a symlink planted inside the skill dir
|
|
45
|
+
* to anywhere on the host.
|
|
46
|
+
*/
|
|
47
|
+
getResource(id: string, resourcePath: string): Promise<string | null>;
|
|
34
48
|
put(skill: SkillDefinition): Promise<void>;
|
|
35
49
|
delete(id: string): Promise<void>;
|
|
36
50
|
index(): Promise<SkillIndexItem[]>;
|
|
@@ -40,9 +40,23 @@ function isValidSkill(candidate) {
|
|
|
40
40
|
/** In-process store backed by a Map. */
|
|
41
41
|
export class InMemorySkillStore {
|
|
42
42
|
skills = new Map();
|
|
43
|
-
|
|
43
|
+
/** skill id → relative path → content. */
|
|
44
|
+
resources = new Map();
|
|
45
|
+
constructor(seed, resources) {
|
|
46
|
+
for (const [skillId, files] of Object.entries(resources ?? {})) {
|
|
47
|
+
this.resources.set(skillId, new Map(Object.entries(files)));
|
|
48
|
+
}
|
|
44
49
|
for (const skill of seed ?? []) {
|
|
45
|
-
this.
|
|
50
|
+
const files = this.resources.get(skill.id);
|
|
51
|
+
const withRefs = files && !skill.resources
|
|
52
|
+
? {
|
|
53
|
+
...skill,
|
|
54
|
+
resources: Array.from(files.keys())
|
|
55
|
+
.sort()
|
|
56
|
+
.map((p) => ({ path: p })),
|
|
57
|
+
}
|
|
58
|
+
: skill;
|
|
59
|
+
this.skills.set(skill.id, normalizeSkill(withRefs));
|
|
46
60
|
}
|
|
47
61
|
}
|
|
48
62
|
async get(id) {
|
|
@@ -57,6 +71,9 @@ export class InMemorySkillStore {
|
|
|
57
71
|
async index() {
|
|
58
72
|
return Array.from(this.skills.values()).map(toSkillIndexItem);
|
|
59
73
|
}
|
|
74
|
+
async getResource(id, resourcePath) {
|
|
75
|
+
return this.resources.get(id)?.get(resourcePath) ?? null;
|
|
76
|
+
}
|
|
60
77
|
}
|
|
61
78
|
/**
|
|
62
79
|
* Parse a markdown document with optional YAML frontmatter into a skill.
|
|
@@ -112,17 +129,56 @@ function parseSkillMarkdown(raw, fallbackId) {
|
|
|
112
129
|
});
|
|
113
130
|
return candidate;
|
|
114
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* List every file bundled beside a skill's SKILL.md as a resource ref,
|
|
134
|
+
* relative paths, sorted for deterministic listings. Recurses into
|
|
135
|
+
* subdirectories (references/, assets/, …).
|
|
136
|
+
*/
|
|
137
|
+
function listSkillResourceFiles(skillDir) {
|
|
138
|
+
const refs = [];
|
|
139
|
+
const walk = (current, relative) => {
|
|
140
|
+
let entries;
|
|
141
|
+
try {
|
|
142
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
const relPath = relative ? `${relative}/${entry.name}` : entry.name;
|
|
149
|
+
if (entry.isDirectory()) {
|
|
150
|
+
walk(path.join(current, entry.name), relPath);
|
|
151
|
+
}
|
|
152
|
+
else if (relPath !== "SKILL.md") {
|
|
153
|
+
try {
|
|
154
|
+
refs.push({
|
|
155
|
+
path: relPath,
|
|
156
|
+
size: fs.statSync(path.join(current, entry.name)).size,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
refs.push({ path: relPath });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
walk(skillDir, "");
|
|
166
|
+
return refs.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
167
|
+
}
|
|
115
168
|
/**
|
|
116
169
|
* Directory-backed store. Read layouts:
|
|
117
170
|
* - `<dir>/<id>.json` — JSON SkillDefinition (mutable)
|
|
118
171
|
* - `<dir>/<name>.md` — frontmatter markdown (read-only source)
|
|
119
|
-
* - `<dir>/<name>/SKILL.md` — Claude-skills directory layout (read-only source)
|
|
172
|
+
* - `<dir>/<name>/SKILL.md` — Claude-skills directory layout (read-only source);
|
|
173
|
+
* sibling files become on-demand resources (read_skill_resource)
|
|
120
174
|
* Mutations always write `<id>.json`; a JSON file shadows a markdown skill
|
|
121
175
|
* with the same id, so updating a markdown-sourced skill "copies up" to JSON.
|
|
122
176
|
*/
|
|
123
177
|
export class FileSystemSkillStore {
|
|
124
178
|
baseDir;
|
|
125
179
|
cache = null;
|
|
180
|
+
/** skill id → directory, for skills loaded from the SKILL.md layout. */
|
|
181
|
+
skillDirs = new Map();
|
|
126
182
|
constructor(baseDir) {
|
|
127
183
|
this.baseDir = baseDir;
|
|
128
184
|
}
|
|
@@ -133,6 +189,36 @@ export class FileSystemSkillStore {
|
|
|
133
189
|
const all = this.load();
|
|
134
190
|
return all.get(id) ?? null;
|
|
135
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Resources exist only for directory-layout skills (`<name>/SKILL.md`) —
|
|
194
|
+
* every sibling file of SKILL.md is addressable by its relative path.
|
|
195
|
+
* The REAL path must stay inside the skill directory: lexical
|
|
196
|
+
* containment alone would follow a symlink planted inside the skill dir
|
|
197
|
+
* to anywhere on the host.
|
|
198
|
+
*/
|
|
199
|
+
async getResource(id, resourcePath) {
|
|
200
|
+
this.load();
|
|
201
|
+
const dir = this.skillDirs.get(id);
|
|
202
|
+
if (!dir) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const realDir = fs.realpathSync(dir);
|
|
207
|
+
const realResolved = fs.realpathSync(path.resolve(dir, resourcePath));
|
|
208
|
+
if (realResolved !== realDir &&
|
|
209
|
+
!realResolved.startsWith(realDir + path.sep)) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return fs.readFileSync(realResolved, "utf-8");
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
const code = error.code;
|
|
216
|
+
if (code === "ENOENT" || code === "EISDIR" || code === "ELOOP") {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
136
222
|
async put(skill) {
|
|
137
223
|
fs.mkdirSync(this.baseDir, { recursive: true });
|
|
138
224
|
const filePath = path.join(this.baseDir, `${skill.id}.json`);
|
|
@@ -164,6 +250,7 @@ export class FileSystemSkillStore {
|
|
|
164
250
|
return this.cache;
|
|
165
251
|
}
|
|
166
252
|
const skills = new Map();
|
|
253
|
+
this.skillDirs = new Map();
|
|
167
254
|
let entries;
|
|
168
255
|
try {
|
|
169
256
|
entries = fs.readdirSync(this.baseDir, { withFileTypes: true });
|
|
@@ -188,7 +275,9 @@ export class FileSystemSkillStore {
|
|
|
188
275
|
if (fs.existsSync(skillMd)) {
|
|
189
276
|
const parsed = parseSkillMarkdown(fs.readFileSync(skillMd, "utf-8"), entry.name);
|
|
190
277
|
if (parsed) {
|
|
191
|
-
|
|
278
|
+
const resources = listSkillResourceFiles(fullPath);
|
|
279
|
+
skills.set(parsed.id, resources.length > 0 ? { ...parsed, resources } : parsed);
|
|
280
|
+
this.skillDirs.set(parsed.id, path.resolve(fullPath));
|
|
192
281
|
}
|
|
193
282
|
}
|
|
194
283
|
}
|
|
@@ -236,7 +325,7 @@ export class FileSystemSkillStore {
|
|
|
236
325
|
/** Resolve a storage config to a concrete store. Defaults to memory. */
|
|
237
326
|
export function createSkillStore(config) {
|
|
238
327
|
if (!config || config.type === "memory") {
|
|
239
|
-
return new InMemorySkillStore(config?.type === "memory" ? config.skills : undefined);
|
|
328
|
+
return new InMemorySkillStore(config?.type === "memory" ? config.skills : undefined, config?.type === "memory" ? config.resources : undefined);
|
|
240
329
|
}
|
|
241
330
|
if (config.type === "filesystem") {
|
|
242
331
|
return new FileSystemSkillStore(config.path);
|
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in skill tools
|
|
3
|
-
*
|
|
2
|
+
* Built-in skill tools.
|
|
3
|
+
*
|
|
4
|
+
* Two factories, mirroring the two disclosure levels:
|
|
5
|
+
*
|
|
6
|
+
* createSkillTools — instance tools registered once at construction:
|
|
7
|
+
* list_skills (lightweight catalog) plus the gated skill_create /
|
|
8
|
+
* skill_update / skill_delete mutation tools.
|
|
9
|
+
*
|
|
10
|
+
* createSkillCallTools — per-call tools injected into options.tools by
|
|
11
|
+
* prepareGenerate/prepareStream (the RAG-tool pattern): use_skill, whose
|
|
12
|
+
* description embeds the `<available_skills>` listing so the model
|
|
13
|
+
* decides from context when a skill applies (no mandatory pre-flight
|
|
14
|
+
* call), and read_skill_resource for on-demand auxiliary files. The
|
|
15
|
+
* sessionId rides in by closure, so activations pin to the session
|
|
16
|
+
* without runtime tool-context plumbing.
|
|
4
17
|
*
|
|
5
18
|
* The manager is resolved lazily via a resolver callback so tools can be
|
|
6
19
|
* registered at construction time while the store initializes on first
|
|
7
20
|
* 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
21
|
*/
|
|
13
|
-
import type { SkillsManagerLike, SkillToolsOptions, Tool } from "../types/index.js";
|
|
22
|
+
import type { SkillCallToolsContext, SkillsManagerLike, SkillToolsOptions, Tool } from "../types/index.js";
|
|
23
|
+
/**
|
|
24
|
+
* Per-call skill tools: use_skill + read_skill_resource. Injected into
|
|
25
|
+
* options.tools for one generate()/stream() call; same-name entries shadow
|
|
26
|
+
* any registered tool, so the description always reflects this call's
|
|
27
|
+
* listing and scope.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createSkillCallTools(resolveManager: () => SkillsManagerLike | null, context: SkillCallToolsContext): Record<string, Tool>;
|
|
14
30
|
/**
|
|
15
|
-
*
|
|
16
|
-
* Returns Vercel AI SDK tool() objects
|
|
17
|
-
* execute) keyed by tool name.
|
|
31
|
+
* Instance skill tools bound to a lazily-resolved manager: list_skills
|
|
32
|
+
* plus the gated mutation tools. Returns Vercel AI SDK tool() objects
|
|
33
|
+
* (description + Zod inputSchema + execute) keyed by tool name.
|
|
18
34
|
*/
|
|
19
35
|
export declare function createSkillTools(resolveManager: () => SkillsManagerLike | null, options?: SkillToolsOptions): Record<string, Tool>;
|