import { pathToFileURL } from 'node:url'; import { createMessage, textOf, configFromEnv } from './client.mjs'; const tools = [{ name: 'get_github_repository', description: 'Read current public GitHub repository metadata. Returns stars, open issues (including pull requests), description, and URL. This does not modify the repository.', input_schema: { type: 'object', properties: { owner: { type: 'string', description: 'Repository owner, such as anthropics.' }, repo: { type: 'string', description: 'Repository name, such as claude-code.' } }, required: ['owner', 'repo'], additionalProperties: false } }]; export async function runTool(call, fetchImpl = fetch) { if (call.name !== 'get_github_repository') throw new Error('Unknown tool.'); const { owner, repo } = call.input || {}; const valid = (value) => typeof value === 'string' && /^[A-Za-z0-9_.-]{1,100}$/.test(value) && value !== '.' && value !== '..'; if (!valid(owner) || !valid(repo)) throw new Error('Invalid repository coordinates.'); // Fixed host and validated path prevent model-supplied arbitrary URL requests. const response = await fetchImpl(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, { headers: { accept: 'application/vnd.github+json', 'user-agent': 'HowUseClaude-Tutorial', 'x-github-api-version': '2022-11-28' }, signal: AbortSignal.timeout(10000) }); if (!response.ok) { await response.body?.cancel(); throw new Error(`GitHub HTTP ${response.status}.`); } const data = await response.json(); return JSON.stringify({ name: data.full_name, description: data.description, stars: data.stargazers_count, open_issues_including_prs: data.open_issues_count, url: data.html_url, retrieved_at: new Date().toISOString() }); } export async function answerWithTools(question, config, dependencies = {}) { const send = dependencies.send || createMessage; const execute = dependencies.execute || runTool; const messages = [{ role: 'user', content: question }]; // Each round can incur model usage. Bound both rounds and calls per response. for (let round = 0; round < 5; round += 1) { const response = await send({ ...config, tools, messages }); if (response.stop_reason === 'end_turn') return textOf(response); if (response.stop_reason !== 'tool_use') throw new Error(`Incomplete response: ${response.stop_reason}`); const calls = response.content.filter((block) => block.type === 'tool_use'); if (!calls.length || calls.length > 4) throw new Error('Unexpected tool call count.'); messages.push({ role: 'assistant', content: response.content }); const results = []; for (const call of calls) { try { results.push({ type: 'tool_result', tool_use_id: call.id, content: await execute(call) }); } catch (error) { results.push({ type: 'tool_result', tool_use_id: call.id, is_error: true, content: error.message }); } } // All results immediately follow the assistant tool calls, in one user turn. messages.push({ role: 'user', content: results }); } throw new Error('Tool round limit reached; inspect the task before trying again.'); } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { try { console.log(await answerWithTools('Look up anthropics/claude-code on GitHub. Report its stars and source URL. Distinguish open issues from the combined issue/PR count.', configFromEnv())); } catch (error) { console.error(error.message); process.exitCode = 1; } }