mirror of
https://github.com/sveltejs/ai-tools.git
synced 2026-07-04 11:42:22 +08:00
Compare commits
4 Commits
opencode-s
...
@sveltejs/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6df3ebe568 | ||
|
|
27a2fc5653 | ||
|
|
5cd99d8234 | ||
|
|
e9f19199cb |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@sveltejs/mcp': patch
|
||||
---
|
||||
|
||||
feat: display similar result & error at the end
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@sveltejs/opencode": patch
|
||||
---
|
||||
|
||||
feat(opencode): mcp enabled option is passed to opencode
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@sveltejs/opencode': patch
|
||||
---
|
||||
|
||||
feat: allow enabling a specific skill in opencode plugin
|
||||
116
.github/workflows/sync-docs-skills.yml
vendored
Normal file
116
.github/workflows/sync-docs-skills.yml
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
name: Sync Skills
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
sync-skills:
|
||||
if: github.repository == 'sveltejs/ai-tools'
|
||||
name: Sync skills from svelte.dev
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24
|
||||
package-manager-cache: false # pnpm is not installed yet
|
||||
|
||||
- name: Install pnpm
|
||||
shell: bash
|
||||
run: |
|
||||
PNPM_VER=$(jq -r '.packageManager | if .[0:5] == "pnpm@" then .[5:] else "packageManager in package.json does not start with pnpm@\n" | halt_error(1) end' package.json)
|
||||
echo installing pnpm version "$PNPM_VER"
|
||||
npm i -g "pnpm@$PNPM_VER"
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24
|
||||
package-manager-cache: true # caches pnpm via packageManager field in package.json
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
|
||||
|
||||
- name: Clone svelte.dev
|
||||
run: git clone --depth 2 https://github.com/sveltejs/svelte.dev.git "${{ runner.temp }}/svelte.dev"
|
||||
|
||||
- name: Discover changed skill files
|
||||
id: discover
|
||||
env:
|
||||
SVELTE_DEV_ROOT: ${{ runner.temp }}/svelte.dev
|
||||
run: |
|
||||
skill_files=$(git -C "$SVELTE_DEV_ROOT" diff --name-only --diff-filter=ACMR HEAD~1 HEAD | grep '^apps/svelte.dev/content/docs/.*\.md$' | xargs -I{} grep -l '^skill: *true' "$SVELTE_DEV_ROOT/{}" || true)
|
||||
echo "skill_files=$skill_files" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Sync skills
|
||||
if: steps.discover.outputs.skill_files != ''
|
||||
env:
|
||||
SVELTE_DEV_ROOT: ${{ runner.temp }}/svelte.dev
|
||||
DOCS_PREFIX: apps/svelte.dev/content/docs/
|
||||
run: |
|
||||
for full_path in ${{ steps.discover.outputs.skill_files }}; do
|
||||
file="${full_path#$SVELTE_DEV_ROOT/}"
|
||||
name=$(grep '^name: ' "$full_path" | head -1 | sed 's/^name: *//')
|
||||
repo="${file#$DOCS_PREFIX}"
|
||||
repo="${repo#/}"
|
||||
repo="${repo%%/*}"
|
||||
|
||||
output_dir="tools/skills/$name"
|
||||
rm -rf "$output_dir"
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
pnpm resolve-references --file "$full_path" --repo "$repo" --output "$output_dir"
|
||||
done
|
||||
|
||||
- name: Sync plugins
|
||||
if: steps.discover.outputs.skill_files != ''
|
||||
run: |
|
||||
pnpm sync-claude-plugin
|
||||
pnpm sync-cursor-plugin
|
||||
pnpm sync-opencode-plugin
|
||||
pnpm generate-skill-docs
|
||||
pnpm bump-plugin-versions
|
||||
|
||||
- name: Check for changes
|
||||
id: git-check
|
||||
run: |
|
||||
git diff --exit-code -- tools/skills/ plugins/ packages/opencode/ documentation/docs/ || echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.git-check.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: sync skills from svelte.dev'
|
||||
branch: chore/sync-skills
|
||||
delete-branch: true
|
||||
title: 'chore: sync skills from svelte.dev'
|
||||
body: |
|
||||
## Summary
|
||||
Automatically synced skill markdown from `sveltejs/svelte.dev` into `tools/skills/`.
|
||||
|
||||
## Changes
|
||||
- Cloned `sveltejs/svelte.dev`
|
||||
- Filtered markdown files with `skill: true` frontmatter
|
||||
- Rebuilt synced skill folders with `scripts/resolve-references.ts`
|
||||
- Synced `plugins/claude/svelte/` (skills, agents)
|
||||
- Synced `plugins/cursor/svelte/` (skills, agents, rules)
|
||||
- Synced `packages/opencode/` (skills, instructions)
|
||||
- Updated documentation
|
||||
|
||||
## Generated by
|
||||
GitHub Action: Sync Skills
|
||||
labels: |
|
||||
chore
|
||||
automated
|
||||
@@ -29,7 +29,15 @@ The default configuration for the Svelte OpenCode plugin looks like this...
|
||||
"enabled": true
|
||||
},
|
||||
"subagent": {
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"agents": {
|
||||
"svelte-file-editor": {
|
||||
"model": "other-model", // defaults to the same as main agent,
|
||||
"temperature": 1, // default to unset
|
||||
"top_p": 0.7, // default to unset,
|
||||
"maxSteps": 20 // default to unlimited
|
||||
}
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"enabled": true // it can also be an array of all the skills to enable like ['svelte-core-bestpractices']
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
"sync-claude-plugin": "node scripts/sync-claude-plugin.ts",
|
||||
"sync-cursor-plugin": "node scripts/sync-cursor-plugin.ts",
|
||||
"sync-opencode-plugin": "node scripts/sync-opencode-plugin.ts",
|
||||
"bump-plugin-versions": "node scripts/bump-plugin-versions.ts"
|
||||
"bump-plugin-versions": "node scripts/bump-plugin-versions.ts",
|
||||
"resolve-references": "node scripts/resolve-references.ts",
|
||||
"postresolve-references": "pnpm format"
|
||||
},
|
||||
"keywords": [
|
||||
"svelte",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @sveltejs/mcp
|
||||
|
||||
## 0.1.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: display similar result & error at the end ([#161](https://github.com/sveltejs/ai-tools/pull/161))
|
||||
|
||||
## 0.1.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sveltejs/mcp",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"mcpName": "dev.svelte/mcp",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"subfolder": "packages/mcp-stdio",
|
||||
"source": "github"
|
||||
},
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"websiteUrl": "https://svelte.dev/docs/mcp/overview",
|
||||
"icons": [
|
||||
{
|
||||
@@ -25,7 +25,7 @@
|
||||
{
|
||||
"registryType": "npm",
|
||||
"identifier": "@sveltejs/mcp",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.21",
|
||||
"runtimeHint": "npx",
|
||||
"transport": {
|
||||
"type": "stdio"
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @sveltejs/opencode
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: merge user-configured svelte-file-editor agent settings ([#176](https://github.com/sveltejs/ai-tools/pull/176))
|
||||
|
||||
- feat(opencode): mcp enabled option is passed to opencode ([#171](https://github.com/sveltejs/ai-tools/pull/171))
|
||||
|
||||
- feat: allow enabling a specific skill in opencode plugin ([#174](https://github.com/sveltejs/ai-tools/pull/174))
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -4,6 +4,28 @@ import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import * as v from 'valibot';
|
||||
|
||||
// Schema for individual agent configuration
|
||||
const agent_config_schema = v.object({
|
||||
model: v.pipe(
|
||||
v.optional(v.string()),
|
||||
v.description('Model identifier for the agent (e.g., "anthropic/claude-sonnet-4-20250514")'),
|
||||
),
|
||||
temperature: v.pipe(
|
||||
v.optional(v.number()),
|
||||
v.description('Temperature setting for the agent (e.g., 0.7)'),
|
||||
),
|
||||
top_p: v.pipe(
|
||||
v.optional(v.number()),
|
||||
v.description(
|
||||
'Control response diversity with the top_p option. Alternative to temperature for controlling randomness.',
|
||||
),
|
||||
),
|
||||
maxSteps: v.pipe(
|
||||
v.optional(v.number()),
|
||||
v.description('Maximum number of steps the agent can take (e.g., 10)'),
|
||||
),
|
||||
});
|
||||
|
||||
const default_config = {
|
||||
mcp: {
|
||||
type: 'remote' as 'remote' | 'local',
|
||||
@@ -11,6 +33,7 @@ const default_config = {
|
||||
},
|
||||
subagent: {
|
||||
enabled: true,
|
||||
agents: {} as Record<string, v.InferInput<typeof agent_config_schema>>,
|
||||
},
|
||||
instructions: {
|
||||
enabled: true,
|
||||
@@ -36,6 +59,7 @@ export const config_schema = v.object({
|
||||
v.optional(
|
||||
v.object({
|
||||
enabled: v.optional(v.boolean()),
|
||||
agents: v.optional(v.record(v.string(), agent_config_schema)),
|
||||
}),
|
||||
),
|
||||
v.description('Configuration for the subagent. You can choose if it should be enabled or not.'),
|
||||
@@ -49,9 +73,6 @@ export const config_schema = v.object({
|
||||
v.description(
|
||||
'Configuration for the automatic AGENTS.md injection. You can choose if it should be enabled or not.',
|
||||
),
|
||||
v.description(
|
||||
'Configuration for the automatic AGENTS.md injection. You can choose if it should be enabled or not.',
|
||||
),
|
||||
),
|
||||
skills: v.pipe(
|
||||
v.optional(
|
||||
@@ -138,8 +159,12 @@ function merge_with_defaults(user_config: Partial<McpConfig>): McpConfig {
|
||||
...user_config.mcp,
|
||||
},
|
||||
subagent: {
|
||||
...default_config.subagent,
|
||||
enabled: default_config.subagent.enabled,
|
||||
...user_config.subagent,
|
||||
agents: {
|
||||
...default_config.subagent.agents,
|
||||
...user_config.subagent?.agents,
|
||||
},
|
||||
},
|
||||
instructions: {
|
||||
...default_config.instructions,
|
||||
@@ -177,7 +202,11 @@ export function get_mcp_config(ctx: PluginInput) {
|
||||
if (parsed.success) {
|
||||
merged = {
|
||||
mcp: { ...merged.mcp, ...parsed.output.mcp },
|
||||
subagent: { ...merged.subagent, ...parsed.output.subagent },
|
||||
subagent: {
|
||||
...merged.subagent,
|
||||
...parsed.output.subagent,
|
||||
agents: { ...merged.subagent?.agents, ...parsed.output.subagent?.agents },
|
||||
},
|
||||
instructions: { ...merged.instructions, ...parsed.output.instructions },
|
||||
skills: { ...merged.skills, ...parsed.output.skills },
|
||||
};
|
||||
|
||||
@@ -73,7 +73,7 @@ export const svelte_plugin: Plugin = async (ctx) => {
|
||||
}
|
||||
if (mcp_config.subagent?.enabled !== false) {
|
||||
// we add the editor subagent that will be used when editing Svelte files to prevent wasting context on the main agent
|
||||
input.agent['svelte-file-editor'] = {
|
||||
const default_config: (typeof input.agent)[string] = {
|
||||
color: '#ff3e00',
|
||||
mode: 'subagent',
|
||||
prompt: `You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with \`get_documentation\` and validating the code with \`svelte_autofixer\`. If the autofixer returns any issue or suggestions try to solve them.
|
||||
@@ -150,6 +150,27 @@ After completing your work, provide:
|
||||
[`${svelte_mcp_name}_*`]: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Get per-agent config from svelte.json (if any)
|
||||
const svelte_file_editor_config = mcp_config.subagent?.agents?.['svelte-file-editor'];
|
||||
|
||||
// Configure agent from svelte.json only
|
||||
// Priority: svelte.json agent config > defaults
|
||||
input.agent['svelte-file-editor'] = {
|
||||
...default_config,
|
||||
...(svelte_file_editor_config?.model !== undefined && {
|
||||
model: svelte_file_editor_config.model,
|
||||
}),
|
||||
...(svelte_file_editor_config?.temperature !== undefined && {
|
||||
temperature: svelte_file_editor_config.temperature,
|
||||
}),
|
||||
...(svelte_file_editor_config?.maxSteps !== undefined && {
|
||||
maxSteps: svelte_file_editor_config.maxSteps,
|
||||
}),
|
||||
...(svelte_file_editor_config?.top_p !== undefined && {
|
||||
top_p: svelte_file_editor_config.top_p,
|
||||
}),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sveltejs/opencode",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/sveltejs/ai-tools#readme",
|
||||
|
||||
@@ -22,6 +22,43 @@
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"agents": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"svelte-file-editor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "Model identifier for the agent (e.g., \"anthropic/claude-sonnet-4-20250514\")"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"description": "Temperature setting for the agent (e.g., 0.7)"
|
||||
},
|
||||
"top_p": {
|
||||
"type": "number",
|
||||
"description": "Control response diversity with the top_p option. Alternative to temperature for controlling randomness."
|
||||
},
|
||||
"maxSteps": {
|
||||
"type": "number",
|
||||
"description": "Maximum number of steps the agent can take (e.g., 10)"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
|
||||
@@ -3,6 +3,15 @@ import { config_schema } from '../config.js';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// Read agent names from tools/agents/*.md files
|
||||
function get_agent_names(agents_dir: string) {
|
||||
if (!fs.existsSync(agents_dir)) return [];
|
||||
return fs
|
||||
.readdirSync(agents_dir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
||||
.map((entry) => entry.name.replace(/\.md$/, ''));
|
||||
}
|
||||
|
||||
function get_skill_names(skills_dir: string) {
|
||||
if (!fs.existsSync(skills_dir)) return [];
|
||||
return fs
|
||||
@@ -32,4 +41,20 @@ if (skill_names.length > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
// Post-process: inject known agent names for intellisense
|
||||
// This is the JSON Schema equivalent of `"a" | "b" | (string & {})` —
|
||||
// editors will autocomplete the known names but any string is still valid.
|
||||
const agents_dir = path.resolve('../../tools/agents');
|
||||
const agent_names = get_agent_names(agents_dir);
|
||||
|
||||
if (agent_names.length > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const agents = (json_schema as any).properties?.subagent?.properties?.agents;
|
||||
if (agents) {
|
||||
agents.propertyNames = {
|
||||
anyOf: [{ enum: agent_names }, { type: 'string' }],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.resolve('./schema.json'), JSON.stringify(json_schema, null, '\t'));
|
||||
|
||||
276
scripts/resolve-references.ts
Normal file
276
scripts/resolve-references.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
file: { type: 'string', short: 'f' },
|
||||
repo: { type: 'string', short: 'r' },
|
||||
output: { type: 'string', short: 'o' },
|
||||
},
|
||||
});
|
||||
|
||||
const { file, repo, output } = values;
|
||||
|
||||
if (!file || !repo || !output) {
|
||||
console.error(
|
||||
'Usage: resolve-references --file <path-or-content> --repo <repo> --output <folder>',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export function remove_llm_ignore_blocks(content: string): string {
|
||||
return content.replace(/<!--\s*llm-ignore-start\s*-->[\s\S]*?<!--\s*llm-ignore-end\s*-->/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the input string is a file path or raw markdown content.
|
||||
* If it's a file, reads and returns its content. Otherwise returns the string as-is.
|
||||
*/
|
||||
async function get_content(input: string) {
|
||||
try {
|
||||
const stat = await fs.stat(input);
|
||||
if (stat.isFile()) {
|
||||
return await fs.readFile(input, 'utf-8');
|
||||
}
|
||||
} catch {
|
||||
// not a file path — treat as raw content
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a section from markdown content based on a heading id (hash).
|
||||
* Finds the heading whose text (lowercased, spaces replaced with `-`) matches
|
||||
* the hash and returns everything from that heading up to the next heading of
|
||||
* the same or higher level.
|
||||
*/
|
||||
function extract_section(content: string, hash: string) {
|
||||
const lines = content.split('\n');
|
||||
let start_index = -1;
|
||||
let heading_level = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]!;
|
||||
const heading_match = line.match(/^(#{1,6})\s+(.+)/);
|
||||
if (!heading_match) continue;
|
||||
|
||||
const level = heading_match[1]!.length;
|
||||
const text = heading_match[2]!;
|
||||
const slug = text.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
if (slug === hash.toLowerCase()) {
|
||||
start_index = i;
|
||||
heading_level = level;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (start_index !== -1 && level <= heading_level) {
|
||||
return lines.slice(start_index, i).join('\n').trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (start_index !== -1) {
|
||||
return lines.slice(start_index).join('\n').trim();
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the `title`, `skill`, and `NOTE` fields from markdown frontmatter, if present.
|
||||
* Removes the entire frontmatter block if they were the only fields.
|
||||
*/
|
||||
function remove_frontmatter_unneeded_fields(content: string) {
|
||||
const frontmatter_match = content.match(/^---\n([\s\S]*?)\n---\n?/);
|
||||
if (!frontmatter_match) return content;
|
||||
|
||||
const frontmatter = frontmatter_match[1]!;
|
||||
const lines = frontmatter.split('\n').filter((line) => !line.match(/^(title|skill|NOTE)\s*:/));
|
||||
|
||||
if (lines.length === 0) {
|
||||
// frontmatter is now empty — remove the whole block
|
||||
return content.slice(frontmatter_match[0].length);
|
||||
}
|
||||
|
||||
return `---\n${lines.join('\n')}\n---\n` + content.slice(frontmatter_match[0].length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a file-safe name from a URL path segment.
|
||||
* e.g. "some/deep/path" -> "path"
|
||||
*/
|
||||
function derive_name(link: string) {
|
||||
const without_hash = link.split('#')[0]!;
|
||||
const segments = without_hash.split('/').filter(Boolean);
|
||||
return segments[segments.length - 1] ?? 'reference';
|
||||
}
|
||||
|
||||
const content = remove_llm_ignore_blocks(
|
||||
remove_frontmatter_unneeded_fields(await get_content(file)),
|
||||
);
|
||||
|
||||
// Match markdown links that are either:
|
||||
// 1. Relative paths (not starting with http://, https://, mailto:, #, or /)
|
||||
// 2. Absolute /docs/ paths (e.g. /docs/svelte/each)
|
||||
const relative_link_regex = /\[([^\]]*)\]\((?!https?:\/\/|mailto:|#|\/)([^)]+)\)/g;
|
||||
const docs_link_regex = /\[([^\]]*)\]\((\/docs\/[^)]+)\)/g;
|
||||
|
||||
interface Link_Info {
|
||||
full_match: string;
|
||||
text: string;
|
||||
href: string;
|
||||
hash: string | undefined;
|
||||
clean_path: string;
|
||||
is_absolute_docs: boolean;
|
||||
}
|
||||
|
||||
const links: Link_Info[] = [];
|
||||
|
||||
let match;
|
||||
while ((match = relative_link_regex.exec(content)) !== null) {
|
||||
const href = match[2]!;
|
||||
const hash_index = href.indexOf('#');
|
||||
const has_hash = hash_index !== -1;
|
||||
|
||||
links.push({
|
||||
full_match: match[0],
|
||||
text: match[1]!,
|
||||
href,
|
||||
hash: has_hash ? href.slice(hash_index + 1) : undefined,
|
||||
clean_path: has_hash ? href.slice(0, hash_index) : href,
|
||||
is_absolute_docs: false,
|
||||
});
|
||||
}
|
||||
|
||||
while ((match = docs_link_regex.exec(content)) !== null) {
|
||||
const href = match[2]!;
|
||||
const hash_index = href.indexOf('#');
|
||||
const has_hash = hash_index !== -1;
|
||||
|
||||
links.push({
|
||||
full_match: match[0],
|
||||
text: match[1]!,
|
||||
href,
|
||||
hash: has_hash ? href.slice(hash_index + 1) : undefined,
|
||||
clean_path: has_hash ? href.slice(0, hash_index) : href,
|
||||
is_absolute_docs: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (links.length === 0) {
|
||||
console.log('No relative links found in the markdown.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`Found ${links.length} relative link(s) to resolve.`);
|
||||
|
||||
const references_dir = path.join(output, 'references');
|
||||
await fs.mkdir(references_dir, { recursive: true });
|
||||
|
||||
let updated_content = content;
|
||||
|
||||
// Track names we've already used to avoid collisions
|
||||
const used_names = new Map<string, number>();
|
||||
|
||||
for (const link of links) {
|
||||
const base_name = derive_name(link.clean_path);
|
||||
const count = used_names.get(base_name) ?? 0;
|
||||
used_names.set(base_name, count + 1);
|
||||
const name = count > 0 ? `${base_name}-${count}` : base_name;
|
||||
|
||||
// For absolute /docs/ links, fetch directly from svelte.dev (supports cross-repo links).
|
||||
// For relative links, prepend /docs/{repo}/.
|
||||
const url = link.is_absolute_docs
|
||||
? `https://svelte.dev${link.clean_path}/llms.txt`
|
||||
: `https://svelte.dev/docs/${repo}/${link.clean_path}/llms.txt`;
|
||||
|
||||
console.log(`Fetching: ${url}${link.hash ? ` (section: #${link.hash})` : ''}`);
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(` Warning: ${response.status} ${response.statusText} for ${url}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let fetched_content = await response.text();
|
||||
|
||||
if (link.hash) {
|
||||
fetched_content = extract_section(fetched_content, link.hash);
|
||||
}
|
||||
|
||||
const ref_filename = `${name}.md`;
|
||||
const ref_path = path.join(references_dir, ref_filename);
|
||||
|
||||
await fs.writeFile(ref_path, remove_llm_ignore_blocks(remove_cut_preambles(fetched_content)));
|
||||
console.log(` Saved: references/${ref_filename}`);
|
||||
|
||||
// Replace the link in the markdown
|
||||
const new_link = `[${link.text}](references/${ref_filename})`;
|
||||
updated_content = updated_content.replace(link.full_match, new_link);
|
||||
} catch (error) {
|
||||
console.warn(` Error fetching ${url}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In fenced code blocks, removes everything from the start of the block
|
||||
* up to and including a `// ---cut---` comment. If no such comment exists
|
||||
* the code block is left unchanged.
|
||||
*/
|
||||
function remove_cut_preambles(content: string) {
|
||||
const lines = content.split('\n');
|
||||
const result: string[] = [];
|
||||
let in_code_block = false;
|
||||
let code_block_buffer: string[] = [];
|
||||
let fence_line = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (!in_code_block && line.match(/^```\w*$/)) {
|
||||
in_code_block = true;
|
||||
fence_line = line;
|
||||
code_block_buffer = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_code_block && line.match(/^```$/)) {
|
||||
// End of code block — check if there was a cut comment
|
||||
const cut_index = code_block_buffer.findIndex((l) => l.match(/^\s*\/\/\s*---cut---\s*$/));
|
||||
|
||||
result.push(fence_line);
|
||||
if (cut_index !== -1) {
|
||||
result.push(...code_block_buffer.slice(cut_index + 1));
|
||||
} else {
|
||||
result.push(...code_block_buffer);
|
||||
}
|
||||
result.push(line);
|
||||
|
||||
in_code_block = false;
|
||||
code_block_buffer = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_code_block) {
|
||||
code_block_buffer.push(line);
|
||||
} else {
|
||||
result.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// If file ends mid-code-block, flush as-is
|
||||
if (in_code_block) {
|
||||
result.push(fence_line);
|
||||
result.push(...code_block_buffer);
|
||||
}
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
|
||||
// Write the updated markdown content to the output folder
|
||||
updated_content = remove_cut_preambles(updated_content);
|
||||
|
||||
const output_filename = path.join(output, 'SKILL.md');
|
||||
await fs.writeFile(output_filename, updated_content);
|
||||
console.log(`\nUpdated markdown written to: ${output_filename}`);
|
||||
Reference in New Issue
Block a user