@vibe-agent-toolkit/resources 0.1.12 → 0.1.14
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/dist/content-transform.d.ts +142 -0
- package/dist/content-transform.d.ts.map +1 -0
- package/dist/content-transform.js +252 -0
- package/dist/content-transform.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/resource-registry.d.ts +50 -20
- package/dist/resource-registry.d.ts.map +1 -1
- package/dist/resource-registry.js +88 -53
- package/dist/resource-registry.js.map +1 -1
- package/package.json +2 -2
- package/src/content-transform.ts +395 -0
- package/src/index.ts +11 -1
- package/src/resource-registry.ts +102 -59
package/src/resource-registry.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type fs from 'node:fs/promises';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
|
|
14
|
-
import { crawlDirectory, type CrawlOptions as UtilsCrawlOptions, type GitTracker, normalizedTmpdir } from '@vibe-agent-toolkit/utils';
|
|
14
|
+
import { crawlDirectory, type CrawlOptions as UtilsCrawlOptions, type GitTracker, normalizedTmpdir, toForwardSlash } from '@vibe-agent-toolkit/utils';
|
|
15
15
|
|
|
16
16
|
import { calculateChecksum } from './checksum.js';
|
|
17
17
|
import { getCollectionsForFile } from './collection-matcher.js';
|
|
@@ -44,8 +44,10 @@ export interface CrawlOptions {
|
|
|
44
44
|
* Options for ResourceRegistry constructor.
|
|
45
45
|
*/
|
|
46
46
|
export interface ResourceRegistryOptions {
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/** Base directory for resources. Used for relative-path ID generation and schema resolution. */
|
|
48
|
+
baseDir?: string;
|
|
49
|
+
/** Frontmatter field name to use as resource ID (optional). When set, the value of this frontmatter field takes priority over path-based ID generation. */
|
|
50
|
+
idField?: string;
|
|
49
51
|
/** Project configuration (optional, enables collection support) */
|
|
50
52
|
config?: ProjectConfig;
|
|
51
53
|
/** Git tracker for efficient git-ignore checking (optional, improves performance) */
|
|
@@ -131,8 +133,11 @@ export interface CollectionStats {
|
|
|
131
133
|
* ```
|
|
132
134
|
*/
|
|
133
135
|
export class ResourceRegistry implements ResourceCollectionInterface {
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
+
/** Base directory for resources. Used for relative-path ID generation and schema resolution. Set via constructor or propagated from crawl(). */
|
|
137
|
+
baseDir?: string;
|
|
138
|
+
|
|
139
|
+
/** Frontmatter field name to use as resource ID. */
|
|
140
|
+
readonly idField?: string;
|
|
136
141
|
|
|
137
142
|
/** Optional project configuration (enables collection support) */
|
|
138
143
|
readonly config?: ProjectConfig;
|
|
@@ -146,8 +151,11 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
146
151
|
private readonly resourcesByChecksum: Map<SHA256, ResourceMetadata[]> = new Map();
|
|
147
152
|
|
|
148
153
|
constructor(options?: ResourceRegistryOptions) {
|
|
149
|
-
if (options?.
|
|
150
|
-
this.
|
|
154
|
+
if (options?.baseDir !== undefined) {
|
|
155
|
+
this.baseDir = options.baseDir;
|
|
156
|
+
}
|
|
157
|
+
if (options?.idField !== undefined) {
|
|
158
|
+
this.idField = options.idField;
|
|
151
159
|
}
|
|
152
160
|
if (options?.config !== undefined) {
|
|
153
161
|
this.config = options.config;
|
|
@@ -158,32 +166,34 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
/**
|
|
161
|
-
* Create an empty registry with a
|
|
169
|
+
* Create an empty registry with a base directory.
|
|
162
170
|
*
|
|
163
|
-
* @param
|
|
171
|
+
* @param baseDir - Base directory for resources
|
|
164
172
|
* @param options - Additional options
|
|
165
173
|
* @returns New empty registry
|
|
166
174
|
*
|
|
167
175
|
* @example
|
|
168
176
|
* ```typescript
|
|
169
177
|
* const registry = ResourceRegistry.empty('/project/docs');
|
|
170
|
-
* console.log(registry.
|
|
178
|
+
* console.log(registry.baseDir); // '/project/docs'
|
|
171
179
|
* console.log(registry.size()); // 0
|
|
172
180
|
* ```
|
|
173
181
|
*/
|
|
174
|
-
static empty(
|
|
175
|
-
return new ResourceRegistry({ ...options,
|
|
182
|
+
static empty(baseDir: string, options?: Omit<ResourceRegistryOptions, 'baseDir'>): ResourceRegistry {
|
|
183
|
+
return new ResourceRegistry({ ...options, baseDir });
|
|
176
184
|
}
|
|
177
185
|
|
|
178
186
|
/**
|
|
179
187
|
* Create a registry from an existing array of resources.
|
|
180
188
|
*
|
|
181
189
|
* Initializes all indexes (by path, ID, name, checksum) from the provided resources.
|
|
190
|
+
* Throws if any resources have duplicate IDs.
|
|
182
191
|
*
|
|
183
|
-
* @param
|
|
192
|
+
* @param baseDir - Base directory for resources
|
|
184
193
|
* @param resources - Array of resource metadata
|
|
185
194
|
* @param options - Additional options
|
|
186
195
|
* @returns New registry with resources
|
|
196
|
+
* @throws Error if duplicate resource IDs are found
|
|
187
197
|
*
|
|
188
198
|
* @example
|
|
189
199
|
* ```typescript
|
|
@@ -193,14 +203,22 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
193
203
|
* ```
|
|
194
204
|
*/
|
|
195
205
|
static fromResources(
|
|
196
|
-
|
|
206
|
+
baseDir: string,
|
|
197
207
|
resources: ResourceMetadata[],
|
|
198
|
-
options?: Omit<ResourceRegistryOptions, '
|
|
208
|
+
options?: Omit<ResourceRegistryOptions, 'baseDir'>,
|
|
199
209
|
): ResourceRegistry {
|
|
200
|
-
const registry = new ResourceRegistry({ ...options,
|
|
210
|
+
const registry = new ResourceRegistry({ ...options, baseDir });
|
|
201
211
|
|
|
202
212
|
// Add all resources to indexes
|
|
203
213
|
for (const resource of resources) {
|
|
214
|
+
// Check for duplicate ID
|
|
215
|
+
const existingById = registry.resourcesById.get(resource.id);
|
|
216
|
+
if (existingById) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Duplicate resource ID '${resource.id}': '${resource.filePath}' conflicts with '${existingById.filePath}'`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
204
222
|
// Add to path index
|
|
205
223
|
registry.resourcesByPath.set(resource.filePath, resource);
|
|
206
224
|
|
|
@@ -241,9 +259,9 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
241
259
|
*/
|
|
242
260
|
static async fromCrawl(
|
|
243
261
|
crawlOptions: CrawlOptions,
|
|
244
|
-
registryOptions?: Omit<ResourceRegistryOptions, '
|
|
262
|
+
registryOptions?: Omit<ResourceRegistryOptions, 'baseDir'>,
|
|
245
263
|
): Promise<ResourceRegistry> {
|
|
246
|
-
const registry = new ResourceRegistry({ ...registryOptions,
|
|
264
|
+
const registry = new ResourceRegistry({ ...registryOptions, baseDir: crawlOptions.baseDir });
|
|
247
265
|
await registry.crawl(crawlOptions);
|
|
248
266
|
return registry;
|
|
249
267
|
}
|
|
@@ -267,11 +285,19 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
267
285
|
// Normalize path to absolute
|
|
268
286
|
const absolutePath = path.resolve(filePath);
|
|
269
287
|
|
|
270
|
-
// Parse the markdown file
|
|
288
|
+
// Parse the markdown file (needed before ID generation for frontmatter lookup)
|
|
271
289
|
const parseResult = await parseMarkdown(absolutePath);
|
|
272
290
|
|
|
273
|
-
// Generate
|
|
274
|
-
const id = this.
|
|
291
|
+
// Generate ID using priority chain: frontmatter field → relative path → filename stem
|
|
292
|
+
const id = this.generateId(absolutePath, parseResult.frontmatter);
|
|
293
|
+
|
|
294
|
+
// Check for duplicate ID (allow re-adding same file path)
|
|
295
|
+
const existingById = this.resourcesById.get(id);
|
|
296
|
+
if (existingById && existingById.filePath !== absolutePath) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
`Duplicate resource ID '${id}': '${absolutePath}' conflicts with '${existingById.filePath}'`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
275
301
|
|
|
276
302
|
// Get file modified time
|
|
277
303
|
const fs = await import('node:fs/promises');
|
|
@@ -307,10 +333,13 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
307
333
|
}
|
|
308
334
|
|
|
309
335
|
/**
|
|
310
|
-
* Add multiple resources to the registry
|
|
336
|
+
* Add multiple resources to the registry sequentially.
|
|
337
|
+
*
|
|
338
|
+
* Sequential execution ensures deterministic duplicate ID detection.
|
|
311
339
|
*
|
|
312
340
|
* @param filePaths - Array of file paths to add
|
|
313
341
|
* @returns Array of parsed resource metadata
|
|
342
|
+
* @throws Error if any resource produces a duplicate ID
|
|
314
343
|
*
|
|
315
344
|
* @example
|
|
316
345
|
* ```typescript
|
|
@@ -322,7 +351,11 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
322
351
|
* ```
|
|
323
352
|
*/
|
|
324
353
|
async addResources(filePaths: string[]): Promise<ResourceMetadata[]> {
|
|
325
|
-
|
|
354
|
+
const results: ResourceMetadata[] = [];
|
|
355
|
+
for (const fp of filePaths) {
|
|
356
|
+
results.push(await this.addResource(fp));
|
|
357
|
+
}
|
|
358
|
+
return results;
|
|
326
359
|
}
|
|
327
360
|
|
|
328
361
|
/**
|
|
@@ -349,6 +382,11 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
349
382
|
followSymlinks = false,
|
|
350
383
|
} = options;
|
|
351
384
|
|
|
385
|
+
// Propagate baseDir to registry if not already set (enables path-relative IDs)
|
|
386
|
+
if (baseDir && !this.baseDir) {
|
|
387
|
+
this.baseDir = baseDir;
|
|
388
|
+
}
|
|
389
|
+
|
|
352
390
|
// Use utils file crawler
|
|
353
391
|
const crawlOptions: UtilsCrawlOptions = {
|
|
354
392
|
baseDir,
|
|
@@ -398,10 +436,10 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
398
436
|
for (const resource of this.resourcesByPath.values()) {
|
|
399
437
|
for (const link of resource.links) {
|
|
400
438
|
// Only pass options if projectRoot is defined (exactOptionalPropertyTypes requirement)
|
|
401
|
-
const validateOptions = this.
|
|
439
|
+
const validateOptions = this.baseDir === undefined
|
|
402
440
|
? { skipGitIgnoreCheck }
|
|
403
441
|
: {
|
|
404
|
-
projectRoot: this.
|
|
442
|
+
projectRoot: this.baseDir,
|
|
405
443
|
skipGitIgnoreCheck,
|
|
406
444
|
...(this.gitTracker !== undefined && { gitTracker: this.gitTracker })
|
|
407
445
|
};
|
|
@@ -515,7 +553,7 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
515
553
|
}
|
|
516
554
|
|
|
517
555
|
const schemaPath = path.resolve(
|
|
518
|
-
this.
|
|
556
|
+
this.baseDir ?? process.cwd(),
|
|
519
557
|
validation.frontmatterSchema
|
|
520
558
|
);
|
|
521
559
|
|
|
@@ -1116,31 +1154,23 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
1116
1154
|
}
|
|
1117
1155
|
|
|
1118
1156
|
/**
|
|
1119
|
-
* Generate a
|
|
1120
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
* 2. Convert to kebab-case
|
|
1124
|
-
* 3. Handle collisions by appending suffix (-2, -3, etc.)
|
|
1157
|
+
* Generate a resource ID using the priority chain:
|
|
1158
|
+
* 1. Frontmatter field (if `idField` is configured and field exists)
|
|
1159
|
+
* 2. Relative path from `baseDir` (if `baseDir` is set)
|
|
1160
|
+
* 3. Filename stem (fallback)
|
|
1125
1161
|
*
|
|
1126
1162
|
* @param filePath - Absolute file path
|
|
1127
|
-
* @
|
|
1163
|
+
* @param frontmatter - Parsed frontmatter (optional)
|
|
1164
|
+
* @returns Resource ID
|
|
1128
1165
|
*/
|
|
1129
|
-
private
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
if (!this.resourcesById.has(baseId)) {
|
|
1134
|
-
return baseId;
|
|
1166
|
+
private generateId(filePath: string, frontmatter?: Record<string, unknown>): string {
|
|
1167
|
+
// Priority 1: Frontmatter field
|
|
1168
|
+
if (this.idField && frontmatter?.[this.idField] !== undefined) {
|
|
1169
|
+
return String(frontmatter[this.idField]);
|
|
1135
1170
|
}
|
|
1136
1171
|
|
|
1137
|
-
//
|
|
1138
|
-
|
|
1139
|
-
while (this.resourcesById.has(`${baseId}-${suffix}`)) {
|
|
1140
|
-
suffix++;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
return `${baseId}-${suffix}`;
|
|
1172
|
+
// Priority 2/3: Path-based (relative to baseDir, or filename stem)
|
|
1173
|
+
return generateIdFromPath(filePath, this.baseDir);
|
|
1144
1174
|
}
|
|
1145
1175
|
|
|
1146
1176
|
/**
|
|
@@ -1212,32 +1242,45 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
1212
1242
|
/**
|
|
1213
1243
|
* Generate an ID from a file path.
|
|
1214
1244
|
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
* 2. Get basename
|
|
1218
|
-
* 3. Convert to kebab-case
|
|
1219
|
-
* 4. Remove non-alphanumeric characters except hyphens
|
|
1245
|
+
* When `baseDir` is provided, computes a relative path from baseDir and uses the full
|
|
1246
|
+
* directory structure in the ID. When no `baseDir`, uses the filename stem only.
|
|
1220
1247
|
*
|
|
1221
|
-
* @param filePath -
|
|
1222
|
-
* @
|
|
1248
|
+
* @param filePath - Absolute file path
|
|
1249
|
+
* @param baseDir - Base directory for relative path computation (optional)
|
|
1250
|
+
* @returns Generated ID in kebab-case
|
|
1223
1251
|
*
|
|
1224
1252
|
* @example
|
|
1225
1253
|
* ```typescript
|
|
1254
|
+
* // Without baseDir: filename stem only
|
|
1226
1255
|
* generateIdFromPath('/project/docs/User Guide.md') // 'user-guide'
|
|
1227
|
-
* generateIdFromPath('/project/README.md')
|
|
1228
|
-
*
|
|
1256
|
+
* generateIdFromPath('/project/README.md') // 'readme'
|
|
1257
|
+
*
|
|
1258
|
+
* // With baseDir: relative path
|
|
1259
|
+
* generateIdFromPath('/project/docs/concepts/core/overview.md', '/project/docs') // 'concepts-core-overview'
|
|
1260
|
+
* generateIdFromPath('/project/docs/guide.md', '/project/docs') // 'guide'
|
|
1229
1261
|
* ```
|
|
1230
1262
|
*/
|
|
1231
|
-
function generateIdFromPath(filePath: string): string {
|
|
1232
|
-
|
|
1233
|
-
|
|
1263
|
+
export function generateIdFromPath(filePath: string, baseDir?: string): string {
|
|
1264
|
+
let rawId: string;
|
|
1265
|
+
|
|
1266
|
+
if (baseDir) {
|
|
1267
|
+
// Compute relative path from baseDir, remove extension
|
|
1268
|
+
const relativePath = path.relative(baseDir, filePath);
|
|
1269
|
+
const ext = path.extname(relativePath);
|
|
1270
|
+
const withoutExt = ext ? relativePath.slice(0, -ext.length) : relativePath;
|
|
1271
|
+
// Normalize path separators to forward slashes (cross-platform), then replace with hyphens
|
|
1272
|
+
rawId = toForwardSlash(withoutExt).replaceAll('/', '-');
|
|
1273
|
+
} else {
|
|
1274
|
+
// Fallback: basename only (no directory context)
|
|
1275
|
+
rawId = path.basename(filePath, path.extname(filePath));
|
|
1276
|
+
}
|
|
1234
1277
|
|
|
1235
1278
|
// Convert to kebab-case:
|
|
1236
1279
|
// 1. Replace underscores and spaces with hyphens
|
|
1237
1280
|
// 2. Convert to lowercase
|
|
1238
1281
|
// 3. Remove non-alphanumeric except hyphens
|
|
1239
1282
|
// 4. Collapse multiple hyphens
|
|
1240
|
-
return
|
|
1283
|
+
return rawId
|
|
1241
1284
|
.replaceAll(/[_\s]+/g, '-')
|
|
1242
1285
|
.toLowerCase()
|
|
1243
1286
|
.replaceAll(/[^\da-z-]/g, '')
|