@totemsdk/mcp-server 0.2.0 → 0.3.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/dist/indexer.js CHANGED
@@ -33,53 +33,93 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.IndexUnavailableError = void 0;
37
+ exports.buildFromMonorepo = buildFromMonorepo;
36
38
  exports.buildIndex = buildIndex;
37
39
  exports.readSourceFile = readSourceFile;
38
40
  const fs = __importStar(require("fs"));
39
41
  const path = __importStar(require("path"));
40
- function findRepoRoot() {
42
+ /**
43
+ * Thrown when no SDK index can be produced — neither a monorepo to scan nor a
44
+ * bundled index shipped in the package. Callers must surface this as an error,
45
+ * never as an empty (and therefore confidently-wrong) index.
46
+ */
47
+ class IndexUnavailableError extends Error {
48
+ constructor(message) {
49
+ super(message);
50
+ this.name = 'IndexUnavailableError';
51
+ }
52
+ }
53
+ exports.IndexUnavailableError = IndexUnavailableError;
54
+ /**
55
+ * Prebuilt index shipped in the published tarball (`data/sdk-index.json`), so
56
+ * the server works outside the monorepo. Generated by
57
+ * `scripts/build-mcp-bundle.mjs` at pack time.
58
+ */
59
+ const BUNDLED_INDEX_PATH = path.join(__dirname, '..', 'data', 'sdk-index.json');
60
+ /** A pnpm-workspace dir is only the SDK root if it actually contains the SDK. */
61
+ function isSdkRoot(dir) {
62
+ return (fs.existsSync(path.join(dir, 'SDK_MANIFEST.json')) ||
63
+ fs.existsSync(path.join(dir, 'packages', 'core', 'package.json')));
64
+ }
65
+ /** Walk up from `__dirname` for a *valid* SDK root; `null` when not found. */
66
+ function detectRepoRoot() {
41
67
  let dir = path.resolve(__dirname);
42
68
  while (dir !== path.dirname(dir)) {
43
- if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml')))
69
+ if (fs.existsSync(path.join(dir, 'pnpm-workspace.yaml')) && isSdkRoot(dir))
44
70
  return dir;
45
71
  dir = path.dirname(dir);
46
72
  }
47
- return dir;
48
- }
49
- const REPO_ROOT = findRepoRoot();
50
- const PKG_DIR = path.join(REPO_ROOT, 'packages', 'totem-sdk', 'packages');
51
- const TOP_PKG_DIR = path.join(REPO_ROOT, 'packages');
52
- const DOMAIN_GROUPS = [
53
- [/^core-wasm$|^core$|^txpow$|^kissvm$/, 'core/crypto'],
54
- [/^identity$|^root-identity$|^authority$|^proof$|^proof-integritas$|^proofgraph$|^manifest$/, 'identity/authority'],
55
- [/^governance$|^recursive-mast$/, 'governance'],
56
- [/^edge$|^edge-adapters$|^industrial-action$|^agent-policy$/, 'edge/runtime'],
57
- [/^edge-(bacnet|ble|can|coap|email|grpc|lorawan|matter|modbus|mqtt|opcua|ros2)$/, 'edge/protocols'],
58
- [/^omnia(-factory|-router|-splice|-vtxo|-host)?$/, 'blockchain/omnia'],
59
- [/^statechain$|^tx-builder$|^chain-provider$|^wots-lease$|^liquidity-bond$|^provider-bond$/, 'blockchain/infra'],
60
- [/^lookup-(client|node|protocol)$/, 'lookup/p2p'],
61
- [/^stream-transport$|^pubsub-transport$|^minima-rpc$|^server$|^se-server$|^realtime$|^connect$|^wallet-adapter$|^pear$|^mcp-server$|^observability$/, 'utilities'],
62
- [/^sdk-tests$/, 'testing'],
63
- ];
64
- function classifyDomain(dirName) {
65
- for (const [pattern, domain] of DOMAIN_GROUPS) {
66
- if (pattern.test(dirName))
67
- return domain;
68
- }
69
- return 'other';
70
- }
71
- function findPackageDirs() {
73
+ return null;
74
+ }
75
+ function loadRepoData(root) {
76
+ const topPkgDir = path.join(root, 'packages');
77
+ const manifest = new Map();
78
+ try {
79
+ const parsed = JSON.parse(fs.readFileSync(path.join(root, 'SDK_MANIFEST.json'), 'utf-8'));
80
+ for (const pkg of parsed.packages ?? []) {
81
+ if (pkg && typeof pkg.name === 'string') {
82
+ manifest.set(pkg.name, { version: pkg.version, description: pkg.description, domain: pkg.domain });
83
+ }
84
+ }
85
+ }
86
+ catch {
87
+ // Manifest missing/unreadable — fall back to filesystem discovery only.
88
+ }
89
+ const ast = new Map();
90
+ try {
91
+ const parsed = JSON.parse(fs.readFileSync(path.join(root, 'scripts', 'mcp-symbols.ast.json'), 'utf-8'));
92
+ for (const [name, pkg] of Object.entries(parsed.packages ?? {})) {
93
+ if (pkg?.symbols)
94
+ ast.set(name, pkg.symbols);
95
+ }
96
+ }
97
+ catch {
98
+ // Optional artifact — fall back to the regex scan.
99
+ }
100
+ return { topPkgDir, manifest, ast };
101
+ }
102
+ function loadBundledIndex() {
103
+ try {
104
+ const idx = JSON.parse(fs.readFileSync(BUNDLED_INDEX_PATH, 'utf-8'));
105
+ if (idx && typeof idx === 'object' && idx.packages && typeof idx.packages === 'object')
106
+ return idx;
107
+ return null;
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ function findPackageDirs(TOP_PKG_DIR) {
72
114
  const dirs = [];
73
- for (const base of [PKG_DIR, TOP_PKG_DIR]) {
74
- if (!fs.existsSync(base))
115
+ if (!fs.existsSync(TOP_PKG_DIR))
116
+ return dirs;
117
+ for (const entry of fs.readdirSync(TOP_PKG_DIR, { withFileTypes: true })) {
118
+ if (!entry.isDirectory())
75
119
  continue;
76
- for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
77
- if (!entry.isDirectory())
78
- continue;
79
- const pj = path.join(base, entry.name, 'package.json');
80
- if (fs.existsSync(pj))
81
- dirs.push(path.join(base, entry.name));
82
- }
120
+ const pj = path.join(TOP_PKG_DIR, entry.name, 'package.json');
121
+ if (fs.existsSync(pj))
122
+ dirs.push(path.join(TOP_PKG_DIR, entry.name));
83
123
  }
84
124
  return dirs.sort();
85
125
  }
@@ -96,66 +136,169 @@ function hasSubdir(dir, name) {
96
136
  return fs.existsSync(d) && fs.statSync(d).isDirectory();
97
137
  }
98
138
  function hasTestDir(dir) {
99
- const srcTest = path.join(dir, 'src', '__tests__');
100
- const srcTestAlt = path.join(dir, 'src', 'test');
101
- const testDir = path.join(dir, 'test');
102
- const testsDir = path.join(dir, 'tests');
103
- return [srcTest, srcTestAlt, testDir, testsDir].some(d => fs.existsSync(d));
104
- }
105
- function parseExports(dir, pkgName) {
106
- const indexFiles = [
107
- path.join(dir, 'src', 'index.ts'),
108
- path.join(dir, 'src', 'index.js'),
139
+ const candidates = [
140
+ path.join(dir, 'src', '__tests__'),
141
+ path.join(dir, 'src', 'test'),
142
+ path.join(dir, 'test'),
143
+ path.join(dir, 'tests'),
109
144
  ];
110
- let content = '';
111
- for (const f of indexFiles) {
112
- if (fs.existsSync(f)) {
113
- content = fs.readFileSync(f, 'utf-8');
145
+ return candidates.some(d => fs.existsSync(d));
146
+ }
147
+ function resolveModule(fromFile, spec) {
148
+ if (!spec.startsWith('.'))
149
+ return null;
150
+ const base = path.resolve(path.dirname(fromFile), spec.replace(/\.js$/, ''));
151
+ const candidates = [`${base}.ts`, `${base}.tsx`, path.join(base, 'index.ts')];
152
+ for (const c of candidates) {
153
+ if (fs.existsSync(c) && fs.statSync(c).isFile())
154
+ return c;
155
+ }
156
+ return null;
157
+ }
158
+ /** Leading JSDoc (`/** … *​/`) or contiguous `//` comment above a declaration. */
159
+ function leadingDoc(lines, lineIndex) {
160
+ let j = lineIndex - 1;
161
+ while (j >= 0 && lines[j].trim() === '')
162
+ j--;
163
+ if (j < 0)
164
+ return {};
165
+ const t = lines[j].trim();
166
+ if (t.endsWith('*/')) {
167
+ const parts = [];
168
+ let k = j;
169
+ while (k >= 0) {
170
+ const line = lines[k].trim();
171
+ parts.unshift(line);
172
+ if (line.startsWith('/**') || line.startsWith('/*'))
173
+ break;
174
+ k--;
175
+ }
176
+ const raw = parts
177
+ .join('\n')
178
+ .replace(/^\/\*\*?/, '')
179
+ .replace(/\*\/$/, '')
180
+ .split('\n')
181
+ .map(l => l.replace(/^\s*\* ?/, ''))
182
+ .join('\n')
183
+ .trim();
184
+ const deprecated = /@deprecated/.test(raw);
185
+ const stripped = raw.replace(/@\w+[^\n]*/g, '').replace(/\n{2,}/g, '\n').trim();
186
+ return { doc: stripped || raw, ...(deprecated ? { deprecated } : {}) };
187
+ }
188
+ if (t.startsWith('//')) {
189
+ const parts = [];
190
+ let k = j;
191
+ while (k >= 0 && lines[k].trim().startsWith('//')) {
192
+ parts.unshift(lines[k].trim().replace(/^\/\/ ?/, ''));
193
+ k--;
194
+ }
195
+ const raw = parts.join('\n').trim();
196
+ return { doc: raw, ...(/@deprecated/.test(raw) ? { deprecated: true } : {}) };
197
+ }
198
+ return {};
199
+ }
200
+ /** Best-effort declaration text up to the body/terminator. */
201
+ function captureSignature(lines, startIdx, stopAtBrace) {
202
+ let text = '';
203
+ for (let j = startIdx; j < Math.min(lines.length, startIdx + 8); j++) {
204
+ text += (j > startIdx ? ' ' : '') + lines[j].trim();
205
+ const brace = text.indexOf('{');
206
+ const semi = text.indexOf(';');
207
+ let cut = -1;
208
+ if (stopAtBrace) {
209
+ if (brace >= 0)
210
+ cut = brace;
211
+ else if (semi >= 0)
212
+ cut = semi;
213
+ }
214
+ else {
215
+ if (semi >= 0)
216
+ cut = semi;
217
+ else if (brace >= 0)
218
+ cut = brace;
219
+ }
220
+ if (cut >= 0) {
221
+ text = text.slice(0, cut);
114
222
  break;
115
223
  }
116
224
  }
117
- const exports = { functions: [], types: [], classes: [], interfaces: [], consts: [] };
118
- if (!content)
119
- return exports;
225
+ return text.replace(/\s+/g, ' ').replace(/^export\s+/, '').replace(/[,\s]+$/, '').trim();
226
+ }
227
+ const KIND_TO_ARRAY = {
228
+ function: 'functions',
229
+ type: 'types',
230
+ class: 'classes',
231
+ interface: 'interfaces',
232
+ const: 'consts',
233
+ variable: 'consts',
234
+ };
235
+ function setSymbol(symbols, name, meta) {
236
+ if (name && !name.endsWith('}') && !symbols.has(name))
237
+ symbols.set(name, meta);
238
+ }
239
+ function collectExports(file, symbols, visited) {
240
+ if (visited.has(file))
241
+ return;
242
+ visited.add(file);
243
+ let content;
244
+ try {
245
+ content = fs.readFileSync(file, 'utf-8');
246
+ }
247
+ catch {
248
+ return;
249
+ }
120
250
  const lines = content.split('\n');
121
251
  let i = 0;
122
252
  while (i < lines.length) {
123
253
  const trimmed = lines[i].trim();
124
- const fnMatch = trimmed.match(/^export (async )?function (\w+)/);
125
- if (fnMatch) {
126
- exports.functions.push(fnMatch[2]);
254
+ const starAsMatch = trimmed.match(/^export \* as (\w+) from ['"]([^'"]+)['"]/);
255
+ if (starAsMatch) {
256
+ setSymbol(symbols, starAsMatch[1], { kind: 'type', ...leadingDoc(lines, i) });
257
+ const target = resolveModule(file, starAsMatch[2]);
258
+ if (target)
259
+ collectExports(target, symbols, visited);
127
260
  i++;
128
261
  continue;
129
262
  }
130
- const typeMatch = trimmed.match(/^export type \{ ([^}]+) \}/);
131
- if (typeMatch) {
132
- typeMatch[1].split(',').map(s => s.trim()).filter(Boolean).forEach(s => {
133
- const name = s.split(/\s+as\s+/).pop()?.trim() || s;
134
- if (name)
135
- exports.types.push(name);
136
- });
263
+ const starMatch = trimmed.match(/^export \* from ['"]([^'"]+)['"]/);
264
+ if (starMatch) {
265
+ const target = resolveModule(file, starMatch[1]);
266
+ if (target)
267
+ collectExports(target, symbols, visited);
137
268
  i++;
138
269
  continue;
139
270
  }
140
- const clsMatch = trimmed.match(/^export class (\w+)/);
271
+ const fnMatch = trimmed.match(/^export (async )?function\*? (\w+)/);
272
+ if (fnMatch) {
273
+ setSymbol(symbols, fnMatch[2], { kind: 'function', signature: captureSignature(lines, i, true), ...leadingDoc(lines, i) });
274
+ i++;
275
+ continue;
276
+ }
277
+ const clsMatch = trimmed.match(/^export (abstract )?class (\w+)/);
141
278
  if (clsMatch) {
142
- exports.classes.push(clsMatch[1]);
279
+ setSymbol(symbols, clsMatch[2], { kind: 'class', signature: captureSignature(lines, i, true), ...leadingDoc(lines, i) });
143
280
  i++;
144
281
  continue;
145
282
  }
146
283
  const ifaceMatch = trimmed.match(/^export interface (\w+)/);
147
284
  if (ifaceMatch) {
148
- exports.interfaces.push(ifaceMatch[1]);
285
+ setSymbol(symbols, ifaceMatch[1], { kind: 'interface', signature: captureSignature(lines, i, true), ...leadingDoc(lines, i) });
149
286
  i++;
150
287
  continue;
151
288
  }
152
289
  const constMatch = trimmed.match(/^export (const|let|var) (\w+)/);
153
290
  if (constMatch) {
154
- exports.consts.push(constMatch[2]);
291
+ setSymbol(symbols, constMatch[2], { kind: 'const', signature: captureSignature(lines, i, false), ...leadingDoc(lines, i) });
292
+ i++;
293
+ continue;
294
+ }
295
+ const typeMatch = trimmed.match(/^export type (\w+)\s*[=<]/);
296
+ if (typeMatch) {
297
+ setSymbol(symbols, typeMatch[1], { kind: 'type', signature: captureSignature(lines, i, false), ...leadingDoc(lines, i) });
155
298
  i++;
156
299
  continue;
157
300
  }
158
- // Handle multi-line export { ... } from '...' blocks
301
+ // Multi-line `export { ... }` / `export type { ... }` (inline or re-export).
159
302
  if (trimmed.startsWith('export {') || trimmed.startsWith('export type {')) {
160
303
  const isType = trimmed.startsWith('export type {');
161
304
  let block = trimmed;
@@ -164,22 +307,14 @@ function parseExports(dir, pkgName) {
164
307
  j++;
165
308
  block += ' ' + lines[j].trim();
166
309
  }
167
- // Extract name list between { and }
168
310
  const braceMatch = block.match(/\{([^}]+)\}/);
169
311
  if (braceMatch) {
170
312
  for (const part of braceMatch[1].split(',')) {
171
313
  const name = part.trim().split(/\s+as\s+/).pop()?.trim() || '';
172
- if (!name || name.endsWith('}'))
314
+ if (!name)
173
315
  continue;
174
- if (isType) {
175
- exports.types.push(name);
176
- }
177
- else {
178
- if (/^[A-Z]/.test(name))
179
- exports.types.push(name);
180
- else
181
- exports.functions.push(name);
182
- }
316
+ const kind = isType || /^[A-Z]/.test(name) ? 'type' : 'function';
317
+ setSymbol(symbols, name, { kind });
183
318
  }
184
319
  }
185
320
  i = j + 1;
@@ -187,67 +322,142 @@ function parseExports(dir, pkgName) {
187
322
  }
188
323
  i++;
189
324
  }
325
+ }
326
+ function parseExports(dir) {
327
+ const symbols = new Map();
328
+ const candidates = [path.join(dir, 'src', 'index.ts'), path.join(dir, 'src', 'index.js')];
329
+ const entry = candidates.find(f => fs.existsSync(f));
330
+ if (entry)
331
+ collectExports(entry, symbols, new Set());
332
+ return symbols;
333
+ }
334
+ function exportsFromSymbols(symbols) {
335
+ const exports = { functions: [], types: [], classes: [], interfaces: [], consts: [] };
336
+ for (const [name, meta] of symbols)
337
+ exports[KIND_TO_ARRAY[meta.kind]].push(name);
190
338
  return exports;
191
339
  }
192
- function buildIndex() {
340
+ /** Overlay exact AST metadata (authoritative) onto the regex-scanned symbols. */
341
+ function mergeAst(symbols, ast) {
342
+ if (!ast)
343
+ return;
344
+ for (const [name, meta] of Object.entries(ast)) {
345
+ const existing = symbols.get(name);
346
+ symbols.set(name, { ...existing, ...meta, kind: meta.kind });
347
+ }
348
+ }
349
+ /** Scan a monorepo root into an `SdkIndex`. */
350
+ function buildFromMonorepo(root, now = Date.now()) {
351
+ const { topPkgDir, manifest: MANIFEST, ast: AST } = loadRepoData(root);
193
352
  const packages = {};
194
353
  const symbolIndex = {};
195
354
  const domainMap = {};
196
- for (const dir of findPackageDirs()) {
355
+ for (const dir of findPackageDirs(topPkgDir)) {
197
356
  const pkg = readPackageJson(dir);
198
357
  if (!pkg || !pkg.name)
199
358
  continue;
359
+ const manifest = MANIFEST.get(pkg.name);
360
+ if (MANIFEST.size > 0 && !manifest)
361
+ continue;
200
362
  const dirName = path.basename(dir);
201
- const deps = Object.keys(pkg.dependencies || {});
202
- const devDeps = Object.keys(pkg.devDependencies || {});
203
- const hasRust = hasSubdir(dir, 'rust') || hasSubdir(dir, 'rust-toolchain');
204
- const hasGo = hasSubdir(dir, 'go');
205
- const hasTests = hasTestDir(dir);
206
- const exports = parseExports(dir, pkg.name);
207
- const domain = classifyDomain(dirName);
363
+ const symbols = parseExports(dir);
364
+ mergeAst(symbols, AST.get(pkg.name));
365
+ const exports = exportsFromSymbols(symbols);
366
+ const domain = manifest?.domain || 'other';
208
367
  const idx = {
209
368
  name: pkg.name,
210
369
  dir: dirName,
211
- version: pkg.version || '0.0.0',
212
- description: pkg.description || '',
213
- dependencies: deps,
214
- devDependencies: devDeps,
215
- hasRust,
216
- hasGo,
217
- hasTests,
370
+ version: manifest?.version || pkg.version || '0.0.0',
371
+ description: manifest?.description || pkg.description || '',
372
+ ...(Array.isArray(pkg.keywords) ? { keywords: pkg.keywords } : {}),
373
+ dependencies: Object.keys(pkg.dependencies || {}),
374
+ devDependencies: Object.keys(pkg.devDependencies || {}),
375
+ hasRust: hasSubdir(dir, 'rust') || hasSubdir(dir, 'rust-toolchain'),
376
+ hasGo: hasSubdir(dir, 'go'),
377
+ hasTests: hasTestDir(dir),
218
378
  exports,
379
+ symbols: Object.fromEntries(symbols),
219
380
  domain,
220
381
  };
221
382
  packages[pkg.name] = idx;
222
383
  if (!domainMap[domain])
223
384
  domainMap[domain] = [];
224
385
  domainMap[domain].push(pkg.name);
225
- const allExports = [];
226
- for (const n of exports.functions)
227
- allExports.push([n, 'function']);
228
- for (const n of exports.types)
229
- allExports.push([n, 'type']);
230
- for (const n of exports.classes)
231
- allExports.push([n, 'class']);
232
- for (const n of exports.interfaces)
233
- allExports.push([n, 'interface']);
234
- for (const n of exports.consts)
235
- allExports.push([n, 'const']);
236
- for (const [name, kind] of allExports) {
237
- if (!symbolIndex[name])
238
- symbolIndex[name] = [];
239
- symbolIndex[name].push({ package: pkg.name, kind });
240
- }
241
- }
242
- return { generatedAt: Date.now(), packages, symbolIndex, domainMap };
386
+ for (const [name, meta] of symbols) {
387
+ (symbolIndex[name] ??= []).push({
388
+ package: pkg.name,
389
+ kind: meta.kind,
390
+ ...(meta.signature !== undefined ? { signature: meta.signature } : {}),
391
+ ...(meta.doc !== undefined ? { doc: meta.doc } : {}),
392
+ ...(meta.deprecated ? { deprecated: true } : {}),
393
+ });
394
+ }
395
+ }
396
+ return { generatedAt: now, packages, symbolIndex, domainMap };
243
397
  }
244
- function readSourceFile(pkgName, filePath) {
245
- const relativePath = path.join('packages', 'totem-sdk', 'packages', pkgName, 'src', filePath);
246
- const absPath = path.join(REPO_ROOT, relativePath);
247
- const altPath = path.join(REPO_ROOT, 'packages', pkgName, 'src', filePath);
248
- for (const p of [absPath, altPath]) {
249
- if (fs.existsSync(p))
250
- return fs.readFileSync(p, 'utf-8');
398
+ /**
399
+ * Build the SDK index. Prefers a live monorepo scan; falls back to the bundled
400
+ * index shipped in the package. Throws {@link IndexUnavailableError} when
401
+ * neither is available — an empty index is never returned, because downstream
402
+ * tools would otherwise answer "not found" as if it were fact.
403
+ */
404
+ function buildIndex(options = {}) {
405
+ const root = options.rootDir === undefined ? detectRepoRoot() : options.rootDir;
406
+ if (root)
407
+ return buildFromMonorepo(root);
408
+ if (!options.noBundled) {
409
+ const bundled = loadBundledIndex();
410
+ if (bundled)
411
+ return bundled;
412
+ }
413
+ throw new IndexUnavailableError('@totemsdk/mcp-server: SDK index unavailable — not running inside the Totem SDK '
414
+ + 'monorepo and no bundled index was shipped. Reinstall @totemsdk/mcp-server '
415
+ + '(expected data/sdk-index.json) or run from the SDK repository.');
416
+ }
417
+ /** Map a package name (`@totemsdk/core`) or dir (`core`) to its absolute directory. */
418
+ const _pkgDirCache = new Map();
419
+ function packageDir(name) {
420
+ if (_pkgDirCache.has(name))
421
+ return _pkgDirCache.get(name) ?? null;
422
+ const root = detectRepoRoot();
423
+ if (!root) {
424
+ _pkgDirCache.set(name, null);
425
+ return null;
426
+ }
427
+ const TOP_PKG_DIR = path.join(root, 'packages');
428
+ const direct = path.join(TOP_PKG_DIR, name);
429
+ if (fs.existsSync(path.join(direct, 'package.json'))) {
430
+ _pkgDirCache.set(name, direct);
431
+ return direct;
432
+ }
433
+ if (fs.existsSync(TOP_PKG_DIR)) {
434
+ for (const entry of fs.readdirSync(TOP_PKG_DIR, { withFileTypes: true })) {
435
+ if (!entry.isDirectory())
436
+ continue;
437
+ const dir = path.join(TOP_PKG_DIR, entry.name);
438
+ const pj = path.join(dir, 'package.json');
439
+ if (!fs.existsSync(pj))
440
+ continue;
441
+ try {
442
+ if (JSON.parse(fs.readFileSync(pj, 'utf-8')).name === name) {
443
+ _pkgDirCache.set(name, dir);
444
+ return dir;
445
+ }
446
+ }
447
+ catch { /* ignore unreadable package.json */ }
448
+ }
251
449
  }
450
+ _pkgDirCache.set(name, null);
252
451
  return null;
253
452
  }
453
+ /** Read a file from a package's `src/` directory (for the `read-source` tool). */
454
+ function readSourceFile(pkgName, filePath) {
455
+ const dir = packageDir(pkgName);
456
+ if (!dir)
457
+ return null;
458
+ const base = path.resolve(dir, 'src');
459
+ const resolved = path.resolve(base, filePath);
460
+ if (resolved !== base && !resolved.startsWith(base + path.sep))
461
+ return null;
462
+ return fs.existsSync(resolved) ? fs.readFileSync(resolved, 'utf-8') : null;
463
+ }
@@ -1,4 +1,8 @@
1
1
  import type { SdkIndex } from './types.js';
2
+ /** Extract a markdown section by heading slug/text (for `totemsdk://rfc/014#6-design`). */
3
+ export declare function extractSection(markdown: string, anchor: string): string | null;
4
+ /** MIME type for a resource URI. */
5
+ export declare function resourceMimeType(uri: string): string;
2
6
  export declare function handleResourceRead(uri: string, index: SdkIndex): string | null;
3
7
  export declare function listResources(index: SdkIndex): Array<{
4
8
  uri: string;