@amcharts/amcharts5-mcp 1.2.0 → 1.3.2
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/README.md +98 -0
- package/content/gantt.md +3 -3
- package/content/stock.md +4 -3
- package/content/timeline.md +29 -25
- package/content/ui-elements.md +5 -3
- package/content/wordcloud.md +4 -4
- package/content/xy.md +1 -1
- package/extended/reference/iserpentinechartsettings.md +1 -0
- package/extended/reference/ispiralchartsettings.md +1 -0
- package/package.json +9 -1
- package/src/content-fs.js +74 -0
- package/src/content.js +214 -0
- package/src/index.js +6 -629
- package/src/tools.js +391 -0
package/README.md
CHANGED
|
@@ -10,6 +10,104 @@ Instead of loading the entire amCharts reference into the AI's context window, t
|
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
|
+
The easiest way is the **hosted server** — no install, just add one URL. Prefer to
|
|
14
|
+
run it yourself? See [Local install](#local-install-npx) below.
|
|
15
|
+
|
|
16
|
+
### Hosted (no install) — recommended
|
|
17
|
+
|
|
18
|
+
Point your AI client at:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
https://mcp.amcharts.com/mcp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Claude Code**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
claude mcp add --transport http amcharts5 https://mcp.amcharts.com/mcp
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Claude Desktop / claude.ai** — Settings → Connectors → **Add custom connector**, then paste `https://mcp.amcharts.com/mcp`.
|
|
31
|
+
|
|
32
|
+
**Cursor** — `.cursor/mcp.json`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"amcharts5": {
|
|
38
|
+
"url": "https://mcp.amcharts.com/mcp"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**VS Code + GitHub Copilot** — `.vscode/mcp.json`:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"servers": {
|
|
49
|
+
"amcharts5": {
|
|
50
|
+
"type": "http",
|
|
51
|
+
"url": "https://mcp.amcharts.com/mcp"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Windsurf** — `~/.codeium/windsurf/mcp_config.json`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"amcharts5": {
|
|
63
|
+
"serverUrl": "https://mcp.amcharts.com/mcp"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**ChatGPT** (Plus/Pro/Enterprise with connectors/developer mode enabled) — Settings → **Connectors** → **Add custom connector**, then paste `https://mcp.amcharts.com/mcp`.
|
|
70
|
+
|
|
71
|
+
**Cline** (VS Code extension) — open the **MCP Servers** panel → **Remote Servers**, paste `https://mcp.amcharts.com/mcp`, or add to its MCP settings JSON:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"amcharts5": {
|
|
77
|
+
"type": "streamableHttp",
|
|
78
|
+
"url": "https://mcp.amcharts.com/mcp"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Clients that only support local (stdio) servers** (e.g. Zed, Continue, Amazon Q Developer) — bridge to the hosted URL with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote). Use this as the server's run command in the client's config:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx mcp-remote https://mcp.amcharts.com/mcp
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For example, in a `command`/`args` style config:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"mcpServers": {
|
|
95
|
+
"amcharts5": {
|
|
96
|
+
"command": "npx",
|
|
97
|
+
"args": ["-y", "mcp-remote", "https://mcp.amcharts.com/mcp"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Any other MCP client that accepts a remote server URL works too — use the `/mcp`
|
|
104
|
+
endpoint (Streamable HTTP) or `/sse` (legacy SSE). For URL-only clients that still
|
|
105
|
+
expect a command, use the `mcp-remote` bridge shown above.
|
|
106
|
+
|
|
107
|
+
## Local install (npx)
|
|
108
|
+
|
|
109
|
+
Prefer running it yourself? The server is also published to npm and runs over stdio.
|
|
110
|
+
|
|
13
111
|
### Claude Code
|
|
14
112
|
|
|
15
113
|
For **all projects** (global, recommended):
|
package/content/gantt.md
CHANGED
|
@@ -47,10 +47,10 @@ var chart = root.container.children.push(
|
|
|
47
47
|
| `editable` | boolean | `true` | Allow user to add, edit, remove, drag tasks |
|
|
48
48
|
| `durationUnit` | string | `"day"` | `"year"`, `"month"`, `"week"`, `"day"`, `"hour"`, `"minute"`, `"second"` |
|
|
49
49
|
| `weekends` | number[] | `[0, 6]` | Non-working days (0=Sun, 1=Mon, ... 6=Sat) |
|
|
50
|
-
| `excludeWeekends` | boolean | `
|
|
50
|
+
| `excludeWeekends` | boolean | `false` | Whether to exclude weekends from task durations |
|
|
51
51
|
| `holidays` | Date[] | `[]` | Specific non-working dates |
|
|
52
52
|
| `snapThreshold` | number | `0.5` | Drag snap position within a period (0–1) |
|
|
53
|
-
| `sidebarWidth` | number |
|
|
53
|
+
| `sidebarWidth` | number \| Percent | `30%` | Sidebar width in pixels or Percent |
|
|
54
54
|
|
|
55
55
|
## Data structure
|
|
56
56
|
|
|
@@ -134,7 +134,7 @@ Set `duration: 0` to create milestone markers.
|
|
|
134
134
|
```js
|
|
135
135
|
chart.yAxis.setAll({
|
|
136
136
|
minCellHeight: 80, // row height in pixels (default: 70)
|
|
137
|
-
childCellSize: 1, // child row size relative to parent (default: 0.
|
|
137
|
+
childCellSize: 1, // child row size relative to parent (default: 0.8, 1 = same)
|
|
138
138
|
childShift: 0 // child label indent in pixels (default: 25)
|
|
139
139
|
});
|
|
140
140
|
```
|
package/content/stock.md
CHANGED
|
@@ -216,7 +216,7 @@ stockChart.indicators.push(am5stock.Volume.new(root, {
|
|
|
216
216
|
| Indicator | Class | Needs volumeSeries |
|
|
217
217
|
|-----------|-------|--------------------|
|
|
218
218
|
| Moving Average | `MovingAverage` | No |
|
|
219
|
-
| Moving Average Cross | `
|
|
219
|
+
| Moving Average Cross | `MACross` | No |
|
|
220
220
|
| Moving Average Envelope | `MovingAverageEnvelope` | No |
|
|
221
221
|
| Moving Average Deviation | `MovingAverageDeviation` | No |
|
|
222
222
|
| Bollinger Bands | `BollingerBands` | No |
|
|
@@ -281,8 +281,9 @@ var data = [
|
|
|
281
281
|
## Events
|
|
282
282
|
|
|
283
283
|
```js
|
|
284
|
-
// Period changed
|
|
285
|
-
stockChart.
|
|
284
|
+
// Period changed — the `periodselected` event fires on the PeriodSelector
|
|
285
|
+
// instance, NOT on stockChart. Attach the handler to the selector you created.
|
|
286
|
+
periodSelector.events.on("periodselected", (ev) => {
|
|
286
287
|
console.log("Period:", ev.period);
|
|
287
288
|
});
|
|
288
289
|
|
package/content/timeline.md
CHANGED
|
@@ -48,20 +48,24 @@ const chart = root.container.children.push(
|
|
|
48
48
|
am5timeline.SerpentineChart.new(root, {
|
|
49
49
|
orientation: "vertical", // "vertical" | "horizontal"
|
|
50
50
|
levelCount: 3, // number of bends/turns
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
startLocation: 0, // relative start of the "road" (0-1)
|
|
52
|
+
endLocation: 1, // relative end of the "road" (0-1)
|
|
53
|
+
yAxisRadius: am5.percent(50), // band thickness (Percent); applied to the Y axes' radius
|
|
53
54
|
})
|
|
54
55
|
);
|
|
55
56
|
```
|
|
56
57
|
|
|
57
|
-
**Serpentine settings
|
|
58
|
+
**Serpentine settings** (`ISerpentineChartSettings`):
|
|
58
59
|
|
|
59
|
-
| Setting | Type | Description |
|
|
60
|
-
|
|
61
|
-
| `orientation` | `"vertical"` \| `"horizontal"` | Direction of the snake pattern |
|
|
62
|
-
| `levelCount` | number | Number of turns/bends |
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
60
|
+
| Setting | Type | Default | Description |
|
|
61
|
+
|---------|------|---------|-------------|
|
|
62
|
+
| `orientation` | `"vertical"` \| `"horizontal"` | `"vertical"` | Direction of the snake pattern |
|
|
63
|
+
| `levelCount` | number | `3` | Number of turns/bends |
|
|
64
|
+
| `startLocation` | number | `0` | Relative location (0-1) of the start |
|
|
65
|
+
| `endLocation` | number | `1` | Relative location (0-1) of the end |
|
|
66
|
+
| `yAxisRadius` | `Percent` | `50%` | Band thickness — applied to every Y axis renderer's radius |
|
|
67
|
+
|
|
68
|
+
> There is no `yAxisInnerRadius` setting on the chart. Band thickness is controlled by `yAxisRadius` (a `Percent`). For a plain `CurveChart` (no Serpentine/Spiral wrapper) the Y-axis band length is set via `AxisRendererCurveY`'s `axisLength` instead.
|
|
65
69
|
|
|
66
70
|
### SpiralChart
|
|
67
71
|
|
|
@@ -71,23 +75,25 @@ Wraps the chart into a spiral shape.
|
|
|
71
75
|
const chart = root.container.children.push(
|
|
72
76
|
am5timeline.SpiralChart.new(root, {
|
|
73
77
|
levelCount: 3, // number of spiral rings
|
|
74
|
-
|
|
78
|
+
startAngle: -90, // start angle in degrees
|
|
75
79
|
endAngle: -135, // end angle in degrees
|
|
76
|
-
yAxisRadius: am5.percent(
|
|
77
|
-
|
|
78
|
-
innerRadius: am5.percent(30), // inner hole radius
|
|
80
|
+
yAxisRadius: am5.percent(60), // band thickness (Percent)
|
|
81
|
+
innerRadius: am5.percent(30), // inner hole radius (Percent)
|
|
79
82
|
})
|
|
80
83
|
);
|
|
81
84
|
```
|
|
82
85
|
|
|
83
|
-
**Spiral settings
|
|
86
|
+
**Spiral settings** (`ISpiralChartSettings`):
|
|
87
|
+
|
|
88
|
+
| Setting | Type | Default | Description |
|
|
89
|
+
|---------|------|---------|-------------|
|
|
90
|
+
| `levelCount` | number | `3` | Number of spiral rings |
|
|
91
|
+
| `startAngle` | number | `-90` | Where the spiral starts (degrees) |
|
|
92
|
+
| `endAngle` | number | `0` | Where the spiral ends (degrees) |
|
|
93
|
+
| `innerRadius` | `Percent` | `60%` | Size of the center hole |
|
|
94
|
+
| `yAxisRadius` | `Percent` | `50%` | Band thickness — applied to every Y axis renderer's radius |
|
|
84
95
|
|
|
85
|
-
|
|
86
|
-
|---------|------|-------------|
|
|
87
|
-
| `levelCount` | number | Number of spiral rings |
|
|
88
|
-
| `inversed` | boolean | Spiral direction |
|
|
89
|
-
| `endAngle` | number | Where the spiral ends (degrees) |
|
|
90
|
-
| `innerRadius` | percent/number | Size of the center hole |
|
|
96
|
+
> There is no `inversed` or `yAxisInnerRadius` setting on `SpiralChart`. To reverse direction use the `startAngle`/`endAngle` pair. (`inversed` is a valid axis-renderer setting, but not a chart setting.)
|
|
91
97
|
|
|
92
98
|
### CurveChart
|
|
93
99
|
|
|
@@ -306,8 +312,7 @@ chart.get("cursor").events.on("cursormoved", function(ev) {
|
|
|
306
312
|
am5timeline.SerpentineChart.new(root, {
|
|
307
313
|
orientation: "vertical",
|
|
308
314
|
levelCount: 3,
|
|
309
|
-
yAxisRadius: am5.percent(
|
|
310
|
-
yAxisInnerRadius: -25
|
|
315
|
+
yAxisRadius: am5.percent(50)
|
|
311
316
|
})
|
|
312
317
|
);
|
|
313
318
|
|
|
@@ -399,10 +404,9 @@ chart.get("cursor").events.on("cursormoved", function(ev) {
|
|
|
399
404
|
var chart = root.container.children.push(
|
|
400
405
|
am5timeline.SpiralChart.new(root, {
|
|
401
406
|
levelCount: 3,
|
|
402
|
-
|
|
407
|
+
startAngle: -90,
|
|
403
408
|
endAngle: -135,
|
|
404
|
-
yAxisRadius: am5.percent(
|
|
405
|
-
yAxisInnerRadius: 0,
|
|
409
|
+
yAxisRadius: am5.percent(60),
|
|
406
410
|
innerRadius: am5.percent(30)
|
|
407
411
|
})
|
|
408
412
|
);
|
package/content/ui-elements.md
CHANGED
|
@@ -81,14 +81,16 @@ button.get("background").states.create("down", {
|
|
|
81
81
|
### Togglable button
|
|
82
82
|
|
|
83
83
|
```js
|
|
84
|
+
// There is no `togglable` setting on Button. Make it a toggle by setting
|
|
85
|
+
// `toggleKey: "active"` (a Sprite setting) — clicks then flip the `active` boolean.
|
|
84
86
|
var button = container.children.push(am5.Button.new(root, {
|
|
85
|
-
|
|
87
|
+
toggleKey: "active",
|
|
86
88
|
label: am5.Label.new(root, { text: "Toggle" })
|
|
87
89
|
}));
|
|
88
90
|
|
|
89
|
-
// Check active state
|
|
91
|
+
// Check active state via the `active` setting (there is no isActive() method)
|
|
90
92
|
button.events.on("click", function(ev) {
|
|
91
|
-
var isActive = ev.target.
|
|
93
|
+
var isActive = ev.target.get("active");
|
|
92
94
|
console.log("Active:", isActive);
|
|
93
95
|
});
|
|
94
96
|
```
|
package/content/wordcloud.md
CHANGED
|
@@ -57,10 +57,10 @@ series.appear(1000);
|
|
|
57
57
|
|---------|------|---------|-------------|
|
|
58
58
|
| `categoryField` | string | — | Data field for word text (required) |
|
|
59
59
|
| `valueField` | string | — | Data field for word weight/size (required) |
|
|
60
|
-
| `maxFontSize` |
|
|
61
|
-
| `minFontSize` |
|
|
62
|
-
| `angles` | number[] | `[0
|
|
63
|
-
| `randomness` | number (0–1) | `0
|
|
60
|
+
| `maxFontSize` | number \| Percent | `100` | Maximum font size (a `Percent` is relative to the smaller chart dimension) |
|
|
61
|
+
| `minFontSize` | number \| Percent | `10` | Minimum font size (a `Percent` is relative to the smaller chart dimension) |
|
|
62
|
+
| `angles` | number[] | `[0]` | Allowed rotation angles for words |
|
|
63
|
+
| `randomness` | number (0–1) | `0` | Layout randomness (0 = ordered by value, 1 = fully random) |
|
|
64
64
|
| `minWordLength` | number | `1` | Skip words shorter than this |
|
|
65
65
|
| `maxCount` | number | — | Maximum number of words to display |
|
|
66
66
|
| `excludeWords` | string[] | `[]` | Words to exclude |
|
package/content/xy.md
CHANGED
|
@@ -670,7 +670,7 @@ am5xy.XYChart.new(root, {
|
|
|
670
670
|
wheelX: "panX", // mouse wheel: "panX", "panY", "zoomX", "zoomY", "none"
|
|
671
671
|
wheelY: "zoomX", // mouse wheel vertical
|
|
672
672
|
pinchZoomX: true, // mobile pinch zoom on X
|
|
673
|
-
|
|
673
|
+
arrangeTooltips: true, // de-overlap stacked series tooltips (NOTE: there is no `arrangeFields` setting)
|
|
674
674
|
maxTooltipDistance: 0, // show tooltips for nearby series (-1 = all)
|
|
675
675
|
})
|
|
676
676
|
```
|
|
@@ -75,4 +75,5 @@ Extends: ICurveChartSettings
|
|
|
75
75
|
- **levelCount** (`undefined | number`) — Default 3 Number of levels in the chart.
|
|
76
76
|
- **orientation** (`"horizontal" | "vertical"`) — Default "vertical" Orientation of the serpatine.
|
|
77
77
|
- **startLocation** (`undefined | number`) — Relative location (0-1) of the start postion. @defult 0
|
|
78
|
+
- **yAxisRadius** (`Percent`) — Default 50% Radius of the Y-axis (band thickness); applied to every Y axis renderer's radius.
|
|
78
79
|
|
|
@@ -75,4 +75,5 @@ Extends: ICurveChartSettings
|
|
|
75
75
|
- **innerRadius** (`Percent`) — Default 60% Inner radius of the spiral in percent.
|
|
76
76
|
- **levelCount** (`undefined | number`) — Default 3 Numer of spiral circles.
|
|
77
77
|
- **startAngle** (`undefined | number`) — Default -90 Start angle of the spiral in degrees.
|
|
78
|
+
- **yAxisRadius** (`Percent`) — Default 50% Y-axis radius in percent (band thickness); applied to every Y axis renderer's radius.
|
|
78
79
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amcharts/amcharts5-mcp",
|
|
3
|
-
"version": "1.2
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"mcpName": "io.github.amcharts/amcharts5",
|
|
5
5
|
"description": "On-demand access to amCharts 5 docs, code examples, and API reference",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"start": "node src/index.js",
|
|
20
20
|
"dev": "node --watch src/index.js",
|
|
21
|
+
"test": "node test.js",
|
|
22
|
+
"build:worker": "node scripts/build-worker-content.js",
|
|
23
|
+
"dev:worker": "npm run build:worker && wrangler dev",
|
|
24
|
+
"deploy": "npm run build:worker && wrangler deploy",
|
|
21
25
|
"prepublishOnly": "node -e \"const fs=require('fs');if(!fs.existsSync('content'))fs.mkdirSync('content');fs.cpSync('amcharts5-skill/amcharts5-skill/SKILL.md','content/SKILL.md');for(const f of fs.readdirSync('amcharts5-skill/amcharts5-skill/references').filter(x=>x.endsWith('.md')))fs.cpSync('amcharts5-skill/amcharts5-skill/references/'+f,'content/'+f)\"",
|
|
22
26
|
"postpublish": "node -e \"require('fs').rmSync('content',{recursive:true,force:true})\""
|
|
23
27
|
},
|
|
@@ -48,5 +52,9 @@
|
|
|
48
52
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
49
53
|
"jsdom": "^29.0.0",
|
|
50
54
|
"turndown": "^7.2.2"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"agents": "^0.16.2",
|
|
58
|
+
"wrangler": "^4.102.0"
|
|
51
59
|
}
|
|
52
60
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Node-only disk loader.
|
|
3
|
+
//
|
|
4
|
+
// Reads raw markdown off the filesystem into the { skillFiles, extendedFiles }
|
|
5
|
+
// shape consumed by buildContent(). Used by the stdio server (src/index.js) and
|
|
6
|
+
// by the Worker content build script (scripts/build-worker-content.js).
|
|
7
|
+
//
|
|
8
|
+
// NOTE: imports `fs` — must NOT be imported by the Worker bundle.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
import { readFileSync, readdirSync, existsSync } from "fs";
|
|
12
|
+
import { join, dirname } from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
import { buildContent } from "./content.js";
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const ROOT = join(__dirname, "..");
|
|
18
|
+
|
|
19
|
+
// Submodule path (local dev) or content/ fallback (npm install)
|
|
20
|
+
const SUBMODULE_DIR = join(ROOT, "amcharts5-skill", "amcharts5-skill");
|
|
21
|
+
const CONTENT_DIR = join(ROOT, "content");
|
|
22
|
+
const SKILL_DIR = existsSync(SUBMODULE_DIR) ? SUBMODULE_DIR : CONTENT_DIR;
|
|
23
|
+
const REFERENCES_DIR = existsSync(join(SKILL_DIR, "references"))
|
|
24
|
+
? join(SKILL_DIR, "references")
|
|
25
|
+
: SKILL_DIR;
|
|
26
|
+
const EXTENDED_DIR = join(ROOT, "extended");
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read all skill + extended markdown into the raw { skillFiles, extendedFiles }
|
|
30
|
+
* shape that buildContent() expects.
|
|
31
|
+
*/
|
|
32
|
+
export function readRawFiles() {
|
|
33
|
+
const skillFiles = {};
|
|
34
|
+
const extendedFiles = {};
|
|
35
|
+
|
|
36
|
+
// Core SKILL.md
|
|
37
|
+
const skillPath = join(SKILL_DIR, "SKILL.md");
|
|
38
|
+
if (existsSync(skillPath)) {
|
|
39
|
+
skillFiles["SKILL"] = readFileSync(skillPath, "utf-8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Reference files (skip SKILL.md if it lives in the same dir as references)
|
|
43
|
+
if (existsSync(REFERENCES_DIR)) {
|
|
44
|
+
for (const file of readdirSync(REFERENCES_DIR)) {
|
|
45
|
+
if (!file.endsWith(".md") || file === "SKILL.md") continue;
|
|
46
|
+
skillFiles[file.replace(".md", "")] = readFileSync(join(REFERENCES_DIR, file), "utf-8");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Extended docs + examples (recursive)
|
|
51
|
+
walkExtended(EXTENDED_DIR, "", extendedFiles);
|
|
52
|
+
|
|
53
|
+
return { skillFiles, extendedFiles };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function walkExtended(dir, prefix, out) {
|
|
57
|
+
if (!existsSync(dir)) return;
|
|
58
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
59
|
+
const fullPath = join(dir, entry.name);
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
walkExtended(fullPath, prefix ? `${prefix}/${entry.name}` : entry.name, out);
|
|
62
|
+
} else if (entry.name.endsWith(".md")) {
|
|
63
|
+
const key = prefix
|
|
64
|
+
? `${prefix}/${entry.name.replace(".md", "")}`
|
|
65
|
+
: entry.name.replace(".md", "");
|
|
66
|
+
out[key] = readFileSync(fullPath, "utf-8");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Read from disk and build the in-memory content structures. */
|
|
72
|
+
export function loadContentFromDisk() {
|
|
73
|
+
return buildContent(readRawFiles());
|
|
74
|
+
}
|
package/src/content.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Shared, transport-agnostic content layer.
|
|
3
|
+
//
|
|
4
|
+
// Pure logic only — NO filesystem, NO Node built-ins — so this module can be
|
|
5
|
+
// bundled into the Cloudflare Worker as-is. The stdio server reads files from
|
|
6
|
+
// disk (see content-fs.js) and the Worker imports a pre-generated bundle (see
|
|
7
|
+
// scripts/build-worker-content.js); both feed the same buildContent() here so
|
|
8
|
+
// the two deployments behave identically.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/** Chart type aliases → reference file mapping */
|
|
12
|
+
export const CHART_TYPE_MAP = {
|
|
13
|
+
// XY family
|
|
14
|
+
"xy": "xy", "line": "xy", "area": "xy", "bar": "xy", "column": "xy",
|
|
15
|
+
"candlestick": "xy", "ohlc": "xy", "scatter": "xy", "bubble": "xy",
|
|
16
|
+
"stacked": "xy", "stacked-bar": "xy", "stacked-column": "xy",
|
|
17
|
+
"stacked-area": "xy", "range": "xy", "waterfall": "xy",
|
|
18
|
+
// Pie family
|
|
19
|
+
"pie": "pie", "donut": "pie", "funnel": "pie", "pyramid": "pie",
|
|
20
|
+
"semi-circle": "pie", "pictorial": "pie", "sliced": "pie",
|
|
21
|
+
// Map
|
|
22
|
+
"map": "map", "choropleth": "map", "geo": "map", "geographic": "map",
|
|
23
|
+
"world": "map", "country": "map", "bubble-map": "map", "point-map": "map",
|
|
24
|
+
// Hierarchy
|
|
25
|
+
"hierarchy": "hierarchy", "treemap": "hierarchy", "sunburst": "hierarchy",
|
|
26
|
+
"force-directed": "hierarchy", "pack": "hierarchy", "partition": "hierarchy",
|
|
27
|
+
"tree": "hierarchy", "org-chart": "hierarchy",
|
|
28
|
+
// Flow
|
|
29
|
+
"flow": "flow", "sankey": "flow", "chord": "flow", "arc": "flow",
|
|
30
|
+
"alluvial": "flow",
|
|
31
|
+
// Radar
|
|
32
|
+
"radar": "radar", "spider": "radar", "polar": "radar", "gauge": "radar",
|
|
33
|
+
"speedometer": "radar", "meter": "radar",
|
|
34
|
+
// Stock
|
|
35
|
+
"stock": "stock", "financial": "stock", "trading": "stock",
|
|
36
|
+
// Timeline
|
|
37
|
+
"timeline": "timeline", "serpentine": "timeline", "spiral": "timeline",
|
|
38
|
+
"curve": "timeline",
|
|
39
|
+
// Gantt
|
|
40
|
+
"gantt": "gantt", "project-timeline": "gantt",
|
|
41
|
+
// Word cloud
|
|
42
|
+
"wordcloud": "wordcloud", "word-cloud": "wordcloud", "tag-cloud": "wordcloud",
|
|
43
|
+
// Venn
|
|
44
|
+
"venn": "venn",
|
|
45
|
+
// UI
|
|
46
|
+
"ui": "ui-elements", "ui-elements": "ui-elements", "button": "ui-elements",
|
|
47
|
+
"slider": "ui-elements", "controls": "ui-elements",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function parseSections(content) {
|
|
51
|
+
const sections = [];
|
|
52
|
+
const lines = content.split("\n");
|
|
53
|
+
let currentHeading = "";
|
|
54
|
+
let currentBody = [];
|
|
55
|
+
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
const headingMatch = line.match(/^(#{1,3})\s+(.+)/);
|
|
58
|
+
if (headingMatch) {
|
|
59
|
+
const level = headingMatch[1].length;
|
|
60
|
+
const title = headingMatch[2].trim();
|
|
61
|
+
if (level <= 2) {
|
|
62
|
+
// Major section boundary (# or ##)
|
|
63
|
+
if (currentHeading || currentBody.length > 0) {
|
|
64
|
+
sections.push({ heading: currentHeading, body: currentBody.join("\n") });
|
|
65
|
+
}
|
|
66
|
+
currentHeading = title;
|
|
67
|
+
currentBody = [];
|
|
68
|
+
} else {
|
|
69
|
+
// Sub-heading (###) — include in current section body
|
|
70
|
+
currentBody.push(line);
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
currentBody.push(line);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (currentHeading || currentBody.length > 0) {
|
|
77
|
+
sections.push({ heading: currentHeading, body: currentBody.join("\n") });
|
|
78
|
+
}
|
|
79
|
+
return sections;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Import sections carry two ### sub-blocks: one for ES modules, one for CDN /
|
|
84
|
+
* script tags. Return only the requested one (esm | html), or "" if not found.
|
|
85
|
+
*/
|
|
86
|
+
export function extractImportBlock(body, format) {
|
|
87
|
+
const lines = body.split("\n");
|
|
88
|
+
const wantEsm = format === "esm";
|
|
89
|
+
let capturing = false;
|
|
90
|
+
let captured = [];
|
|
91
|
+
let sawSub = false;
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
const sub = line.match(/^###\s+(.+)/);
|
|
94
|
+
if (sub) {
|
|
95
|
+
sawSub = true;
|
|
96
|
+
const h = sub[1].toLowerCase();
|
|
97
|
+
const isEsm = /es module|esm|typescript|import/.test(h);
|
|
98
|
+
// Note: "typescript" contains "script", so match "script tag"/"cdn", not bare "script".
|
|
99
|
+
const isCdn = /cdn|script tag|<script/.test(h);
|
|
100
|
+
capturing = wantEsm ? isEsm : isCdn;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (capturing) captured.push(line);
|
|
104
|
+
}
|
|
105
|
+
if (!sawSub) return ""; // no sub-blocks → caller falls back to full body
|
|
106
|
+
return captured.join("\n").trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build the in-memory content structures from raw markdown files.
|
|
111
|
+
*
|
|
112
|
+
* @param {{ skillFiles: Record<string,string>, extendedFiles: Record<string,string> }} raw
|
|
113
|
+
* skillFiles — name (no .md) → raw markdown for SKILL.md + references/*.md
|
|
114
|
+
* extendedFiles — path key (no .md) → raw markdown for everything under extended/
|
|
115
|
+
* @returns {{ docs: Map, extendedDocs: Map, examples: Map }}
|
|
116
|
+
*/
|
|
117
|
+
export function buildContent({ skillFiles = {}, extendedFiles = {} } = {}) {
|
|
118
|
+
/** @type {Map<string, {title: string, content: string, sections: {heading: string, body: string}[]}>} */
|
|
119
|
+
const docs = new Map();
|
|
120
|
+
/** @type {Map<string, {title: string, content: string, sections: {heading: string, body: string}[], path: string}>} */
|
|
121
|
+
const extendedDocs = new Map();
|
|
122
|
+
/** @type {Map<string, {title: string, category: string, content: string, source: string}>} */
|
|
123
|
+
const examples = new Map();
|
|
124
|
+
|
|
125
|
+
// Skill docs (SKILL.md + references/*.md)
|
|
126
|
+
for (const [name, content] of Object.entries(skillFiles)) {
|
|
127
|
+
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
128
|
+
const title = titleMatch ? titleMatch[1] : name;
|
|
129
|
+
docs.set(name, { title, content, sections: parseSections(content) });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Extended docs + examples (frontmatter-tagged markdown)
|
|
133
|
+
for (const [key, content] of Object.entries(extendedFiles)) {
|
|
134
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
135
|
+
const body = fmMatch ? fmMatch[2].trim() : content;
|
|
136
|
+
const fm = fmMatch ? fmMatch[1] : "";
|
|
137
|
+
const titleMatch = fm.match(/title:\s*"(.+?)"/);
|
|
138
|
+
const categoryMatch = fm.match(/category:\s*"(.+?)"/);
|
|
139
|
+
const sourceMatch = fm.match(/source:\s*"(.+?)"/);
|
|
140
|
+
const fallbackTitle = key.split("/").pop();
|
|
141
|
+
const title = titleMatch ? titleMatch[1] : fallbackTitle;
|
|
142
|
+
|
|
143
|
+
if (key.startsWith("examples/")) {
|
|
144
|
+
examples.set(key, {
|
|
145
|
+
title,
|
|
146
|
+
category: categoryMatch ? categoryMatch[1] : "",
|
|
147
|
+
content: body,
|
|
148
|
+
source: sourceMatch ? sourceMatch[1] : "",
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
extendedDocs.set(key, {
|
|
152
|
+
title,
|
|
153
|
+
content: body,
|
|
154
|
+
sections: parseSections(body),
|
|
155
|
+
path: key,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { docs, extendedDocs, examples };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Keyword search over the skill docs and (optionally) the extended docs.
|
|
165
|
+
* @param {{docs: Map, extendedDocs: Map}} content
|
|
166
|
+
*/
|
|
167
|
+
export function searchDocs(content, query, maxResults = 10, scope = "skill") {
|
|
168
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
169
|
+
const results = [];
|
|
170
|
+
|
|
171
|
+
// Skill docs (SKILL.md + references/*.md)
|
|
172
|
+
for (const [name, doc] of content.docs) {
|
|
173
|
+
for (const section of doc.sections) {
|
|
174
|
+
const text = (section.heading + " " + section.body).toLowerCase();
|
|
175
|
+
const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
|
|
176
|
+
if (score > 0) {
|
|
177
|
+
results.push({
|
|
178
|
+
file: name,
|
|
179
|
+
source: "skill",
|
|
180
|
+
docTitle: doc.title,
|
|
181
|
+
heading: section.heading,
|
|
182
|
+
body: section.body.trim(),
|
|
183
|
+
// Nudge skill results slightly ahead of extended on ties — the skill
|
|
184
|
+
// is the curated layer; extended is the exhaustive API reference.
|
|
185
|
+
score: score + 0.1,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Extended docs (charts/, concepts/, getting-started/, reference/<class>) —
|
|
192
|
+
// this is where the per-class API + defaults live. Only scanned when scope=all.
|
|
193
|
+
if (scope === "all") {
|
|
194
|
+
for (const [key, doc] of content.extendedDocs) {
|
|
195
|
+
for (const section of doc.sections) {
|
|
196
|
+
const text = (section.heading + " " + section.body).toLowerCase();
|
|
197
|
+
const score = terms.reduce((s, t) => s + (text.includes(t) ? 1 : 0), 0);
|
|
198
|
+
if (score > 0) {
|
|
199
|
+
results.push({
|
|
200
|
+
file: key,
|
|
201
|
+
source: "extended",
|
|
202
|
+
docTitle: doc.title,
|
|
203
|
+
heading: section.heading,
|
|
204
|
+
body: section.body.trim(),
|
|
205
|
+
score,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
results.sort((a, b) => b.score - a.score);
|
|
213
|
+
return results.slice(0, maxResults);
|
|
214
|
+
}
|