· 8 min read
DIY AI: How to Build Your Own AI Tool for JavaScript Development from Scratch
A practical, step-by-step guide to building a personalized AI assistant for JavaScript projects. Learn how to ingest your codebase, create embeddings, run retrieval-augmented generation, integrate with editors, and follow best practices for performance, privacy, and accuracy.
Introduction
Developers increasingly want AI tools that understand their codebase, style, and constraints. Instead of relying solely on general-purpose assistants, you can build a tailored AI tool that helps with code search, refactoring suggestions, generating tests, documentation, and more - all specifically tuned to your JavaScript project.
This guide walks you through building a practical Retrieval-Augmented Generation (RAG) system for JavaScript development using Node.js. We’ll cover architecture, ingestion, embeddings, vector search, prompt design, a working code example (OpenAI + Pinecone), local alternatives, editor integration ideas, and key best practices.
Why RAG for code?
- Projects contain domain-specific knowledge (architecture, coding patterns, config) that generic models don’t know.
- RAG lets you combine a language model with an indexed snapshot of your project so answers are grounded in your repo.
- You can control what information the AI sees and keep private code local or encrypted.
High-level architecture
- Project ingestion: scan files, extract relevant text (code, README, comments).
- Chunking: split large files into context-sized chunks with overlap.
- Embeddings: convert chunks to vectors.
- Vector store: index vectors for fast similarity search.
- Query pipeline: embed user query, retrieve nearest chunks, construct prompt, call LLM, post-process result.
- UI: CLI, web app, or editor extension (e.g., VS Code).
Step 0 - Decide constraints and risks
- Privacy: Will code be sent to a third-party API? If not, use local models or self-hosted vector DBs.
- Cost: Embeddings and LLM calls add expense. Cache embeddings and retrieved contexts when possible.
- Freshness: How often will the index be rebuilt? On each commit, nightly, or on demand?
Tools & options
Cloud-hosted (fast to build):
- Embeddings + LLM: OpenAI (Embeddings + Chat) - https://platform.openai.com/docs/guides/embeddings
- Vector DB: Pinecone - https://www.pinecone.io/docs/
- Or use LangChain.js for orchestration - https://js.langchain.com/docs/
Local / open-source alternatives:
- Embeddings: sentence-transformers (Python) or Open-Source embedding models via Hugging Face.
- LLM: Llama 2, Mistral, or other open models via local runtimes (llama.cpp, GGML, or on-prem inference).
- Vector DB: FAISS (https://faiss.ai), Milvus, or PGVector (Postgres).
Step 1 - Project ingestion (Node.js)
Goal: read files, extract meaningful text, and produce metadata for each chunk.
Key ideas:
- Include: source code, doc comments, README, package.json, tests, example usage.
- Exclude: build artifacts, node_modules, large binary files.
- Keep metadata: file path, language, start/end lines, commit hash.
Example ingestion script (simplified):
// ingest.js (Node.js)
const fs = require('fs');
const path = require('path');
function walk(dir, fileList = []) {
fs.readdirSync(dir).forEach(file => {
const fp = path.join(dir, file);
if (fs.statSync(fp).isDirectory()) {
if (file === 'node_modules' || file === '.git') return;
walk(fp, fileList);
} else {
// Only ingest common source files
if (/\.(js|jsx|ts|tsx|json|md)$/i.test(fp)) fileList.push(fp);
}
});
return fileList;
}
function readFiles(root) {
const files = walk(root);
return files.map(fp => ({ path: fp, text: fs.readFileSync(fp, 'utf8') }));
}
module.exports = { readFiles };
Step 2 - Chunking code effectively
Why chunk? Embedding models and LLMs have context limits. Proper chunks increase retrieval accuracy.
Best practices:
- Chunk by function or logical unit when possible by parsing AST (better) or by fixed token/line windows that respect boundaries.
- Use overlap (e.g., 50–100 tokens) so related context isn’t cut off.
Simple line-chunking example:
function chunkText(text, maxLines = 120, overlap = 10) {
const lines = text.split('\n');
const chunks = [];
for (let i = 0; i < lines.length; i += maxLines - overlap) {
const slice = lines.slice(i, i + maxLines).join('\n');
chunks.push(slice);
}
return chunks;
}
For JS specifically, consider using a parser like Recast/Esprima to break into function-level chunks for richer results.
Step 3 - Embeddings
Embeddings turn text into numeric vectors for semantic search.
Cloud example (OpenAI embeddings):
- Model: text-embedding-3-small or -large depending on budget/accuracy.
- In Node.js you can call the OpenAI SDK to create embeddings for each chunk.
Example (Node.js + openai):
// embeddings.js
import OpenAI from 'openai';
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function embedTexts(texts) {
const res = await client.embeddings.create({
model: 'text-embedding-3-small',
input: texts,
});
return res.data.map(d => d.embedding);
}
Local alternative:
- Use sentence-transformers in Python to create embeddings offline (better for privacy). See https://huggingface.co/docs/sentence-transformers.
Step 4 - Vector store
Store embeddings with metadata for retrieval. Options:
- Managed: Pinecone (easy), Milvus, Qdrant.
- Self-hosted: FAISS (Python), PGVector (Postgres).
Pinecone upsert example (Node.js):
// pinecone-upload.js
import { PineconeClient } from '@pinecone-database/pinecone';
const pinecone = new PineconeClient();
await pinecone.init({
apiKey: process.env.PINECONE_API_KEY,
environment: process.env.PINECONE_ENV,
});
const index = pinecone.Index('your-index-name');
// upsert vectors: [{ id, values, metadata }]
await index.upsert({ upsertRequest: { vectors: vectorsToUpsert } });
Step 5 - Query pipeline (RAG)
When the developer asks a question (e.g., “How does authentication work in this project?”), the pipeline:
- Embed the query.
- Use vector DB to retrieve top-K relevant chunks.
- Construct a prompt that includes system instructions + retrieved chunks + the developer’s question.
- Call the LLM to produce an answer.
Prompt template example:
System: You are a helpful assistant with deep knowledge of the provided code excerpts. Answer concisely and show code locations where applicable.
Context:
<RETRIEVED CHUNK 1>
---
<RETRIEVED CHUNK 2>
User question: <USER QUESTION>
Answer (include file path and line references when possible):
Node.js query example (OpenAI Chat + Pinecone retrieval):
// query.js (simplified)
import OpenAI from 'openai';
import { PineconeClient } from '@pinecone-database/pinecone';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const pinecone = new PineconeClient();
await pinecone.init({
apiKey: process.env.PINECONE_API_KEY,
environment: process.env.PINECONE_ENV,
});
const index = pinecone.Index('your-index-name');
async function answerQuestion(question) {
// 1) Embed query
const qEmbResp = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: question,
});
const qVec = qEmbResp.data[0].embedding;
// 2) Retrieve top K
const retrieved = await index.query({
queryRequest: { topK: 5, vector: qVec, includeMetadata: true },
});
// 3) Build prompt
const context = retrieved.matches
.map(m => `FILE: ${m.metadata.path}\n${m.metadata.text}`)
.join('\n---\n');
const prompt = `You are an assistant that answers questions about the following JavaScript project files. Use them to answer precisely.\n\n${context}\n\nQuestion: ${question}\n\nAnswer:`;
// 4) Call chat/completion
const chat = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: 'You are a helpful JS dev assistant.' },
{ role: 'user', content: prompt },
],
temperature: 0.0,
});
return chat.choices[0].message.content;
}
Note: adjust model names to your provider’s available models and pricing.
Step 6 - Build a developer-friendly UI
Options:
- CLI tool (fast to implement). Use commander.js or oclif.
- Web interface that highlights file snippets and links back to code.
- VS Code extension for in-editor queries and inline code actions. See https://code.visualstudio.com/api
VS Code extension idea:
- Add a command (e.g., “Ask project AI”) that opens an input box.
- Use the Query pipeline server (local or remote) and show results in a webview with file links and suggested edits.
Step 7 - Use cases and sample prompts
Examples to try with your new tool:
- “Find all places that call fetch without error handling.”
- “Suggest a refactor to reduce duplication in auth middleware (show diff).”
- “Write unit tests for src/utils/formatter.js with coverage for edge cases.”
- “Explain how session tokens are generated and where they’re validated.”
When you want the AI to produce code changes, ask for patch/diff output (e.g., unified diff or GitHub PR style).
Best practices and tips
- Chunk smartly: AST-based function chunks beat naive line-chunking for code tasks.
- Keep metadata: file path, start/end lines, commit hash so answers can reference exact locations.
- Temperature: use low temperature (0–0.3) for deterministic code tasks; higher for brainstorming.
- K and context length: retrieve enough chunks to cover the answer but not so much that you exceed token limits.
- Caching: cache embeddings and frequent query results to save cost and latency.
- Version your index: re-index on releases/major changes and keep metadata linking chunks to commit hashes.
- Rate limits and batching: batch embedding calls to reduce overhead.
- Safety & privacy: avoid sending secrets. Use filters or redact env/config files. Consider local embeddings/LLM if code is sensitive.
- Evaluation: create a test-suite of questions with expected answers or code changes to track regressions.
Local-only alternative (privacy-first)
If you must avoid external APIs, build a stack like:
- Embeddings: sentence-transformers (Python) or small HF models.
- Vector DB: FAISS or Milvus hosted locally.
- LLM: a local Llama 2 / Mistral model run via llama.cpp or a local containerized inference server.
This requires more ops work but avoids sending code to third parties.
Monitoring, logging, and observability
- Log which files are used to answer queries (for debugging and trust).
- Track latency and cost metrics since LLM usage can be expensive.
- Provide a “source of truth” view so users can see the exact snippets used for an answer.
Extending the tool
- Fine-tuning: If allowed and needed, fine-tune a model on your repo and commit history for more project-aligned behavior.
- Specialized agents: add code-execution sandboxing to run tests or validate patches before suggesting changes.
- Automated PR creation: implement a workflow that crafts a PR based on a suggested fix and runs CI.
Example end-to-end workflow (one minute to try):
- Run the ingestion script to build or update the index.
- Start a small API server exposing a /query endpoint that runs the RAG pipeline.
- Use a CLI or VS Code extension to call /query and display results.
Resources & references
- OpenAI Embeddings guide: https://platform.openai.com/docs/guides/embeddings
- OpenAI Chat Completions: https://platform.openai.com/docs/guides/chat
- Pinecone docs: https://www.pinecone.io/docs/
- LangChain.js: https://js.langchain.com/docs/
- FAISS: https://faiss.ai
- VS Code extension API: https://code.visualstudio.com/api
Conclusion
Building your own AI assistant for JavaScript development is a highly practical way to make your workflow more efficient and project-aware. Start small: index important files, use RAG for retrieval, and iterate on prompt templates and chunking strategy. As your tool matures, integrate it into your editor or CI pipeline and tune privacy, caching, and cost controls to fit your team’s needs.
If you follow the steps above and adopt the best practices, you’ll have a tailored AI that understands your codebase and helps you ship better software faster.