Created
April 3, 2026 23:46
-
-
Save muniter/219c5fde3d4bfa4b6f16c31407e88d8f to your computer and use it in GitHub Desktop.
stately/agents claude code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| javierlopez in (v2)~ -> npx tsx example/file_searcher.ts | |
| What file are you looking for? I json file from my Downloads foer | |
| Summary: Found JSON files in your Downloads folder inside the `pixel-perfect-match-604-main` project. No standalone JSON files exist at the top level of Downloads — all meaningful JSON files are inside that downloaded project's root (excluding node_modules). | |
| Candidates: | |
| 1. /Users/javierlopez/Downloads/pixel-perfect-match-604-main/package.json | |
| Root-level package.json of the downloaded project — the most commonly referenced JSON file in a frontend project. | |
| 2. /Users/javierlopez/Downloads/pixel-perfect-match-604-main/tsconfig.json | |
| Root TypeScript configuration file for the downloaded project. | |
| 3. /Users/javierlopez/Downloads/pixel-perfect-match-604-main/tsconfig.app.json | |
| App-specific TypeScript config in the downloaded project. | |
| 4. /Users/javierlopez/Downloads/pixel-perfect-match-604-main/tsconfig.node.json | |
| Node-specific TypeScript config in the downloaded project. | |
| Type a number to select a file, "refine" to continue searching, or press enter to cancel: 1 | |
| Done: | |
| { | |
| "selected": { | |
| "path": "/Users/javierlopez/Downloads/pixel-perfect-match-604-main/package.json", | |
| "reason": "Root-level package.json of the downloaded project — the most commonly referenced JSON file in a frontend project." | |
| } | |
| } | |
| javierlopez in (v2)~ -> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import dotenv from 'dotenv'; | |
| import { promisify } from 'node:util'; | |
| import { execFile } from 'node:child_process'; | |
| import { createInterface } from 'node:readline/promises'; | |
| import { stdin as input, stdout as output } from 'node:process'; | |
| import { z } from 'zod'; | |
| import { | |
| createAdapter, | |
| createAgentMachine, | |
| decide, | |
| } from '../src/index.js'; | |
| dotenv.config({ override: true }); | |
| const execFileAsync = promisify(execFile); | |
| const fileCandidateSchema = z.object({ | |
| path: z.string(), | |
| reason: z.string(), | |
| }); | |
| const searchResultSchema = z.object({ | |
| summary: z.string(), | |
| candidates: z.array(fileCandidateSchema).max(5), | |
| }); | |
| type FileCandidate = z.infer<typeof fileCandidateSchema>; | |
| type SearchResult = z.infer<typeof searchResultSchema>; | |
| function buildSearchPrompt(args: { query: string; refinements: string[] }): string { | |
| return [ | |
| 'You are a file finder working inside a code repository.', | |
| 'Use the available tools to inspect the workspace and find files that best match the user request.', | |
| 'Return 1 to 5 candidate files. Prefer precise matches over broad guesses.', | |
| '', | |
| `User request: ${args.query}`, | |
| args.refinements.length > 0 | |
| ? `Refinements so far:\n${args.refinements.map((item, index) => `${index + 1}. ${item}`).join('\n')}` | |
| : 'Refinements so far: none', | |
| '', | |
| 'For each candidate, explain briefly why it is relevant.', | |
| 'Only return files that actually exist in the workspace.', | |
| ].join('\n'); | |
| } | |
| async function runClaude(args: { | |
| prompt: string; | |
| sessionId: string | null; | |
| tools: string; | |
| schema: Record<string, unknown>; | |
| model: string; | |
| workdir: string; | |
| }): Promise<{ session_id: string; structured_output: unknown }> { | |
| const cliArgs = [ | |
| '-p', | |
| '--output-format', | |
| 'json', | |
| '--model', | |
| args.model, | |
| '--permission-mode', | |
| 'bypassPermissions', | |
| '--add-dir', | |
| args.workdir, | |
| '--tools', | |
| args.tools, | |
| '--json-schema', | |
| JSON.stringify(args.schema), | |
| ...(args.sessionId ? ['--resume', args.sessionId] : []), | |
| args.prompt, | |
| ]; | |
| const { stdout, stderr } = await execFileAsync('claude', cliArgs, { | |
| cwd: args.workdir, | |
| maxBuffer: 5 * 1024 * 1024, | |
| }); | |
| const payloadText = stdout.trim(); | |
| if (!payloadText) { | |
| throw new Error(stderr.trim() || 'Claude returned no output.'); | |
| } | |
| const payload = JSON.parse(payloadText) as { | |
| is_error?: boolean; | |
| errors?: string[]; | |
| session_id?: string; | |
| structured_output?: unknown; | |
| }; | |
| if (payload.is_error) { | |
| throw new Error(payload.errors?.join('\n') || 'Claude returned an error.'); | |
| } | |
| if (!payload.session_id) { | |
| throw new Error('Claude response did not include a session_id.'); | |
| } | |
| return { | |
| session_id: payload.session_id, | |
| structured_output: payload.structured_output, | |
| }; | |
| } | |
| function createClaudeCodeAdapter(workdir: string) { | |
| let sessionId: string | null = null; | |
| return createAdapter({ | |
| async decide({ prompt, options, model }) { | |
| const searchOption = options.showResults; | |
| if (!searchOption?.schema) { | |
| throw new Error('ClaudeCode adapter expects a showResults option with a schema.'); | |
| } | |
| const cliResult = await runClaude({ | |
| prompt, | |
| sessionId, | |
| tools: 'Glob,Grep,Read', | |
| schema: { | |
| type: 'object', | |
| properties: { | |
| summary: { type: 'string' }, | |
| candidates: { | |
| type: 'array', | |
| maxItems: 5, | |
| items: { | |
| type: 'object', | |
| properties: { | |
| path: { type: 'string' }, | |
| reason: { type: 'string' }, | |
| }, | |
| required: ['path', 'reason'], | |
| additionalProperties: false, | |
| }, | |
| }, | |
| }, | |
| required: ['summary', 'candidates'], | |
| additionalProperties: false, | |
| }, | |
| model: model === 'claude-code' ? 'sonnet' : model, | |
| workdir, | |
| }); | |
| sessionId = cliResult.session_id; | |
| const parsed = searchResultSchema.parse(cliResult.structured_output); | |
| return { | |
| choice: 'showResults', | |
| data: parsed, | |
| }; | |
| }, | |
| }); | |
| } | |
| const searchAdapter = createClaudeCodeAdapter(process.cwd()); | |
| const fileSearcherMachine = createAgentMachine({ | |
| id: 'file-searcher', | |
| schemas: { | |
| input: z.object({ | |
| rootDir: z.string(), | |
| }), | |
| events: { | |
| 'user.search': z.object({ | |
| query: z.string(), | |
| }), | |
| 'user.selectCandidate': z.object({ | |
| index: z.number().int().min(0), | |
| }), | |
| 'user.refineSearch': z.object({ | |
| feedback: z.string(), | |
| }), | |
| 'user.cancel': z.object({}), | |
| }, | |
| }, | |
| context: (input: { rootDir: string }) => ({ | |
| rootDir: input.rootDir, | |
| query: '', | |
| refinements: [] as string[], | |
| summary: null as string | null, | |
| candidates: [] as FileCandidate[], | |
| selected: null as FileCandidate | null, | |
| }), | |
| adapter: searchAdapter, | |
| initial: 'collectingRequest', | |
| states: { | |
| collectingRequest: { | |
| on: { | |
| 'user.search': ({ event }) => ({ | |
| target: 'searching', | |
| context: { | |
| query: event.query, | |
| refinements: [], | |
| summary: null, | |
| candidates: [], | |
| selected: null, | |
| }, | |
| }), | |
| 'user.cancel': () => ({ target: 'cancelled' }), | |
| }, | |
| }, | |
| searching: decide({ | |
| model: 'claude-code', | |
| prompt: ({ context }) => | |
| buildSearchPrompt({ | |
| query: context.query as string, | |
| refinements: context.refinements as string[], | |
| }), | |
| options: { | |
| showResults: { | |
| description: 'Return the best matching file candidates for the search request.', | |
| schema: searchResultSchema, | |
| }, | |
| }, | |
| onDone: ({ result }) => ({ | |
| target: 'reviewingCandidates', | |
| context: { | |
| summary: result.data.summary, | |
| candidates: result.data.candidates, | |
| }, | |
| }), | |
| }), | |
| reviewingCandidates: { | |
| on: { | |
| 'user.selectCandidate': ({ event, context }) => { | |
| const candidate = context.candidates[event.index]; | |
| if (!candidate) { | |
| throw new Error(`Candidate index ${event.index} is out of bounds.`); | |
| } | |
| return { | |
| target: 'done', | |
| context: { | |
| selected: candidate, | |
| }, | |
| }; | |
| }, | |
| 'user.refineSearch': ({ event, context }) => ({ | |
| target: 'searching', | |
| context: { | |
| refinements: [...context.refinements, event.feedback], | |
| }, | |
| }), | |
| 'user.cancel': () => ({ target: 'cancelled' }), | |
| }, | |
| }, | |
| done: { | |
| type: 'final', | |
| output: ({ context }) => ({ | |
| selected: context.selected, | |
| }), | |
| }, | |
| cancelled: { | |
| type: 'final', | |
| output: ({ context }) => ({ | |
| cancelled: true, | |
| lastSummary: context.summary, | |
| }), | |
| }, | |
| }, | |
| }); | |
| async function main() { | |
| const rl = createInterface({ input, output }); | |
| let state = fileSearcherMachine.getInitialState({ rootDir: process.cwd() }); | |
| try { | |
| for (;;) { | |
| const result = await fileSearcherMachine.execute(state); | |
| if (result.status === 'error') { | |
| console.error('\nSearch failed:', result.error); | |
| return; | |
| } | |
| if (result.status === 'done') { | |
| console.log('\nDone:'); | |
| console.log(JSON.stringify(result.output, null, 2)); | |
| return; | |
| } | |
| if (String(result.value) === 'collectingRequest') { | |
| const query = await rl.question('What file are you looking for? '); | |
| if (!query.trim()) { | |
| state = fileSearcherMachine.transition(result.state, { type: 'user.cancel' }); | |
| continue; | |
| } | |
| state = fileSearcherMachine.transition(result.state, { | |
| type: 'user.search', | |
| query, | |
| }); | |
| continue; | |
| } | |
| if (String(result.value) === 'reviewingCandidates') { | |
| console.log(`\nSummary: ${result.context.summary ?? 'No summary provided.'}`); | |
| console.log('\nCandidates:'); | |
| result.context.candidates.forEach((candidate, index) => { | |
| console.log(`${index + 1}. ${candidate.path}`); | |
| console.log(` ${candidate.reason}`); | |
| }); | |
| const answer = await rl.question( | |
| '\nType a number to select a file, "refine" to continue searching, or press enter to cancel: ' | |
| ); | |
| if (!answer.trim()) { | |
| state = fileSearcherMachine.transition(result.state, { type: 'user.cancel' }); | |
| continue; | |
| } | |
| if (answer.trim().toLowerCase() === 'refine') { | |
| const feedback = await rl.question('How should Claude refine the search? '); | |
| state = fileSearcherMachine.transition(result.state, { | |
| type: 'user.refineSearch', | |
| feedback, | |
| }); | |
| continue; | |
| } | |
| const index = Number(answer) - 1; | |
| if (!Number.isInteger(index)) { | |
| console.log('Please enter a valid number or "refine".'); | |
| continue; | |
| } | |
| state = fileSearcherMachine.transition(result.state, { | |
| type: 'user.selectCandidate', | |
| index, | |
| }); | |
| } | |
| } | |
| } finally { | |
| rl.close(); | |
| } | |
| } | |
| main().catch((error) => { | |
| console.error(error); | |
| process.exitCode = 1; | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment