CodeCraft AI began in late 2023 as an open custom GPT to give self-taught developers a free alternative to $20/month AI subscriptions. Today, that experiment has evolved into Dropout Agent: an autonomous developer companion running straight in your browser and terminal. Read workspaces, modify code, and verify test suites with zero subscription fees.
src/auth/token.ts, add isTokenExpired() helper with defensive error checks, and verify tests pass. isTokenExpired() in src/auth/token.ts with defensive error handling for malformed tokens. All 4 unit tests in tests/token.test.ts are passing. Evolution
Pasting individual snippets into a browser chat window strips away compiler feedback, package manifests, and project relationships. Here is how an autonomous agent fixes that.
Instead of asking you to copy code back and forth, the agent reads and writes directly into your workspace files, preserving project imports and type definitions.
Normal LLMs guess. CodeCraft AI executes your test runner (npm test or pytest). If a test fails, it inspects the error trace and fixes the code automatically before finishing.
Safety by design. Path resolution ensures the model never accesses sensitive system locations outside your designated project root, blocking malicious directory traversal attempts.
Construct an autonomous, project-aware coding companion in under 200 lines of TypeScript using Node 22 LTS, Vercel AI SDK, and Anthropic tool calling.
User Prompt: "Add slugify helper to string-utils and verify tests pass"
│
▼
[Agent Controller (Vercel AI SDK)] ───► Evaluates available tools
│
├── Step 1: Tool Call `read_file` (src/string-utils.ts)
│ └── Tool Result: Returns existing source code
│
├── Step 2: Tool Call `write_file` (adds slugify function and export)
│ └── Tool Result: File updated on disk
│
├── Step 3: Tool Call `run_command` (npm test tests/string-utils.test.ts)
│ └── Tool Result: PASS 1 test passed (14ms)
│
▼
Final Response: "Added slugify() with unicode normalization. Tests pass." We use Node 22 LTS with native ES modules and TypeScript 5.7. The Vercel AI SDK provides high-level primitives for multi-step tool resolution (maxSteps), streaming token delivery, and provider abstractions.
# Initialize project
mkdir dev-companion && cd dev-companion
npm init -y
# Install Vercel AI SDK, Anthropic provider, and Zod
npm install ai @ai-sdk/anthropic zod dotenv
npm install -D typescript @types/node tsx
# Initialize TypeScript configuration
npx tsc --initStore your API key in a local .env file:
ANTHROPIC_API_KEY=sk-ant-api03-xxxx
WORKSPACE_ROOT=/Users/username/projects/my-appThe companion requires three deterministic operations: reading file contents, writing updated source files, and executing validation commands. Each tool uses Zod schemas to ensure the model supplies strictly typed arguments.
// src/tools.ts
import { tool } from 'ai';
import { z } from 'zod';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
const ROOT_DIR = path.resolve(process.env['WORKSPACE_ROOT'] || process.cwd());
// Prevent path traversal outside the project directory
function assertSafePath(targetPath: string): string {
const resolved = path.resolve(ROOT_DIR, targetPath);
if (!resolved.startsWith(ROOT_DIR)) {
throw new Error(`Access denied: Path ${targetPath} points outside workspace.`);
}
return resolved;
}
export const workspaceTools = {
readFile: tool({
description: 'Read the UTF-8 text contents of a workspace file.',
parameters: z.object({
filePath: z.string().describe('Relative path to the target file from workspace root'),
}),
execute: async ({ filePath }) => {
try {
const fullPath = assertSafePath(filePath);
const content = await fs.readFile(fullPath, 'utf-8');
return { success: true, content };
} catch (err: any) {
return { success: false, error: err.message };
}
},
}),
writeFile: tool({
description: 'Write complete source code to a workspace file. Overwrites existing content.',
parameters: z.object({
filePath: z.string().describe('Relative path to the target file from workspace root'),
content: z.string().describe('The complete file content to write'),
}),
execute: async ({ filePath, content }) => {
try {
const fullPath = assertSafePath(filePath);
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, content, 'utf-8');
return { success: true, message: `Wrote ${content.length} characters to ${filePath}` };
} catch (err: any) {
return { success: false, error: err.message };
}
},
}),
runCommand: tool({
description: 'Run a shell command within the workspace. Restricted to build, test, and lint runners.',
parameters: z.object({
command: z.string().describe('Shell command to execute, such as npm test or tsc --noEmit'),
}),
execute: async ({ command }) => {
// Defensive check: block destructive commands
const banned = ['rm -rf', 'mkfs', 'dd ', ':(){ :|:& };:', 'curl', 'wget'];
if (banned.some((term) => command.includes(term))) {
return { success: false, error: 'Command rejected: Contains banned shell instructions.' };
}
try {
const { stdout, stderr } = await execAsync(command, { cwd: ROOT_DIR, timeout: 30000 });
return { success: true, stdout: stdout.slice(0, 4000), stderr: stderr.slice(0, 2000) };
} catch (err: any) {
return {
success: false,
exitCode: err.code,
stdout: err.stdout?.slice(0, 2000) || '',
stderr: err.stderr?.slice(0, 2000) || err.message,
};
}
},
}),
}; By default, models generate a single completion. With Vercel AI SDK's streamText and maxSteps, the framework automatically executes tool requests returned by the model, feeds tool outputs back into the conversation, and continues prompting until the model delivers its final answer.
// src/agent.ts
import { streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { workspaceTools } from './tools.js';
import * as dotenv from 'dotenv';
dotenv.config();
export async function runCodingTask(instruction: string): Promise<void> {
const systemPrompt = `You are a Principal Software Engineer companion running in the user's terminal.
Your task is to inspect code, make precise edits, and verify your changes with existing test suites.
Strict Rules:
1. Always read existing files before proposing edits.
2. Do not emit markdown code blocks when you intend to edit files; invoke the writeFile tool instead.
3. After modifying any file, invoke runCommand with 'npm test' or the project's typechecker.
4. If a test fails, read the stack trace, formulate a hypothesis, and fix the code.
5. Keep final summaries concise. Detail what changed and confirmed test results.`;
console.log(`\n[Agent Task]: ${instruction}\n`);
const result = streamText({
model: anthropic('claude-3-5-sonnet-20241022'),
system: systemPrompt,
prompt: instruction,
tools: workspaceTools,
maxSteps: 6, // Allows up to 6 back-and-forth tool call cycles
onStepFinish: (step) => {
if (step.toolCalls.length > 0) {
for (const call of step.toolCalls) {
console.log(`[Tool Call]: ${call.toolName}(${JSON.stringify(call.args)})`);
}
}
if (step.toolResults.length > 0) {
for (const res of step.toolResults) {
console.log(`[Tool Result]: ${JSON.stringify(res.result).slice(0, 120)}...`);
}
}
},
});
// Stream final textual synthesis directly to stdout
for await (const delta of result.textStream) {
process.stdout.write(delta);
}
console.log('\n');
}Let us execute the agent against a concrete repository task: adding input validation to an authentication helper and testing the change.
// src/index.ts
import { runCodingTask } from './agent.js';
async function main() {
const task = `
1. Inspect src/auth/token.ts.
2. Add an expiration check helper function 'isTokenExpired(token: string): boolean'.
3. Handle invalid base64 and malformed payload strings gracefully by returning true.
4. Run 'npm test tests/token.test.ts' to verify implementation.
`;
await runCodingTask(task);
}
main().catch(console.error);When executed via npx tsx src/index.ts, the agent runs the sequence autonomously:
[Agent Task]: Inspect src/auth/token.ts and add expiration check...
[Tool Call]: readFile({"filePath":"src/auth/token.ts"})
[Tool Result]: {"success":true,"content":"import jwt from 'jsonwebtoken';\n..."}...
[Tool Call]: writeFile({"filePath":"src/auth/token.ts","content":"..."})
[Tool Result]: {"success":true,"message":"Wrote 1420 characters to src/auth/token.ts"}...
[Tool Call]: runCommand({"command":"npm test tests/token.test.ts"})
[Tool Result]: {"success":true,"stdout":"PASS tests/token.test.ts\n✓ isTokenExpired handles expired JWTs..."}...
Implemented isTokenExpired() in src/auth/token.ts with defensive try/catch blocks for malformed tokens. All 4 unit tests in tests/token.test.ts are passing.path.resolve and verify that target paths start with your project root directory to block path traversal attacks.git checkout . restores sanity instantly.Clarifications
Join self-taught developers across India escaping tutorial loops and building production-grade software with AI assistance.