Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions main.js

Large diffs are not rendered by default.

76 changes: 2 additions & 74 deletions src/tool-icons.ts

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions src/tool-svgs.ts

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions vscode/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
*.vsix
5 changes: 5 additions & 0 deletions vscode/.vscodeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
src/**
esbuild.config.mjs
tsconfig.json
node_modules/**
*.tmp.mjs
661 changes: 661 additions & 0 deletions vscode/bun.lock

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions vscode/esbuild.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import esbuild from "esbuild";
import { fileURLToPath } from "url";
import { dirname, join } from "path";

const here = dirname(fileURLToPath(import.meta.url));
const production = process.argv[2] === "production";

const obsidianShim = {
name: "obsidian-shim",
setup(build) {
build.onResolve({ filter: /^obsidian$/ }, () => ({
path: join(here, "src", "obsidian-shim.ts"),
}));
},
};

const ctx = await esbuild.context({
entryPoints: [join(here, "src", "extension.ts")],
bundle: true,
external: ["vscode"],
format: "cjs",
platform: "node",
target: "node18",
outfile: join(here, "dist", "extension.js"),
plugins: [obsidianShim],
sourcemap: production ? false : "inline",
minify: production,
logLevel: "info",
});

if (production) {
await ctx.rebuild();
process.exit(0);
} else {
await ctx.watch();
}
1 change: 1 addition & 0 deletions vscode/media/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions vscode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"name": "agentfiles-vscode",
"displayName": "agentfiles",
"description": "Browse, open, and install AI agent skills across 18 tools from one panel",
"version": "0.1.0",
"publisher": "railly",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/Railly/agentfiles",
"directory": "vscode"
},
"engines": {
"vscode": "^1.90.0"
},
"categories": ["Other"],
"activationEvents": [],
"main": "./dist/extension.js",
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "agentfiles",
"title": "agentfiles",
"icon": "media/icon.svg"
}
]
},
"views": {
"agentfiles": [
{
"id": "agentfilesSkills",
"name": "Skills"
}
]
},
"commands": [
{
"command": "agentfiles.refresh",
"title": "agentfiles: Refresh skills",
"icon": "$(refresh)"
},
{
"command": "agentfiles.installSkill",
"title": "agentfiles: Install skill from GitHub"
},
{
"command": "agentfiles.openSkill",
"title": "agentfiles: Open skill file"
}
],
"menus": {
"view/title": [
{
"command": "agentfiles.refresh",
"when": "view == agentfilesSkills",
"group": "navigation"
},
{
"command": "agentfiles.installSkill",
"when": "view == agentfilesSkills"
}
]
}
},
"scripts": {
"build": "node esbuild.config.mjs production",
"dev": "node esbuild.config.mjs",
"typecheck": "tsc --noEmit",
"package": "vsce package --no-dependencies"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.14.0",
"@types/vscode": "^1.90.0",
"@vscode/vsce": "^3.2.1",
"esbuild": "^0.24.0",
"js-yaml": "^4.1.1",
"typescript": "^5.6.0"
}
}
147 changes: 147 additions & 0 deletions vscode/src/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import * as vscode from "vscode";
import { scanAll } from "../../src/scanner";
import { TOOL_CONFIGS } from "../../src/tool-configs";
import { TOOL_SVGS } from "../../src/tool-svgs";
import { DEFAULT_SETTINGS, type SkillItem } from "../../src/types";
import { installSkillAsync, TOOL_TO_AGENT, VALID_AGENTS } from "../../src/marketplace";

type TreeNode = ToolNode | SkillNode;

interface ToolNode {
kind: "tool";
id: string;
name: string;
count: number;
}

interface SkillNode {
kind: "skill";
item: SkillItem;
toolId: string;
}

class SkillsProvider implements vscode.TreeDataProvider<TreeNode> {
private emitter = new vscode.EventEmitter<TreeNode | undefined>();
readonly onDidChangeTreeData = this.emitter.event;
private byTool = new Map<string, SkillItem[]>();
private iconCache = new Map<string, vscode.Uri>();

constructor(private storageUri: vscode.Uri) {}

async refresh(): Promise<void> {
const items = scanAll(DEFAULT_SETTINGS);
this.byTool.clear();
for (const item of items.values()) {
for (const toolId of item.tools) {
const list = this.byTool.get(toolId) || [];
list.push(item);
this.byTool.set(toolId, list);
}
}
for (const list of this.byTool.values()) {
list.sort((a, b) => a.name.localeCompare(b.name));
}
await Promise.all([...this.byTool.keys()].map((toolId) => this.ensureToolIcon(toolId)));
this.emitter.fire(undefined);
}

private async ensureToolIcon(toolId: string): Promise<void> {
if (this.iconCache.has(toolId)) return;
const svg = TOOL_SVGS[toolId];
if (!svg) return;
const content = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${svg.viewBox}" width="16" height="16" fill="none" color="#9da5b4">${svg.paths}</svg>`;
const uri = vscode.Uri.joinPath(this.storageUri, `${toolId}.svg`);
await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(content));
this.iconCache.set(toolId, uri);
}

getChildren(node?: TreeNode): TreeNode[] {
if (!node) {
return TOOL_CONFIGS.filter((t) => this.byTool.has(t.id))
.map((t) => ({
kind: "tool" as const,
id: t.id,
name: t.name,
count: this.byTool.get(t.id)?.length ?? 0,
}));
}
if (node.kind === "tool") {
return (this.byTool.get(node.id) || []).map((item) => ({
kind: "skill" as const,
item,
toolId: node.id,
}));
}
return [];
}

getTreeItem(node: TreeNode): vscode.TreeItem {
if (node.kind === "tool") {
const el = new vscode.TreeItem(node.name, vscode.TreeItemCollapsibleState.Collapsed);
el.description = String(node.count);
el.contextValue = "tool";
const icon = this.toolIcon(node.id);
if (icon) el.iconPath = icon;
return el;
}
const el = new vscode.TreeItem(node.item.name, vscode.TreeItemCollapsibleState.None);
el.description = node.item.type;
el.tooltip = node.item.description || node.item.filePath;
el.resourceUri = vscode.Uri.file(node.item.filePath);
el.command = {
command: "vscode.open",
title: "Open skill",
arguments: [vscode.Uri.file(node.item.filePath)],
};
el.contextValue = "skill";
return el;
}

private toolIcon(toolId: string): vscode.Uri | undefined {
return this.iconCache.get(toolId);
}
}

export async function activate(context: vscode.ExtensionContext): Promise<void> {
await vscode.workspace.fs.createDirectory(context.globalStorageUri);
const provider = new SkillsProvider(context.globalStorageUri);

context.subscriptions.push(
vscode.window.registerTreeDataProvider("agentfilesSkills", provider),
vscode.commands.registerCommand("agentfiles.refresh", () => provider.refresh()),
vscode.commands.registerCommand("agentfiles.installSkill", async () => {
const source = await vscode.window.showInputBox({
prompt: "GitHub source (owner/repo or owner/repo@skill)",
placeHolder: "vercel-labs/agent-skills",
});
if (!source) return;
const installedToolIds = TOOL_CONFIGS.filter((t) => t.isInstalled()).map((t) => t.id);
const agentIds = [...new Set(installedToolIds.map((t) => TOOL_TO_AGENT[t]).filter(Boolean))];
const picks = await vscode.window.showQuickPick(
VALID_AGENTS.filter((a) => agentIds.includes(a.id)).map((a) => ({
label: a.label,
id: a.id,
picked: true,
})),
{ canPickMany: true, title: "Install for which agents?" },
);
if (!picks || picks.length === 0) return;
await vscode.window.withProgress(
{ location: vscode.ProgressLocation.Notification, title: `Installing ${source}...` },
async () => {
const result = await installSkillAsync(source, picks.map((p) => p.id));
if (result.success) {
vscode.window.showInformationMessage(`Installed ${source}`);
await provider.refresh();
} else {
vscode.window.showErrorMessage(`Install failed: ${result.output.slice(0, 300)}`);
}
},
);
}),
);

await provider.refresh();
}

export function deactivate(): void {}
46 changes: 46 additions & 0 deletions vscode/src/obsidian-shim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { load } from "js-yaml";

export function parseYaml(text: string): unknown {
return load(text);
}

export interface RequestUrlParam {
url: string;
method?: string;
headers?: Record<string, string>;
body?: string;
throw?: boolean;
}

export interface RequestUrlResponse {
status: number;
text: string;
json: unknown;
arrayBuffer: ArrayBuffer;
headers: Record<string, string>;
}

export async function requestUrl(param: RequestUrlParam | string): Promise<RequestUrlResponse> {
const p: RequestUrlParam = typeof param === "string" ? { url: param } : param;
const res = await fetch(p.url, {
method: p.method || "GET",
headers: p.headers,
body: p.body,
});
const buf = await res.arrayBuffer();
const text = new TextDecoder().decode(buf);
if (p.throw !== false && res.status >= 400) {
throw new Error(`Request failed, status ${res.status}`);
}
let json: unknown = null;
try {
json = JSON.parse(text);
} catch {
json = null;
}
const headers: Record<string, string> = {};
res.headers.forEach((v, k) => {
headers[k] = v;
});
return { status: res.status, text, json, arrayBuffer: buf, headers };
}
27 changes: 27 additions & 0 deletions vscode/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"paths": {
"obsidian": ["./src/obsidian-shim.ts"]
}
},
"include": ["src/**/*.ts", "../src/**/*.ts"],
"exclude": [
"../src/main.ts",
"../src/views/**",
"../src/editor/**",
"../src/settings.ts",
"../src/watcher.ts",
"../src/store.ts",
"../src/conversations/**",
"../src/tool-icons.ts",
"../src/utils/shell.ts"
]
}