tool registry, claude tool awareness, and model selector fix
- Add agent-agnostic tool registry (tool-registry.ts) that discovers tools from disk - Embed tool-loader extension as platform infrastructure (ensure-tool-loader.ts) - Inject tool context into Claude prompts on first message - Add marketplace tool sync (sync-marketplace.ts) - Fix model selector defaulting to claude-code when no model explicitly selected - Exclude tool-loader-source.ts from tsconfig (Pi-specific deps) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { DATA_PATH } from './data-path';
|
||||
|
||||
type ToolInput = {
|
||||
type: string;
|
||||
description: string;
|
||||
values?: string;
|
||||
optional?: boolean;
|
||||
};
|
||||
|
||||
export type ToolDefinition = {
|
||||
name: string;
|
||||
label: string;
|
||||
description: string;
|
||||
language: string;
|
||||
inputs: Record<string, ToolInput>;
|
||||
implementationPath: string;
|
||||
body: string;
|
||||
};
|
||||
|
||||
type ToolMeta = {
|
||||
name: string;
|
||||
label: string;
|
||||
description: string;
|
||||
language: string;
|
||||
inputs: Record<string, ToolInput>;
|
||||
};
|
||||
|
||||
function parseFrontmatter(content: string): { meta: Partial<ToolMeta>; body: string } {
|
||||
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
||||
if (!match) return { meta: {}, body: content };
|
||||
|
||||
const yamlBlock = match[1]!;
|
||||
const body = match[2]!;
|
||||
const meta: Record<string, unknown> = {};
|
||||
|
||||
const lines = yamlBlock.split('\n');
|
||||
let currentKey: string | null = null;
|
||||
let currentObj: Record<string, unknown> | null = null;
|
||||
let currentSubKey: string | null = null;
|
||||
let currentSubObj: Record<string, unknown> | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
const topMatch = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
||||
if (topMatch && !line.startsWith(' ')) {
|
||||
if (currentSubObj && currentSubKey && currentObj) {
|
||||
currentObj[currentSubKey] = currentSubObj;
|
||||
currentSubObj = null;
|
||||
currentSubKey = null;
|
||||
}
|
||||
if (currentObj && currentKey) {
|
||||
meta[currentKey] = currentObj;
|
||||
currentObj = null;
|
||||
currentKey = null;
|
||||
}
|
||||
const [, key, value] = topMatch;
|
||||
if (!value || value.trim() === '') {
|
||||
currentKey = key!;
|
||||
currentObj = {};
|
||||
} else {
|
||||
meta[key!] = value.trim();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const midMatch = line.match(/^ (\w[\w-]*):\s*(.*)$/);
|
||||
if (midMatch && currentObj !== null) {
|
||||
if (currentSubObj && currentSubKey) {
|
||||
currentObj[currentSubKey] = currentSubObj;
|
||||
currentSubObj = null;
|
||||
currentSubKey = null;
|
||||
}
|
||||
const [, key, value] = midMatch;
|
||||
if (!value || value.trim() === '') {
|
||||
currentSubKey = key!;
|
||||
currentSubObj = {};
|
||||
} else {
|
||||
currentObj[key!] = value.trim();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const deepMatch = line.match(/^ (\w[\w-]*):\s*(.*)$/);
|
||||
if (deepMatch && currentSubObj !== null) {
|
||||
const [, key, value] = deepMatch;
|
||||
currentSubObj[key!] = value!.trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
const arrayMatch = line.match(/^ - (.+)$/);
|
||||
if (arrayMatch && currentSubObj !== null) {
|
||||
const key = Object.keys(currentSubObj).at(-1);
|
||||
if (key) {
|
||||
const arr = currentSubObj[key];
|
||||
if (Array.isArray(arr)) {
|
||||
arr.push(arrayMatch[1]!.trim());
|
||||
} else {
|
||||
currentSubObj[key] = [arrayMatch[1]!.trim()];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSubObj && currentSubKey && currentObj) {
|
||||
currentObj[currentSubKey] = currentSubObj;
|
||||
}
|
||||
if (currentObj && currentKey) {
|
||||
meta[currentKey] = currentObj;
|
||||
}
|
||||
|
||||
return { meta: meta as Partial<ToolMeta>, body };
|
||||
}
|
||||
|
||||
function discoverToolsInDir(dir: string): ToolDefinition[] {
|
||||
if (!existsSync(dir)) return [];
|
||||
|
||||
const tools: ToolDefinition[] = [];
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
|
||||
const toolDir = join(dir, entry.name);
|
||||
const toolMdPath = join(toolDir, 'TOOL.md');
|
||||
if (!existsSync(toolMdPath)) continue;
|
||||
|
||||
const indexTs = join(toolDir, 'index.ts');
|
||||
const indexJs = join(toolDir, 'index.js');
|
||||
const entryFile = existsSync(indexTs) ? indexTs : existsSync(indexJs) ? indexJs : null;
|
||||
if (!entryFile) continue;
|
||||
|
||||
const content = readFileSync(toolMdPath, 'utf-8');
|
||||
const { meta, body } = parseFrontmatter(content);
|
||||
|
||||
if (!meta.name || !meta.description) continue;
|
||||
|
||||
tools.push({
|
||||
name: meta.name,
|
||||
label: meta.label ?? meta.name,
|
||||
description: meta.description,
|
||||
language: meta.language ?? 'typescript',
|
||||
inputs: (meta.inputs ?? {}) as Record<string, ToolInput>,
|
||||
implementationPath: entryFile,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
export function discoverTools(email: string): ToolDefinition[] {
|
||||
const globalDir = join(DATA_PATH, 'tools');
|
||||
const userDir = join(DATA_PATH, email, 'tools');
|
||||
|
||||
const globalTools = discoverToolsInDir(globalDir);
|
||||
const userTools = discoverToolsInDir(userDir);
|
||||
|
||||
// User tools override global tools with the same name
|
||||
const seen = new Map<string, ToolDefinition>();
|
||||
for (const tool of globalTools) {
|
||||
seen.set(tool.name, tool);
|
||||
}
|
||||
for (const tool of userTools) {
|
||||
seen.set(tool.name, tool);
|
||||
}
|
||||
|
||||
return Array.from(seen.values());
|
||||
}
|
||||
Reference in New Issue
Block a user