Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@avcodes/mi

Package Overview
Dependencies
Maintainers
1
Versions
35
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@avcodes/mi - npm Package Compare versions

Comparing version
1.0.3
to
1.0.4
+56
-27
index.mjs
#!/usr/bin/env node
import { createInterface } from 'readline'; import { spawn } from 'child_process'; import { readFileSync, writeFileSync } from 'fs';
// Import readline to handle interactive command line input, child_process to spawn external commands, fs to read and write files, and os to get the user's home directory. If the OPENAI_API_KEY environment variable is missing and the user didn't ask for help (-h), print an error to stderr and exit with a non-zero status.
import { createInterface } from 'readline'; import { spawn } from 'child_process'; import { readFileSync, writeFileSync, existsSync } from 'fs'; import { homedir } from 'os'; if (!process.env.OPENAI_API_KEY && !process.argv.includes('-h')) { console.error('OPENAI_API_KEY required'); process.exit(1); }
// Define a collection of available tools that the agent can execute.
const tools = {
bash: ({command})=>new Promise(r=>{const c=spawn('bash',['-c',command],{stdio:['ignore','pipe','pipe'],detached:true});let o='';c.stdout.on('data',d=>o+=d);c.stderr.on('data',d=>o+=d);const h=()=>{try{process.kill(-c.pid)}catch(e){}};process.on('SIGINT',h);c.on('exit',()=>{process.off('SIGINT',h);r(o)})}),
read: ({path}) => readFileSync(path,'utf8'),
write: ({path,content}) => (writeFileSync(path,content),'ok'),
};
const mkp = (...keys) => ({type:'object',properties:Object.fromEntries(keys.map(k=>[k,{type:'string'}])),required:keys});
const defs = [{name:'bash',description:'run bash cmd',parameters:mkp('command')},{name:'read',description:'read a file',parameters:mkp('path')},
{name:'write',description:'write a file',parameters:mkp('path','content')}].map(f=>({type:'function',function:f}));
async function run(msgs) { while (true) {
const base = (process.env.OPENAI_BASE_URL||'https://api.openai.com').replace(/\/+$/,'');
const r = await fetch(`${base}/v1/chat/completions`,{method:'POST',
headers:{'Content-Type':'application/json',Authorization:`Bearer ${process.env.OPENAI_API_KEY}`},
body:JSON.stringify({model:process.env.MODEL||'gpt-5.4',messages:msgs,tools:defs})}).then(r=>r.json());
const msg = r.choices?.[0]?.message; if (!msg) throw new Error(JSON.stringify(r));
msgs.push(msg); if (!msg.tool_calls) return msg.content;
for (const t of msg.tool_calls) {
const {name}=t.function, args=JSON.parse(t.function.arguments), dim=s=>`\x1b[90m${s}\x1b[0m`;
console.log(dim(`⟡ ${name}(${JSON.stringify(args)})`));
const out=String(await tools[name](args)); console.log(dim(out.length>200?out.slice(0,200)+'…':out));
msgs.push({role:'tool',tool_call_id:t.id,content:out});
}
}
}
// Define the bash tool, which takes a command and executes it in a detached bash shell, returning a promise that resolves with the output.
bash: ({command}) => new Promise(resolve => { const child = spawn('bash', ['-c', command], { stdio: ['ignore', 'pipe', 'pipe'], detached: true });
// Initialize an empty string to capture the command's output, and append any data received from stdout or stderr.
let output = ''; child.stdout.on('data', data => output += data); child.stderr.on('data', data => output += data);
// Create a cleanup function that attempts to kill the entire process group if needed, and register it to run on SIGINT (Ctrl+C).
const cleanup = () => { try { process.kill(-child.pid) } catch (err) {} }; process.on('SIGINT', cleanup);
// When the child process exits, remove the SIGINT listener and resolve the promise with the captured output.
child.on('exit', () => { process.off('SIGINT', cleanup); resolve(output); }); }),
// Define the read tool to synchronously read the contents of a file as a UTF-8 string.
read: ({path}) => readFileSync(path, 'utf8'),
// Define the write tool to synchronously write a string to a file, and return the string 'ok' to indicate success.
write: ({path,content}) => (writeFileSync(path, content), 'ok'),
// Define the skill tool to synchronously read a skill's markdown definition file from the user's .agents/skills directory.
skill: ({name}) => readFileSync(`${process.env.HOME || homedir()}/.agents/skills/${name}/SKILL.md`, 'utf8')
// Close the tools object and define a helper function makeParams that creates a JSON schema object for the function parameters given a list of property keys.
}; const makeParams = (...keys) => ({ type: 'object', properties: Object.fromEntries(keys.map(key => [key, { type: 'string' }])), required: keys });
// Create an array of tool definitions formatted for the OpenAI API, mapping our tool descriptions and parameters into the expected structure.
const toolsDef = [{ name: 'bash', description: 'run bash cmd', parameters: makeParams('command') }, { name: 'read', description: 'read a file', parameters: makeParams('path') }, { name: 'write', description: 'write a file', parameters: makeParams('path', 'content') }, { name: 'skill', description: 'load skill', parameters: makeParams('name') }].map(func => ({ type: 'function', function: func }));
// Define an asynchronous function that takes an array of messages and interacts with the chat API until a final response is reached.
async function run(messages) { while (true) {
// Send a POST request to the chat completions endpoint using the configured base URL, API key, model, messages, and tool definitions.
const response = await fetch(`${(process.env.OPENAI_BASE_URL || 'https://api.openai.com').replace(/\/+$/, '')}/v1/chat/completions`, { method: 'POST',
// Set the appropriate headers and stringify the payload body for the request, then parse the JSON response.
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.OPENAI_API_KEY}` }, body: JSON.stringify({ model: process.env.MODEL || 'gpt-5.4', messages, tools: toolsDef }) }).then(res => res.json());
// If the API response contains an error, throw an exception; otherwise, extract the message and throw if it's missing.
if (response.error) throw new Error(response.error.message || JSON.stringify(response.error)); const message = response.choices?.[0]?.message; if (!message) throw new Error(JSON.stringify(response));
// Append the assistant's message to the conversation history, and if there are no tool calls, return the final text content.
messages.push(message); if (!message.tool_calls) return message.content;
// Iterate over each tool call requested by the assistant.
for (const toolCall of message.tool_calls) {
// Extract the tool name, parse its arguments as JSON, and define a helper function to format console output in a dim color.
const {name} = toolCall.function, args = JSON.parse(toolCall.function.arguments), formatDim = str => `\x1b[90m${str}\x1b[0m`;
// Log the tool execution to the console, run the corresponding tool function with the parsed arguments, and ensure the result is a string.
console.log(formatDim(`⟡ ${name}(${JSON.stringify(args)})`)); const out = String(await tools[name](args));
// Log a truncated version of the tool output, and append the tool response to the conversation history.
console.log(formatDim(out.length > 200 ? out.slice(0, 200) + '…' : out)); messages.push({ role: 'tool', tool_call_id: toolCall.id, content: out });
// Close the tool call loop and the while loop within the run function.
} } }
// Define the system prompt with the agent's instructions, appending the current working directory and the current date/time.
const SYSTEM = (process.env.SYSTEM_PROMPT || 'You are an autonomous agent. Prefer action over speculation—use tools to answer questions and complete tasks.\nbash runs any shell command: curl/wget for HTTP, git, package managers, compilers, anything available on the system.\nread/write operate on local files. Always read before editing; write complete files.\nApproach: explore, plan, act one step at a time, verify. Be concise.') + `\nCWD: ${process.cwd()}\nDate: ${new Date().toISOString()}`;
const hist = [{role:'system',content:SYSTEM}]; const pIdx = process.argv.indexOf('-p');
if (pIdx !== -1 && process.argv[pIdx+1]) { hist.push({role:'user',content:process.argv[pIdx+1]}); console.log(await run(hist)); process.exit(0);
} else { const rl = createInterface({input:process.stdin,output:process.stdout}); const ask = q => new Promise(r=>rl.question(q,r));
rl.on('close',()=>process.exit(0)); while (true) { const i = await ask('\n> '); if (i.trim()) { hist.push({role:'user',content:i}); console.log(await run(hist)); } } }
// Initialize the message history with the system prompt, and define a helper function getArg to retrieve values for specific command-line flags.
const history = [{ role: 'system', content: SYSTEM }], getArg = key => (idx => idx >= 0 && process.argv[idx + 1])(process.argv.indexOf(key));
// If the user provided the -h flag, print usage instructions to the console and exit successfully.
if (process.argv.includes('-h')) { console.log('usage: mi [-p prompt] [-f file] [-h]\n pipe: echo "..." | mi repl: /reset clears history\nenv: OPENAI_API_KEY, MODEL, OPENAI_BASE_URL, SYSTEM_PROMPT'); process.exit(0); }
// Check for the -f file flag, append its contents to the system prompt if present, and also append the AGENTS.md file if it exists.
const fileArg = getArg('-f'); if (fileArg) history[0].content += `\n\nFile (${fileArg}):\n` + readFileSync(fileArg, 'utf8'); if (existsSync('AGENTS.md')) history[0].content += '\n' + readFileSync('AGENTS.md', 'utf8');
// If a prompt was provided via the -p flag, add it to the history, run the agent, log the final response, and exit.
if (getArg('-p')) { history.push({ role: 'user', content: getArg('-p') }); console.log(await run(history)); process.exit(0); }
// If the script is receiving input via a pipe (non-TTY stdin), read all the data, add it as a user message, run the agent, and exit.
if (!process.stdin.isTTY) { let inputStr = ''; for await (const chunk of process.stdin) inputStr += chunk; history.push({ role: 'user', content: inputStr.trim() }); console.log(await run(history)); process.exit(0); }
// Create a readline interface for interactive console input and output, and define a helper function to prompt the user.
const readLine = createInterface({ input: process.stdin, output: process.stdout }); const promptUser = query => new Promise(resolve => readLine.question(query, resolve));
// Exit when the readline interface is closed; otherwise enter a loop to continually prompt the user, handle /reset commands, and pass input to the agent.
readLine.on('close', () => process.exit(0)); while (true) { const input = await promptUser('\n> '); if (input === '/reset') { history.splice(1); continue; } if (input.trim()) { history.push({ role: 'user', content: input }); console.log(await run(history)); } }
{
"name": "@avcodes/mi",
"version": "1.0.3",
"version": "1.0.4",
"description": "agentic coding in 30 loc. a loop, three tools, and an llm.",

@@ -12,3 +12,11 @@ "type": "module",

],
"keywords": ["ai", "agent", "coding-agent", "llm", "repl", "cli", "openai"],
"keywords": [
"ai",
"agent",
"coding-agent",
"llm",
"repl",
"cli",
"openai"
],
"license": "MIT",

@@ -15,0 +23,0 @@ "repository": {

+131
-3
![Splash image](./assets/splash.png)
agentic coding in 30 loc. a loop, three tools, and an llm.
https://github.com/user-attachments/assets/9289d105-5a40-442d-b1b5-773723c95c13
agentic coding in 30 loc. a loop, four tools, and an llm.
## features
- `bash`, `read` and `write` tools
- chat REPL by default
- `bash`, `read`, `write`, and `skill` tools
- `skill` tool loads agent skills from `~/.agents/skills/`
- accepts stdin via pipes (e.g. `echo "do this" | mi`)
- file context ingestion via `-f <file>` argument
- automatic ingestion of `AGENTS.md` if it exists in current directory
- chat REPL by default with interactive `/reset` command to clear history
- graceful `SIGINT` (Ctrl+C) handling for bash child processes
- non-interactive mode with `-p 'prompt'` arg

@@ -43,1 +50,122 @@

| `SYSTEM_PROMPT` | built-in agent prompt | override the system prompt entirely |
## deep dive
an agentic harness is surprisingly simple. it's a loop that calls an llm, checks if it wants to use tools, executes them, feeds results back, and repeats. here's how each part works.
### tools
the agent needs to affect the outside world. tools are just functions that take structured args and return a string. four tools is enough for a general-purpose coding agent:
```js
const tools = {
bash: ({ command }) => execShell(command), // run any shell command
read: ({ path }) => readFileSync(path, 'utf8'), // read a file
write: ({ path, content }) => (writeFileSync(path, content), 'ok'), // write a file
};
```
`bash` gives the agent access to the entire system: git, curl, compilers, package managers. `read` and `write` handle files. every tool returns a string because that's what goes back into the conversation.
### tool definitions
the llm doesn't see your functions. it sees json schemas that describe what tools are available and what arguments they accept:
```js
const defs = [
{ name: 'bash', description: 'run bash cmd', parameters: mkp('command') },
{ name: 'read', description: 'read a file', parameters: mkp('path') },
{ name: 'write', description: 'write a file', parameters: mkp('path', 'content') },
].map(f => ({ type: 'function', function: f }));
```
`mkp` is a helper that builds a json schema object from a list of key names. each key becomes a required string property. the `defs` array is sent along with every api call so the model knows what it can do.
### messages
the conversation is a flat array of message objects. each message has a `role` (`system`, `user`, `assistant`, or `tool`) and `content`. this array is the agent's entire memory:
```js
const hist = [{ role: 'system', content: SYSTEM }];
// user says something
hist.push({ role: 'user', content: 'fix the bug in server.js' });
// assistant replies (pushed inside the loop)
// tool results get pushed too (role: 'tool')
```
the system message sets the agent's personality and context (working directory, date). every user message, assistant response, and tool result gets appended. the model sees the full history on each call, which is how it maintains context across multiple tool uses.
### the api call
each iteration makes a single call to the chat completions endpoint. the model receives the full message history and the tool definitions:
```js
const r = await fetch(`${base}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },
body: JSON.stringify({ model, messages: msgs, tools: defs }),
}).then(r => r.json());
const msg = r.choices[0].message;
```
the response message either has `content` (a text reply to the user) or `tool_calls` (the model wants to use tools). this is the decision point that drives the whole loop.
### the agentic loop
this is the core of the harness. it's a `while (true)` that keeps calling the llm until it responds with text instead of tool calls:
```js
async function run(msgs) {
while (true) {
const msg = await callLLM(msgs); // make the api call
msgs.push(msg); // add assistant response to history
if (!msg.tool_calls) return msg.content; // no tools? we're done
// otherwise, execute tools and continue...
}
}
```
the loop exits only when the model decides it has enough information to respond directly. the model might call tools once or twenty times, it drives its own execution. this is what makes it *agentic*: the llm decides when it's done, not the code.
### tool execution
when the model returns `tool_calls`, the harness executes each one and pushes the result back into the message history as a `tool` message:
```js
for (const t of msg.tool_calls) {
const { name } = t.function;
const args = JSON.parse(t.function.arguments);
const result = String(await tools[name](args));
msgs.push({ role: 'tool', tool_call_id: t.id, content: result });
}
```
each tool result is tagged with the `tool_call_id` so the model knows which call it corresponds to. after all tool results are pushed, the loop goes back to the top and calls the llm again, now with the tool outputs in context.
### the repl
the outer shell is a simple read-eval-print loop. it reads user input, pushes it as a user message, calls `run()`, and prints the result:
```js
while (true) {
const input = await ask('\n> ');
if (input.trim()) {
hist.push({ role: 'user', content: input });
console.log(await run(hist));
}
}
```
there's also a one-shot mode (`-p 'prompt'`) that skips the repl and exits after a single run. both modes use the same `run()` function. the agentic loop doesn't care where the prompt came from.
### putting it together
the full flow looks like this:
```
user prompt → [system, user] → llm → tool_calls? → execute tools → [tool results] → llm → ... → text response
```
more sophisticated agents add things like memory, retries, parallel tool calls, or multi-agent delegation, but the core is always: **loop, call, check for tools, execute, repeat**.