DIY Mini Cursor: Simple Creation Guide

Cursor is an AI-powered code editor you might already be familiar with. But have you ever paused to wonder how it actually works under the hood? What kind of "magic" powers an intelligent code companion? In this post, we’ll uncover the concept behind such tools and build a mini version of Cursor ourselves to truly understand the mechanics.
What is Agentic AI?
Before we dive into coding, it's important to understand the concept of AI Agents—a fundamental part of what makes tools like Cursor work.
At a high level, AI agents are intelligent systems enhanced with tools. These tools are built by us (developers) to extend the AI's native capabilities. While the base AI model provides reasoning, context understanding, and natural language generation, these agents can decide when and how to use specific tools to accomplish a goal.
Real-World Example: A Weather Agent
Let’s understand this with a practical use case.
Imagine you're building an AI agent that provides real-time weather updates.
By default, large language models (LLMs) like GPT or Gemini don’t have access to the internet or real-time data. But you can overcome this limitation by giving the model access to a tool—an external API endpoint that fetches live weather data.
How It Works
You can define a simple instruction like this for your AI agent:
"If someone asks for weather information, call the
/get-weather?city=<CITY_NAME>API and return the result."
Now, if someone says:
"What's the weather in Mohali right now?"
The AI will:
Detect the user intent (
weather inquiry).Trigger the API with the appropriate city (
Mohali).Parse the API response.
Respond with something like:
"The current temperature in Mohali is 23°C with clear skies."
This is the core of agentic AI—giving your LLM the autonomy to use tools intelligently.
Next Step: Make It Real
To bring this to life, you'll need an OpenAI API key (or you can use Gemini with a few small changes). We'll write a simple script where the AI uses an external weather API to answer real-time queries—just like an actual agent.
import os
import requests
import json
from dotenv import load_dotenv
from openai import OpenAI
# Load environment variables for openAI API Key
load_dotenv()
client = OpenAI() # Create an openAI Client
# Create an API to fetch real-time weather data
def get_weather(city: str):
print("⛏️Tool Called: get_weather for : ", city)
url = f"https://wttr.in/{city}?format=%C+%t"
response = requests.get(url)
if response.status_code == 200 :
return response.text
return "Something went wrong. Couldn't fetch weather"
# Make a dictionary of available tools
available_tools = {
"get_weather": {
"fn": get_weather,
"description": "Takes a city name as an input and returns the current weather for the city"
}
}
# Give a detailed system prompt to customize the behaviour of AI
system_prompt = f"""
You are an helpful AI Assistant who is specialized in resolving user query.
You work on start, plan, action, observe mode.
For the given user query and available tools, plan the step by step execution, based on the planning,
select the relevant tool from the available tool. and based on the tool selection you perform an action to call the tool.
Wait for the observation and based on the observation from the tool call resolve the user query.
Rules:
- Follow the Output JSON Format.
- Always perform one step at a time and wait for next input
- Carefully analyse the user query
Output JSON Format:
{{
"step": "string",
"content": "string",
"function": "The name of function if the step is action",
"input": "The input parameter for the function",
}}
Available Tools:
- get_weather: Takes a city name as an input and returns the current weather for the city
Example:
User Query: What is the weather of new york?
Output: {{ "step": "plan", "content": "The user is interseted in weather data of new york" }}
Output: {{ "step": "plan", "content": "From the available tools I should call get_weather" }}
Output: {{ "step": "action", "function": "get_weather", "input": "new york" }}
Output: {{ "step": "observe", "output": "12 Degree Cel" }}
Output: {{ "step": "output", "content": "The weather for new york seems to be 12 degrees." }}
"""
# A messages list to store the conversation
messages = [
{"role": "system", "content" : system_prompt}
]
#This is where all magic happens
while True:
user_query = input('> ')
messages.append({"role": "user", "content": user_query})
while True:
response = client.chat.completions.create(
model="openai/gpt-4o",
response_format={"type": "json_object"},
messages = messages,
)
parsed_response = json.loads(response.choices[0].message.content)
messages.append({"role": "assistant", "content": json.dumps(parsed_response)})
if parsed_response.get("step") == "plan":
print(f"🧠 Thinking: ", parsed_response.get("content"))
continue
if parsed_response.get("step") == "action":
tool_name = parsed_response.get("function")
if available_tools.get(tool_name, False) != False:
fn_output = available_tools[tool_name]["fn"](parsed_response.get("input"))
messages.append({"role": "assistant", "content": json.dumps({ "step": "observe", "output": fn_output})})
continue
if parsed_response.get("step") == "output":
print(f"🤖: {parsed_response.get("content")}")
break
Mini Cursor
Now, let’s take the weather agent concept one step further—and this is where it starts getting exciting.
Imagine replacing the weather API with an API that interacts with your own terminal. That’s right—your AI agent can now send commands directly to your system through a controlled backend API. This is the core idea behind building a mini version of Cursor.
How It Works
Here’s what happens behind the scenes:
The AI decides what needs to be done (e.g., create a folder, write a file, run a script).
It sends a request to your API.
Your API executes the command on your local machine (via shell or OS-level commands).
The result is sent back to the model, which presents the output or continues the workflow.
Example Actions
Let’s say the AI wants to:
Create a folder:
It calls the API → API runsmkdir my-folder→ Folder is created.Write a file:
It sends file content + path → API gets OS-level permission → File is written.Start a server:
It calls the API → API runsnpm start→ Server starts running.
In short, the LLM becomes an intelligent assistant that not only suggests code but also executes real commands, acting like an automated developer sidekick.
Ready to Try It?
Let’s turn this concept into code. Below is a script that allows your AI agent to run terminal commands securely via API.
import os
import subprocess
import platform
import shlex
import json
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI()
def run_command(command: str, background=False):
print("⛏️Tool Called: run_command for : ", command)
if not background:
# Run in foreground (blocking) - original behavior
result = os.system(command)
return result
else:
# Run in background (non-blocking)
# Commands like `npm start` need to keep running, If you run this command on primary terminal, It'll block your terminal and you'll not be able to chat further
try:
# Create a detached process based on the OS
if platform.system() == "Windows":
# For Windows, use CREATE_NEW_CONSOLE flag
full_command = f'start /min cmd /c "{command}"'
subprocess.Popen(full_command, shell=True)
print(f"Process started in background with Windows 'start' command")
return True
else:
# For Unix/Linux/Mac, use setsid to create new session
command_parts = shlex.split(command)
process = subprocess.Popen(
command_parts,
preexec_fn=os.setsid, # Detaches from parent process
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
print(f"Process started in background with PID: {process.pid}")
return True
except Exception as e:
print(f"Error running command in background: {e}")
return False
def write_to_file(input_json):
try:
if isinstance(input_json, str):
params = json.loads(input_json)
else:
params = input_json
filename = params.get("filename")
content = params.get("content")
print("⛏️Tool Called: write_to_file for : ", filename)
if not filename or content is None:
print("Error: Missing filename or content in parameters")
return False
os.makedirs(os.path.dirname(filename) or '.', exist_ok=True)
with open(filename, 'w') as file:
file.write(content)
return True
except Exception as e:
print(f"Error writing to file: {e}")
return False
available_tools = {
"run_command": {
"fn": run_command,
"description": "Takes two parameters, 'command: string' and 'background: boolean' as input and executes command on system. If command need to be keep running in background (like npm start), pass it as True and it'll return True, if execution was successful, else it's false by default and returns result of command."
},
"write_to_file": {
"fn": write_to_file,
"description": "Takes a JSON input with 'filename' and 'content' keys. Creates or overwrites the file with the specified content and returns True or False according to the status."
}
}
system_prompt = f"""
You are an helpful AI Assistant who is specialized in resolving user query.
You work on start, plan, action, observe mode.
For the given user query and available tools, plan the step by step execution, based on the planning,
select the relevant tool from the available tool. and based on the tool selection you perform an action to call the tool.
Wait for the observation and based on the observation from the tool call resolve the user query.
Rules:
- Follow the Output JSON Format.
- Always perform one step at a time and wait for next input
- Carefully analyse the user query
- Follow best folder structure and coding practices.
- New project should always be created in a seperate folder. And all subsequent commands will be run in new folder. Eg: cd new_folder && npm i
- Create seperate folders for database related files, controllers, middlewares, routes etc in backend project.
- Create utilities to send structured API response and error for backend projects
- Create seperate folders for components, hooks etc in frontend projects.
- Try to use '-y' flag in commands, whenever required to reduce manual interruptions.
- Never install any dependency by editing package.json file directly. Use npm install <pkg> command
- To activate virtual-environment use 'cd new-folder && .\\venv_name\\Scripts\\activate' command
- Use nodemon or --watch kind of tools to be watchful of changes.
Output JSON Format:
{{
"step": "string",
"content": "string",
"function": "The name of function if the step is action",
"input": "The input parameter for the function",
}}
Available Tools:
- run_command: Takes two parameters, 'command: string' and 'background: boolean' as input and executes command on system. If command need to be keep running in background (like npm start, uvicorn main:app --reload etc.), pass it as True and it'll return True, if execution was successful, else it's false by default and returns result of command.
- write_to_file: Takes a JSON input with 'filename' and 'content' keys. Creates or overwrites the file with the specified content.
Example:
User Query: Create a basic react project?
Output: {{ "step": "plan", "content": "The user is interseted in creating a bsic react project" }}
Output: {{ "step": "plan", "content": "Let me check if node is installed on users's system or not." }}
Output: {{ "step": "action", "function": "run_command", "input": "node -v" }}
Output: {{ "step": "observe", "output": "v22.14.0" }}
Output: {{ "step": "plan", "content": "Since npm is giving the version i.e node is installed. Now I should call run_command again to create a react project in a seperate folder" }}
Output: {{ "step": "action", "function": "run_command", "input": "npx create-react-app my-app -y" }}
Output: {{ "step": "observe", "output": "Success! Created my-app" }}
Output: {{ "step": "plan", "content": "Now I need to start the app after navigating to my-app directory" }}
Output: {{ "step": "action", "function": "run_command", "input": "cd my-app && npm start, True" }}
Output: {{ "step": "observe", "output": "True" }}
Output: {{ "step": "output", "output": "Project created successfully!" }}
Example:
User Query: Create a test.txt file in temp folder and write Hello with each character in new line.
Output: {{ "step": "plan", "content": "The user is interseted in creating a test.txt file in temp folder and write Hello in it" }}
Output: {{ "step": "plan", "content": "The available tool I found is write_to_file" }}
Output: {{ "step": "action", "function": "write_to_file", "input": "{{"filename": "temp/test.txt", "content": "H\\ne\\nl\\nl\\no"}} }}
Output: {{ "step": "observe", "output": "True" }}
Output: {{ "step": "output", "output": "File Created successfully" }}
"""
messages = [
{"role": "system", "content" : system_prompt}
]
while True:
user_query = input('> ')
messages.append({"role": "user", "content": user_query})
while True:
response = client.chat.completions.create(
model="openai/gpt-4o-mini",
response_format={"type": "json_object"},
messages = messages,
)
parsed_response = json.loads(response.choices[0].message.content)
messages.append({"role": "assistant", "content": json.dumps(parsed_response)})
if parsed_response.get("step") == "plan":
print(f"🧠 Thinking: ", parsed_response.get("content"))
continue
if parsed_response.get("step") == "action":
tool_name = parsed_response.get("function")
if available_tools.get(tool_name, False) != False:
fn_output = available_tools[tool_name]["fn"](parsed_response.get("input"))
messages.append({"role": "assistant", "content": json.dumps({ "step": "observe", "output": fn_output})})
continue
if parsed_response.get("step") == "output":
print(f"🤖: {parsed_response.get("content")}")
break
Final Thoughts
If you've carefully followed the code and logic, you'll notice something interesting—the core architecture hasn’t changed at all.
All we did was swap out the tools (APIs) the agent uses:
First, it was a weather API.
Then, it became a terminal command executor.
That’s it.
And just like that, you’ve built your own Mini Cursor.
It’s Not Magic, It’s Engineering
Cursor isn’t some black-box sorcery—it’s simply a well-orchestrated system of:
AI + tool access (via APIs),
Structured system prompts,
And intelligent orchestration logic.
Now that you understand the concept and have hands-on experience, you can imagine just how powerful things can get when you scale this architecture.
See It in Action
Curious how my version of Cursor works in real life?
Watch the demo video here:




