@xano/developer-mcp 1.0.56 → 1.0.57

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.
@@ -0,0 +1,197 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { join, dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { getXanoscriptDocsPath, setXanoscriptDocsPath, xanoscriptDocs, xanoscriptDocsTool, } from "./xanoscript_docs.js";
5
+ import { toMcpResponse } from "./types.js";
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const DOCS_PATH = join(__dirname, "..", "xanoscript_docs");
9
+ afterEach(() => {
10
+ // Reset to default so tests don't interfere with each other
11
+ setXanoscriptDocsPath(DOCS_PATH);
12
+ });
13
+ describe("getXanoscriptDocsPath", () => {
14
+ it("should return a string path", () => {
15
+ const path = getXanoscriptDocsPath();
16
+ expect(typeof path).toBe("string");
17
+ expect(path.length).toBeGreaterThan(0);
18
+ });
19
+ it("should return a path ending with xanoscript_docs", () => {
20
+ const path = getXanoscriptDocsPath();
21
+ expect(path).toMatch(/xanoscript_docs$/);
22
+ });
23
+ it("should return a consistent path on repeated calls", () => {
24
+ const path1 = getXanoscriptDocsPath();
25
+ const path2 = getXanoscriptDocsPath();
26
+ expect(path1).toBe(path2);
27
+ });
28
+ });
29
+ describe("setXanoscriptDocsPath", () => {
30
+ it("should override the docs path", () => {
31
+ const customPath = "/custom/docs/path";
32
+ setXanoscriptDocsPath(customPath);
33
+ expect(getXanoscriptDocsPath()).toBe(customPath);
34
+ });
35
+ it("should be used by xanoscriptDocs when set", () => {
36
+ setXanoscriptDocsPath(DOCS_PATH);
37
+ const result = xanoscriptDocs();
38
+ expect(result.documentation).toContain("Documentation version:");
39
+ });
40
+ });
41
+ describe("xanoscriptDocs", () => {
42
+ it("should return README when called with no args", () => {
43
+ const result = xanoscriptDocs();
44
+ expect(result).toHaveProperty("documentation");
45
+ expect(typeof result.documentation).toBe("string");
46
+ expect(result.documentation).toContain("Documentation version:");
47
+ });
48
+ it("should return specific topic documentation", () => {
49
+ const result = xanoscriptDocs({ topic: "syntax" });
50
+ expect(result.documentation).toContain("Documentation version:");
51
+ });
52
+ it("should return context-aware docs for file_path", () => {
53
+ const result = xanoscriptDocs({ file_path: "apis/users/create.xs" });
54
+ expect(result.documentation).toContain("XanoScript Documentation for:");
55
+ expect(result.documentation).toContain("Matched topics:");
56
+ });
57
+ it("should support quick_reference mode", () => {
58
+ const full = xanoscriptDocs({ topic: "syntax", mode: "full" });
59
+ const quick = xanoscriptDocs({ topic: "syntax", mode: "quick_reference" });
60
+ expect(quick.documentation.length).toBeLessThanOrEqual(full.documentation.length);
61
+ });
62
+ it("should throw for unknown topic", () => {
63
+ expect(() => xanoscriptDocs({ topic: "nonexistent" })).toThrow('Unknown topic "nonexistent"');
64
+ });
65
+ it("should throw for invalid docs path", () => {
66
+ setXanoscriptDocsPath("/nonexistent/path");
67
+ expect(() => xanoscriptDocs({ topic: "syntax" })).toThrow();
68
+ });
69
+ });
70
+ describe("xanoscriptDocsTool", () => {
71
+ it("should return success with data for valid call", () => {
72
+ const result = xanoscriptDocsTool();
73
+ expect(result.success).toBe(true);
74
+ expect(result.data).toBeDefined();
75
+ expect(typeof result.data).toBe("string");
76
+ expect(result.error).toBeUndefined();
77
+ });
78
+ it("should return success with topic docs", () => {
79
+ const result = xanoscriptDocsTool({ topic: "syntax" });
80
+ expect(result.success).toBe(true);
81
+ expect(result.data).toContain("Documentation version:");
82
+ });
83
+ it("should return error for unknown topic", () => {
84
+ const result = xanoscriptDocsTool({ topic: "nonexistent" });
85
+ expect(result.success).toBe(false);
86
+ expect(result.error).toBeDefined();
87
+ expect(result.error).toContain("Unknown topic");
88
+ });
89
+ it("should return error for invalid docs path", () => {
90
+ setXanoscriptDocsPath("/nonexistent/path");
91
+ const result = xanoscriptDocsTool({ topic: "syntax" });
92
+ expect(result.success).toBe(false);
93
+ expect(result.error).toBeDefined();
94
+ expect(result.error).toContain("Error retrieving XanoScript documentation");
95
+ });
96
+ it("should return multi-content array for file_path mode", () => {
97
+ const result = xanoscriptDocsTool({ file_path: "apis/users/create.xs" });
98
+ expect(result.success).toBe(true);
99
+ expect(Array.isArray(result.data)).toBe(true);
100
+ const data = result.data;
101
+ // First element is the header
102
+ expect(data[0]).toContain("XanoScript Documentation for: apis/users/create.xs");
103
+ expect(data[0]).toContain("Matched topics:");
104
+ expect(data[0]).toContain("Version:");
105
+ // Remaining elements are per-topic content
106
+ expect(data.length).toBeGreaterThan(1);
107
+ });
108
+ it("should return single string for topic mode", () => {
109
+ const result = xanoscriptDocsTool({ topic: "syntax" });
110
+ expect(result.success).toBe(true);
111
+ expect(typeof result.data).toBe("string");
112
+ expect(Array.isArray(result.data)).toBe(false);
113
+ });
114
+ it("should return single string for no-args mode", () => {
115
+ const result = xanoscriptDocsTool();
116
+ expect(result.success).toBe(true);
117
+ expect(typeof result.data).toBe("string");
118
+ expect(Array.isArray(result.data)).toBe(false);
119
+ });
120
+ it("should produce multiple MCP content blocks for file_path via toMcpResponse", () => {
121
+ const result = xanoscriptDocsTool({ file_path: "apis/users/create.xs" });
122
+ const mcpResponse = toMcpResponse(result);
123
+ expect(mcpResponse.isError).toBeUndefined();
124
+ // Should have multiple content blocks (header + N topics)
125
+ expect(mcpResponse.content.length).toBeGreaterThan(1);
126
+ // All blocks should be text type
127
+ for (const block of mcpResponse.content) {
128
+ expect(block.type).toBe("text");
129
+ expect(typeof block.text).toBe("string");
130
+ }
131
+ });
132
+ it("should produce single MCP content block for topic mode via toMcpResponse", () => {
133
+ const result = xanoscriptDocsTool({ topic: "syntax" });
134
+ const mcpResponse = toMcpResponse(result);
135
+ expect(mcpResponse.content).toHaveLength(1);
136
+ expect(mcpResponse.content[0].type).toBe("text");
137
+ });
138
+ it("should include structuredContent for file_path mode", () => {
139
+ const result = xanoscriptDocsTool({ file_path: "apis/users/create.xs" });
140
+ expect(result.success).toBe(true);
141
+ expect(result.structuredContent).toBeDefined();
142
+ expect(result.structuredContent).toHaveProperty("file_path", "apis/users/create.xs");
143
+ expect(result.structuredContent).toHaveProperty("mode", "quick_reference");
144
+ expect(result.structuredContent).toHaveProperty("version");
145
+ expect(result.structuredContent).toHaveProperty("topics");
146
+ expect(Array.isArray(result.structuredContent.topics)).toBe(true);
147
+ });
148
+ it("should include structuredContent for topic mode", () => {
149
+ const result = xanoscriptDocsTool({ topic: "syntax" });
150
+ expect(result.success).toBe(true);
151
+ expect(result.structuredContent).toBeDefined();
152
+ expect(result.structuredContent).toHaveProperty("documentation");
153
+ });
154
+ it("should include structuredContent in MCP response", () => {
155
+ const result = xanoscriptDocsTool({ file_path: "apis/users/create.xs" });
156
+ const mcpResponse = toMcpResponse(result);
157
+ expect(mcpResponse.structuredContent).toBeDefined();
158
+ expect(mcpResponse.structuredContent).toHaveProperty("file_path");
159
+ expect(mcpResponse.structuredContent).toHaveProperty("topics");
160
+ });
161
+ });
162
+ describe("xanoscriptDocs structured output", () => {
163
+ it("should include topics array for file_path mode", () => {
164
+ const result = xanoscriptDocs({ file_path: "apis/users/create.xs" });
165
+ expect(result.topics).toBeDefined();
166
+ expect(Array.isArray(result.topics)).toBe(true);
167
+ expect(result.topics.length).toBeGreaterThan(0);
168
+ });
169
+ it("should have topic and content fields in each TopicDoc", () => {
170
+ const result = xanoscriptDocs({ file_path: "apis/users/create.xs" });
171
+ for (const doc of result.topics) {
172
+ expect(doc).toHaveProperty("topic");
173
+ expect(doc).toHaveProperty("content");
174
+ expect(typeof doc.topic).toBe("string");
175
+ expect(typeof doc.content).toBe("string");
176
+ expect(doc.content.length).toBeGreaterThan(0);
177
+ }
178
+ });
179
+ it("should not include topics array for topic mode", () => {
180
+ const result = xanoscriptDocs({ topic: "syntax" });
181
+ expect(result.topics).toBeUndefined();
182
+ });
183
+ it("should not include topics array for no-args mode", () => {
184
+ const result = xanoscriptDocs();
185
+ expect(result.topics).toBeUndefined();
186
+ });
187
+ it("should respect exclude_topics in structured output", () => {
188
+ const result = xanoscriptDocs({
189
+ file_path: "apis/users/create.xs",
190
+ exclude_topics: ["syntax", "essentials"],
191
+ });
192
+ expect(result.topics).toBeDefined();
193
+ const topicNames = result.topics.map((d) => d.topic);
194
+ expect(topicNames).not.toContain("syntax");
195
+ expect(topicNames).not.toContain("essentials");
196
+ });
197
+ });
@@ -12,10 +12,15 @@ export interface DocConfig {
12
12
  export interface XanoscriptDocsArgs {
13
13
  topic?: string;
14
14
  file_path?: string;
15
- mode?: "full" | "quick_reference";
15
+ mode?: "full" | "quick_reference" | "index";
16
16
  exclude_topics?: string[];
17
17
  }
18
18
  export declare const XANOSCRIPT_DOCS_V2: Record<string, DocConfig>;
19
+ /**
20
+ * Clear all cached documentation content and version data.
21
+ * Useful for testing or when docs files change at runtime.
22
+ */
23
+ export declare function clearDocsCache(): void;
19
24
  /**
20
25
  * Get list of topics that apply to a given file path based on applyTo patterns
21
26
  */
@@ -32,6 +37,17 @@ export declare function getXanoscriptDocsVersion(docsPath: string): string;
32
37
  * Read XanoScript documentation with v2 structure
33
38
  */
34
39
  export declare function readXanoscriptDocsV2(docsPath: string, args?: XanoscriptDocsArgs): string;
40
+ export interface TopicDoc {
41
+ topic: string;
42
+ content: string;
43
+ }
44
+ /**
45
+ * Read documentation as structured per-topic entries for file_path mode.
46
+ * Returns each matched topic as a separate object for multi-content MCP responses.
47
+ */
48
+ export declare function readXanoscriptDocsStructured(docsPath: string, args: XanoscriptDocsArgs & {
49
+ file_path: string;
50
+ }): TopicDoc[];
35
51
  /**
36
52
  * Get available topic names
37
53
  */
@@ -7,191 +7,43 @@
7
7
  import { readFileSync } from "fs";
8
8
  import { join } from "path";
9
9
  import { minimatch } from "minimatch";
10
+ import docsIndex from "./xanoscript_docs/docs_index.json" with { type: "json" };
10
11
  // =============================================================================
11
- // Documentation Configuration
12
+ // Documentation Configuration (loaded from docs_index.json)
12
13
  // =============================================================================
13
- export const XANOSCRIPT_DOCS_V2 = {
14
- readme: {
15
- file: "README.md",
16
- applyTo: [],
17
- description: "XanoScript overview, workspace structure, and quick reference",
18
- },
19
- essentials: {
20
- file: "essentials.md",
21
- applyTo: ["**/*.xs"],
22
- description: "Common patterns, quick reference, and common mistakes to avoid",
23
- },
24
- syntax: {
25
- file: "syntax.md",
26
- applyTo: ["**/*.xs"],
27
- description: "Expressions, operators, and filters for all XanoScript code",
28
- },
29
- "syntax/string-filters": {
30
- file: "syntax/string-filters.md",
31
- applyTo: [],
32
- description: "String filters, regex, encoding, security filters, text functions",
33
- },
34
- "syntax/array-filters": {
35
- file: "syntax/array-filters.md",
36
- applyTo: [],
37
- description: "Array filters, functional operations, and array functions",
38
- },
39
- "syntax/functions": {
40
- file: "syntax/functions.md",
41
- applyTo: [],
42
- description: "Math filters/functions, object functions, bitwise operations",
43
- },
44
- types: {
45
- file: "types.md",
46
- applyTo: ["functions/**/*.xs", "apis/**/*.xs", "tools/**/*.xs", "agents/**/*.xs"],
47
- description: "Data types, input blocks, and validation",
48
- },
49
- tables: {
50
- file: "tables.md",
51
- applyTo: ["tables/*.xs"],
52
- description: "Database schema definitions with indexes and relationships",
53
- },
54
- functions: {
55
- file: "functions.md",
56
- applyTo: ["functions/**/*.xs"],
57
- description: "Reusable function stacks with inputs and responses",
58
- },
59
- apis: {
60
- file: "apis.md",
61
- applyTo: ["apis/**/*.xs"],
62
- description: "HTTP endpoint definitions with authentication and CRUD patterns",
63
- },
64
- tasks: {
65
- file: "tasks.md",
66
- applyTo: ["tasks/*.xs"],
67
- description: "Scheduled and cron jobs",
68
- },
69
- triggers: {
70
- file: "triggers.md",
71
- applyTo: ["triggers/**/*.xs"],
72
- description: "Event-driven handlers (table, realtime, workspace, agent, MCP)",
73
- },
74
- database: {
75
- file: "database.md",
76
- applyTo: ["functions/**/*.xs", "apis/**/*.xs", "tasks/*.xs", "tools/**/*.xs"],
77
- description: "All db.* operations: query, get, add, edit, patch, delete",
78
- },
79
- agents: {
80
- file: "agents.md",
81
- applyTo: ["agents/**/*.xs"],
82
- description: "AI agent configuration with LLM providers and tools",
83
- },
84
- tools: {
85
- file: "tools.md",
86
- applyTo: ["tools/**/*.xs"],
87
- description: "AI tools for agents and MCP servers",
88
- },
89
- "mcp-servers": {
90
- file: "mcp-servers.md",
91
- applyTo: ["mcp_servers/**/*.xs"],
92
- description: "MCP server definitions exposing tools",
93
- },
94
- "unit-testing": {
95
- file: "unit-testing.md",
96
- applyTo: ["functions/**/*.xs", "apis/**/*.xs", "middleware/**/*.xs"],
97
- description: "Unit tests, mocks, and assertions within functions, APIs, and middleware",
98
- },
99
- "workflow-tests": {
100
- file: "workflow-tests.md",
101
- applyTo: ["workflow_test/**/*.xs"],
102
- description: "End-to-end workflow tests with data source selection and tags",
103
- },
104
- integrations: {
105
- file: "integrations.md",
106
- applyTo: [],
107
- description: "External service integrations index - see sub-topics for details",
108
- },
109
- "integrations/cloud-storage": {
110
- file: "integrations/cloud-storage.md",
111
- applyTo: [],
112
- description: "AWS S3, Azure Blob, and GCP Storage operations",
113
- },
114
- "integrations/search": {
115
- file: "integrations/search.md",
116
- applyTo: [],
117
- description: "Elasticsearch, OpenSearch, and Algolia search operations",
118
- },
119
- "integrations/redis": {
120
- file: "integrations/redis.md",
121
- applyTo: [],
122
- description: "Redis caching, rate limiting, and queue operations",
123
- },
124
- "integrations/external-apis": {
125
- file: "integrations/external-apis.md",
126
- applyTo: [],
127
- description: "HTTP requests with api.request patterns",
128
- },
129
- "integrations/utilities": {
130
- file: "integrations/utilities.md",
131
- applyTo: [],
132
- description: "Local storage, email, zip, and Lambda utilities",
133
- },
134
- frontend: {
135
- file: "frontend.md",
136
- applyTo: ["static/**/*"],
137
- description: "Static frontend development and deployment",
138
- },
139
- run: {
140
- file: "run.md",
141
- applyTo: ["run/**/*.xs"],
142
- description: "Run job and service configurations for the Xano Job Runner",
143
- },
144
- addons: {
145
- file: "addons.md",
146
- applyTo: ["addons/*.xs"],
147
- description: "Reusable subqueries for fetching related data",
148
- },
149
- debugging: {
150
- file: "debugging.md",
151
- applyTo: ["**/*.xs"],
152
- description: "Logging, inspecting, and debugging XanoScript execution",
153
- },
154
- performance: {
155
- file: "performance.md",
156
- applyTo: ["functions/**/*.xs", "apis/**/*.xs"],
157
- description: "Performance optimization best practices",
158
- },
159
- realtime: {
160
- file: "realtime.md",
161
- applyTo: ["triggers/**/*.xs"],
162
- description: "Real-time channels and events for push updates",
163
- },
164
- schema: {
165
- file: "schema.md",
166
- applyTo: [],
167
- description: "Runtime schema parsing and validation",
168
- },
169
- security: {
170
- file: "security.md",
171
- applyTo: ["functions/**/*.xs", "apis/**/*.xs"],
172
- description: "Security best practices for authentication and authorization",
173
- },
174
- streaming: {
175
- file: "streaming.md",
176
- applyTo: [],
177
- description: "Streaming data from files, requests, and responses",
178
- },
179
- middleware: {
180
- file: "middleware.md",
181
- applyTo: ["middleware/**/*.xs"],
182
- description: "Request/response interceptors for functions, queries, tasks, and tools",
183
- },
184
- branch: {
185
- file: "branch.md",
186
- applyTo: ["branch.xs"],
187
- description: "Branch-level settings: middleware, history retention, visual styling",
188
- },
189
- workspace: {
190
- file: "workspace.md",
191
- applyTo: ["workspace/**/*.xs"],
192
- description: "Workspace-level settings: environment variables, preferences, realtime",
193
- },
194
- };
14
+ function buildDocsConfig() {
15
+ const config = {};
16
+ for (const [key, topic] of Object.entries(docsIndex.topics)) {
17
+ config[key] = {
18
+ file: topic.file,
19
+ applyTo: topic.applyTo,
20
+ description: topic.description,
21
+ };
22
+ }
23
+ return config;
24
+ }
25
+ export const XANOSCRIPT_DOCS_V2 = buildDocsConfig();
26
+ // =============================================================================
27
+ // Content Cache
28
+ // =============================================================================
29
+ const _fileCache = new Map();
30
+ let _versionCache = null;
31
+ /**
32
+ * Clear all cached documentation content and version data.
33
+ * Useful for testing or when docs files change at runtime.
34
+ */
35
+ export function clearDocsCache() {
36
+ _fileCache.clear();
37
+ _versionCache = null;
38
+ }
39
+ function cachedReadFile(filePath) {
40
+ const cached = _fileCache.get(filePath);
41
+ if (cached !== undefined)
42
+ return cached;
43
+ const content = readFileSync(filePath, "utf-8");
44
+ _fileCache.set(filePath, content);
45
+ return content;
46
+ }
195
47
  // =============================================================================
196
48
  // Core Functions
197
49
  // =============================================================================
@@ -239,9 +91,14 @@ export function extractQuickReference(content, topic) {
239
91
  * Get the documentation version from the version.json file
240
92
  */
241
93
  export function getXanoscriptDocsVersion(docsPath) {
94
+ if (_versionCache && _versionCache.path === docsPath) {
95
+ return _versionCache.version;
96
+ }
242
97
  try {
243
- const versionFile = readFileSync(join(docsPath, "version.json"), "utf-8");
244
- return JSON.parse(versionFile).version || "unknown";
98
+ const versionFile = cachedReadFile(join(docsPath, "version.json"));
99
+ const version = JSON.parse(versionFile).version || "unknown";
100
+ _versionCache = { path: docsPath, version };
101
+ return version;
245
102
  }
246
103
  catch {
247
104
  return "unknown";
@@ -251,13 +108,39 @@ export function getXanoscriptDocsVersion(docsPath) {
251
108
  * Read XanoScript documentation with v2 structure
252
109
  */
253
110
  export function readXanoscriptDocsV2(docsPath, args) {
111
+ const version = getXanoscriptDocsVersion(docsPath);
112
+ // Index mode: return compact topic listing with byte sizes
113
+ if (args?.mode === "index") {
114
+ const rows = Object.entries(XANOSCRIPT_DOCS_V2).map(([name, config]) => {
115
+ let size;
116
+ try {
117
+ size = cachedReadFile(join(docsPath, config.file)).length;
118
+ }
119
+ catch {
120
+ size = 0;
121
+ }
122
+ const sizeKb = (size / 1024).toFixed(1);
123
+ return `| ${name} | ${config.description} | ${sizeKb} KB |`;
124
+ });
125
+ return [
126
+ `# XanoScript Documentation Index`,
127
+ ``,
128
+ `Version: ${version}`,
129
+ `Topics: ${rows.length}`,
130
+ ``,
131
+ `| Topic | Description | Size |`,
132
+ `|-------|-------------|------|`,
133
+ ...rows,
134
+ ``,
135
+ `Use topic='<name>' to load a specific topic. Use mode='quick_reference' for compact output.`,
136
+ ].join("\n");
137
+ }
254
138
  // Default to quick_reference for file_path mode (loads many topics),
255
139
  // full for topic mode (loads single topic)
256
140
  const mode = args?.mode || (args?.file_path ? "quick_reference" : "full");
257
- const version = getXanoscriptDocsVersion(docsPath);
258
141
  // Default: return README
259
142
  if (!args?.topic && !args?.file_path) {
260
- const readme = readFileSync(join(docsPath, "README.md"), "utf-8");
143
+ const readme = cachedReadFile(join(docsPath, "README.md"));
261
144
  return `${readme}\n\n---\nDocumentation version: ${version}`;
262
145
  }
263
146
  // Context-aware: return docs matching file pattern
@@ -272,7 +155,7 @@ export function readXanoscriptDocsV2(docsPath, args) {
272
155
  }
273
156
  const docs = topics.map((t) => {
274
157
  const config = XANOSCRIPT_DOCS_V2[t];
275
- const content = readFileSync(join(docsPath, config.file), "utf-8");
158
+ const content = cachedReadFile(join(docsPath, config.file));
276
159
  return mode === "quick_reference"
277
160
  ? extractQuickReference(content, t)
278
161
  : content;
@@ -286,12 +169,39 @@ export function readXanoscriptDocsV2(docsPath, args) {
286
169
  const availableTopics = Object.keys(XANOSCRIPT_DOCS_V2).join(", ");
287
170
  throw new Error(`Unknown topic "${args.topic}".\n\nAvailable topics: ${availableTopics}`);
288
171
  }
289
- const content = readFileSync(join(docsPath, config.file), "utf-8");
172
+ const content = cachedReadFile(join(docsPath, config.file));
290
173
  const doc = mode === "quick_reference"
291
174
  ? extractQuickReference(content, args.topic)
292
175
  : content;
293
176
  return `${doc}\n\n---\nDocumentation version: ${version}`;
294
177
  }
178
+ /**
179
+ * Read documentation as structured per-topic entries for file_path mode.
180
+ * Returns each matched topic as a separate object for multi-content MCP responses.
181
+ */
182
+ export function readXanoscriptDocsStructured(docsPath, args) {
183
+ const mode = args.mode || "quick_reference";
184
+ let topics = getDocsForFilePath(args.file_path);
185
+ if (args.exclude_topics && args.exclude_topics.length > 0) {
186
+ topics = topics.filter((t) => !args.exclude_topics.includes(t));
187
+ }
188
+ if (topics.length === 0) {
189
+ throw new Error(`No documentation found for file pattern: ${args.file_path}\n\nAvailable topics: ${Object.keys(XANOSCRIPT_DOCS_V2).join(", ")}`);
190
+ }
191
+ return topics.map((t) => {
192
+ const config = XANOSCRIPT_DOCS_V2[t];
193
+ const content = cachedReadFile(join(docsPath, config.file));
194
+ return {
195
+ topic: t,
196
+ content: mode === "quick_reference"
197
+ ? extractQuickReference(content, t)
198
+ : content,
199
+ };
200
+ });
201
+ }
202
+ // =============================================================================
203
+ // Topic Metadata
204
+ // =============================================================================
295
205
  /**
296
206
  * Get available topic names
297
207
  */
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { join, dirname } from "path";
3
3
  import { fileURLToPath } from "url";
4
- import { XANOSCRIPT_DOCS_V2, getDocsForFilePath, extractQuickReference, getXanoscriptDocsVersion, readXanoscriptDocsV2, getTopicNames, getTopicDescriptions, } from "./xanoscript.js";
4
+ import { XANOSCRIPT_DOCS_V2, getDocsForFilePath, extractQuickReference, getXanoscriptDocsVersion, readXanoscriptDocsV2, readXanoscriptDocsStructured, getTopicNames, getTopicDescriptions, } from "./xanoscript.js";
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = dirname(__filename);
7
7
  const DOCS_PATH = join(__dirname, "xanoscript_docs");
@@ -316,6 +316,30 @@ Even more content.
316
316
  it("should throw for invalid docs path", () => {
317
317
  expect(() => readXanoscriptDocsV2("/nonexistent/path", { topic: "syntax" })).toThrow();
318
318
  });
319
+ it("should return compact index with mode: index", () => {
320
+ const result = readXanoscriptDocsV2(DOCS_PATH, { mode: "index" });
321
+ expect(result).toContain("# XanoScript Documentation Index");
322
+ expect(result).toContain("Version:");
323
+ expect(result).toContain("Topics:");
324
+ expect(result).toContain("| Topic | Description | Size |");
325
+ // Should contain some known topics
326
+ expect(result).toContain("| syntax |");
327
+ expect(result).toContain("| essentials |");
328
+ expect(result).toContain("| database |");
329
+ // Should contain KB size indicators
330
+ expect(result).toContain("KB |");
331
+ });
332
+ it("should return index mode without requiring topic or file_path", () => {
333
+ const result = readXanoscriptDocsV2(DOCS_PATH, { mode: "index" });
334
+ // Index mode ignores topic/file_path — just returns the listing
335
+ expect(result).toContain("# XanoScript Documentation Index");
336
+ });
337
+ it("should return index that is significantly smaller than full docs", () => {
338
+ const index = readXanoscriptDocsV2(DOCS_PATH, { mode: "index" });
339
+ const full = readXanoscriptDocsV2(DOCS_PATH, { topic: "syntax", mode: "full" });
340
+ // Index should be compact — smaller than even a single full topic
341
+ expect(index.length).toBeLessThan(full.length);
342
+ });
319
343
  });
320
344
  describe("getTopicNames", () => {
321
345
  it("should return all topic names", () => {
@@ -347,4 +371,54 @@ Even more content.
347
371
  expect(descriptions).toContain("Expressions, operators");
348
372
  });
349
373
  });
374
+ describe("readXanoscriptDocsStructured", () => {
375
+ it("should return array of TopicDoc objects", () => {
376
+ const result = readXanoscriptDocsStructured(DOCS_PATH, {
377
+ file_path: "apis/users/create.xs",
378
+ });
379
+ expect(Array.isArray(result)).toBe(true);
380
+ expect(result.length).toBeGreaterThan(0);
381
+ for (const doc of result) {
382
+ expect(doc).toHaveProperty("topic");
383
+ expect(doc).toHaveProperty("content");
384
+ expect(typeof doc.topic).toBe("string");
385
+ expect(typeof doc.content).toBe("string");
386
+ }
387
+ });
388
+ it("should match the same topics as getDocsForFilePath", () => {
389
+ const filePath = "apis/users/create.xs";
390
+ const expected = getDocsForFilePath(filePath);
391
+ const result = readXanoscriptDocsStructured(DOCS_PATH, { file_path: filePath });
392
+ const resultTopics = result.map((d) => d.topic);
393
+ expect(resultTopics).toEqual(expected);
394
+ });
395
+ it("should respect exclude_topics", () => {
396
+ const result = readXanoscriptDocsStructured(DOCS_PATH, {
397
+ file_path: "apis/users/create.xs",
398
+ exclude_topics: ["syntax", "essentials"],
399
+ });
400
+ const topics = result.map((d) => d.topic);
401
+ expect(topics).not.toContain("syntax");
402
+ expect(topics).not.toContain("essentials");
403
+ });
404
+ it("should throw when all topics are excluded", () => {
405
+ expect(() => readXanoscriptDocsStructured(DOCS_PATH, {
406
+ file_path: "branch.xs",
407
+ exclude_topics: ["syntax", "essentials", "debugging", "branch"],
408
+ })).toThrow("No documentation found");
409
+ });
410
+ it("should use quick_reference mode by default", () => {
411
+ const quickResult = readXanoscriptDocsStructured(DOCS_PATH, {
412
+ file_path: "tables/users.xs",
413
+ });
414
+ const fullResult = readXanoscriptDocsStructured(DOCS_PATH, {
415
+ file_path: "tables/users.xs",
416
+ mode: "full",
417
+ });
418
+ // Quick reference content should be shorter than full
419
+ const quickTotal = quickResult.reduce((sum, d) => sum + d.content.length, 0);
420
+ const fullTotal = fullResult.reduce((sum, d) => sum + d.content.length, 0);
421
+ expect(quickTotal).toBeLessThanOrEqual(fullTotal);
422
+ });
423
+ });
350
424
  });